diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee5faa..9b51404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +### Added + +- The plugin now generates the text file `build/projectversion.txt` containing the + complete semantic version number of the plugin. + ## [2.0.0] - 2024-10-26 - The plugin has been migrated from JSR 305 to [JSpecify](https://jspecify.dev/) for `null` checking diff --git a/README.md b/README.md index e91d7b5..b881048 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ enforces the following: * The project version is an instance of the ProjectVersion class * Release builds do not depend on snapshot versions of C Thing Software artifacts +When applied to the root project, the plugin generates the file `build/projectversion.txt` +containing the complete semantic version of the project. + +The plugin provides the `version` task, which displays the complete semantic version of the +project on the standard output. + ## Usage The plugin is available from the diff --git a/build.gradle.kts b/build.gradle.kts index e491c6d..9ac3a72 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,7 +30,7 @@ buildscript { } } -version = ProjectVersion("2.0.1", BuildType.snapshot) +version = ProjectVersion("3.0.0", BuildType.snapshot) group = "org.cthing" description = "A Gradle plugin that establishes the versioning scheme for C Thing Software projects." diff --git a/src/main/java/org/cthing/gradle/plugins/versioning/VersioningPlugin.java b/src/main/java/org/cthing/gradle/plugins/versioning/VersioningPlugin.java index a2a2c02..24e8bea 100644 --- a/src/main/java/org/cthing/gradle/plugins/versioning/VersioningPlugin.java +++ b/src/main/java/org/cthing/gradle/plugins/versioning/VersioningPlugin.java @@ -5,6 +5,12 @@ package org.cthing.gradle.plugins.versioning; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.regex.Pattern; @@ -14,6 +20,8 @@ import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Dependency; +import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.tasks.TaskExecutionException; /** @@ -22,6 +30,7 @@ *
  * version = ProjectVersion("1.2.3", BuildType.snapshot)
  * 
+ * *

