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);