* The plugin also enforces that release builds only depend on release versions of C Thing Software * artifacts. @@ -31,13 +40,20 @@ public class VersioningPlugin implements Plugin { public static final String VERSION_TASK_NAME = "version"; + private static final String VERSION_FILE_TASK_NAME = "projectVersionFile"; + private static final String VERSION_FILE_TASK_PATH = ":" + VERSION_FILE_TASK_NAME; private static final Pattern SNAPHOT_VERSION_PATTERN = Pattern.compile(".*(?:\\+|-SNAPSHOT|-\\d+)$"); + private static final String PROJECT_VERSION_FILENAME = "projectversion.txt"; private static final Set CTHING_GROUPS = Set.of("com.cthing", "org.cthing"); private static final Set BUILD_CONFIGS = Set.of("api", "compileOnly", "compileOnlyApi", "implementation", "runtimeOnly"); + private static final List BUILD_RELATED_FILES = List.of("gradle.properties", + "settings.gradle", + "settings.gradle.kts", + "gradle/libs.versions.toml"); @Override public void apply(final Project project) { @@ -52,11 +68,11 @@ public void apply(final Project project) { // Validate that a release build only depends on release build internal artifacts. validateReleaseDependencies(project); - project.getTasks().register(VERSION_TASK_NAME, task -> { - task.setGroup("Help"); - task.setDescription("Display project version number"); - task.doFirst(t -> System.out.println(project.getVersion())); - }); + // Create the task that writes the version file. + createVersionFileTask(project); + + // Create the task that displays the version. + createVersionTask(project); } } @@ -76,9 +92,8 @@ private void validateReleaseDependencies(final Project project) { final String group = dep.getGroup(); if (group != null && CTHING_GROUPS.contains(group) && (version == null || SNAPHOT_VERSION_PATTERN.matcher(version).matches())) { - proj.getLogger().error( - "Release build depends on snapshot artifact {}:{}:{} ({})", - group, dep.getName(), version, config.getName()); + proj.getLogger().error("Release build depends on snapshot artifact {}:{}:{} ({})", + group, dep.getName(), version, config.getName()); throw new GradleException("Release build depends on snapshot artifacts"); } } @@ -88,4 +103,77 @@ private void validateReleaseDependencies(final Project project) { } }); } + + /** + * Create the task that writes the version file. + * + * @param project Project whose version is to be written + */ + private void createVersionFileTask(final Project project) { + // Only write the file if this is the root project and "clean" is not the only task. + if (project.equals(project.getRootProject()) && isNotCleanOnly(project)) { + project.getTasks().register(VERSION_FILE_TASK_NAME, task -> { + final File buildDir = project.getLayout().getBuildDirectory().get().getAsFile(); + final File projectVersionFile = new File(buildDir, PROJECT_VERSION_FILENAME); + + // The project version file is the output + task.getOutputs().file(projectVersionFile); + + // Build related files are the inputs. + project.getAllprojects().forEach(proj -> task.getInputs().file(proj.getBuildFile())); + BUILD_RELATED_FILES.forEach(filename -> { + final File file = project.file(filename); + if (file.exists()) { + task.getInputs().file(file); + } + }); + + // If "clean" is one of the tasks, generate the project version file after it has run + if (project.getTasks().findByName(BasePlugin.CLEAN_TASK_NAME) != null) { + task.mustRunAfter(BasePlugin.CLEAN_TASK_NAME); + } + + task.doLast(t -> { + try { + Files.writeString(projectVersionFile.toPath(), + project.getRootProject().getVersion().toString(), + StandardCharsets.UTF_8); + } catch (final IOException ex) { + throw new TaskExecutionException(task, ex); + } + }); + }); + + // Ensure that the task is always considered for execution. + final List taskNames = new ArrayList<>(project.getGradle().getStartParameter().getTaskNames()); + taskNames.add(VERSION_FILE_TASK_PATH); + project.getGradle().getStartParameter().setTaskNames(taskNames); + } + } + + /** + * Creates the task to display the project version. + * + * @param project Project whose version is to be displayed + */ + private void createVersionTask(final Project project) { + project.getTasks().register(VERSION_TASK_NAME, task -> { + task.setGroup("Help"); + task.setDescription("Display project version number"); + task.doFirst(t -> System.out.println(project.getVersion())); + }); + } + + /** + * Indicates whether the build will run more than just the 'clean' task. + * + * @param project Root project to test for tasks + * @return {@code true} if the there are no tasks specified (i.e. use default tasks) or the tasks include more + * than just "clean". + */ + private static boolean isNotCleanOnly(final Project project) { + final List taskNames = project.getGradle().getStartParameter().getTaskNames(); + return taskNames.isEmpty() + || taskNames.stream().anyMatch(name -> !BasePlugin.CLEAN_TASK_NAME.equals(name)); + } } diff --git a/src/test/java/org/cthing/gradle/plugins/versioning/PluginApplyTest.java b/src/test/java/org/cthing/gradle/plugins/versioning/PluginApplyTest.java index 5527e9b..768bec4 100644 --- a/src/test/java/org/cthing/gradle/plugins/versioning/PluginApplyTest.java +++ b/src/test/java/org/cthing/gradle/plugins/versioning/PluginApplyTest.java @@ -23,6 +23,9 @@ public void testApply(@TempDir final File projectDir) { final Project project = ProjectBuilder.builder().withName("testProject").withProjectDir(projectDir).build(); project.getPluginManager().apply("org.cthing.cthing-versioning"); + final Task versionFileTask = project.getTasks().findByName("projectVersionFile"); + assertThat(versionFileTask).isNotNull(); + final Task versionTask = project.getTasks().findByName("version"); assertThat(versionTask).isNotNull(); } diff --git a/src/test/java/org/cthing/gradle/plugins/versioning/PluginIntegTest.java b/src/test/java/org/cthing/gradle/plugins/versioning/PluginIntegTest.java index e41f697..cb162a7 100644 --- a/src/test/java/org/cthing/gradle/plugins/versioning/PluginIntegTest.java +++ b/src/test/java/org/cthing/gradle/plugins/versioning/PluginIntegTest.java @@ -33,7 +33,7 @@ public class PluginIntegTest { public static Stream gradleVersionProvider() { return Stream.of( arguments("8.0"), - arguments("8.10.2") + arguments("8.11.1") ); } @@ -72,11 +72,12 @@ public void testGoodVersion(final String gradleVersion) throws IOException { version = ProjectVersion("1.2.3", BuildType.snapshot) """); - final BuildResult result = createGradleRunner(gradleVersion).build(); + final BuildResult result = createGradleRunner(gradleVersion, "version").build(); final BuildTask versionTask = result.task(":version"); assertThat(versionTask).isNotNull(); assertThat(versionTask.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(result.getOutput()).contains("1.2.3"); + assertThat(this.projectDir.resolve("build/projectversion.txt")).exists(); } @ParameterizedTest @@ -91,8 +92,9 @@ public void testStringVersion(final String gradleVersion) throws IOException { version = "1.2.3" """); - final UnexpectedBuildFailure exception = catchThrowableOfType(UnexpectedBuildFailure.class, - () -> createGradleRunner(gradleVersion).build()); + final UnexpectedBuildFailure exception = + catchThrowableOfType(UnexpectedBuildFailure.class, + () -> createGradleRunner(gradleVersion, "version").build()); final BuildResult result = exception.getBuildResult(); assertThat(result.getOutput()).contains("Version is not an instance of org.cthing.projectversion.ProjectVersion"); } @@ -130,16 +132,88 @@ public void testRelaseWithSnapshots(final String gradleVersion) throws IOExcepti } """); - final UnexpectedBuildFailure exception = catchThrowableOfType(UnexpectedBuildFailure.class, - () -> createGradleRunner(gradleVersion).build()); + final UnexpectedBuildFailure exception = + catchThrowableOfType(UnexpectedBuildFailure.class, + () -> createGradleRunner(gradleVersion, "version").build()); final BuildResult result = exception.getBuildResult(); assertThat(result.getOutput()).contains("Release build depends on snapshot artifact org.cthing:versionparser:4.+ (implementation)"); } - private GradleRunner createGradleRunner(final String gradleVersion) { + @ParameterizedTest + @MethodSource("gradleVersionProvider") + public void testVersionFile(final String gradleVersion) throws IOException { + Files.writeString(this.projectDir.resolve("settings.gradle.kts"), "rootProject.name=\"test\""); + Files.writeString(this.projectDir.resolve("build.gradle.kts"), """ + import org.cthing.projectversion.BuildType + import org.cthing.projectversion.ProjectVersion + + repositories { + mavenCentral() + } + + plugins { + id("org.cthing.cthing-versioning") + } + + buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.cthing:cthing-projectversion:1.0.0") + } + } + + version = ProjectVersion("1.2.3", BuildType.snapshot) + """); + + final BuildResult result = createGradleRunner(gradleVersion, "projectVersionFile").build(); + final BuildTask versionFileTask = result.task(":projectVersionFile"); + assertThat(versionFileTask).isNotNull(); + assertThat(versionFileTask.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(this.projectDir.resolve("build/projectversion.txt")).exists(); + } + + @ParameterizedTest + @MethodSource("gradleVersionProvider") + public void testVersionFileCleanOnly(final String gradleVersion) throws IOException { + Files.writeString(this.projectDir.resolve("settings.gradle.kts"), "rootProject.name=\"test\""); + Files.writeString(this.projectDir.resolve("build.gradle.kts"), """ + import org.cthing.projectversion.BuildType + import org.cthing.projectversion.ProjectVersion + + repositories { + mavenCentral() + } + + plugins { + id("java") + id("org.cthing.cthing-versioning") + } + + buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.cthing:cthing-projectversion:1.0.0") + } + } + + version = ProjectVersion("1.2.3", BuildType.snapshot) + """); + + final BuildResult result = createGradleRunner(gradleVersion, "clean").build(); + final BuildTask cleanTask = result.task(":clean"); + assertThat(cleanTask).isNotNull(); + assertThat(cleanTask.getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE); + assertThat(this.projectDir.resolve("build/projectversion.txt")).doesNotExist(); + } + + private GradleRunner createGradleRunner(final String gradleVersion, final String... arguments) { return GradleRunner.create() .withProjectDir(this.projectDir.toFile()) - .withArguments("version") + .withArguments(arguments) .withPluginClasspath() .withEnvironment(Map.of("CTHING_CI", "true")) .withGradleVersion(gradleVersion);