diff --git a/build-logic/android-plugins/build.gradle.kts b/build-logic/android-plugins/build.gradle.kts index 2586ab4f..825ccb0f 100644 --- a/build-logic/android-plugins/build.gradle.kts +++ b/build-logic/android-plugins/build.gradle.kts @@ -42,10 +42,15 @@ gradlePlugin { id = "dev.msfjarvis.claw.rename-artifacts" implementationClass = "dev.msfjarvis.aps.gradle.RenameArtifactsPlugin" } + register("versioning") { + id = "dev.msfjarvis.claw.versioning-plugin" + implementationClass = "dev.msfjarvis.aps.gradle.versioning.VersioningPlugin" + } } } dependencies { implementation(libs.build.agp) implementation(libs.build.cachefix) + implementation(libs.build.semver) } diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/Constants.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/Constants.kt new file mode 100644 index 00000000..46b8ccf9 --- /dev/null +++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/Constants.kt @@ -0,0 +1,9 @@ +package dev.msfjarvis.aps.gradle.versioning + +const val VERSIONING_PROP_FILE = "version.properties" +const val VERSIONING_PROP_VERSION_NAME = "versioning-plugin.versionName" +const val VERSIONING_PROP_VERSION_CODE = "versioning-plugin.versionCode" +const val VERSIONING_PROP_COMMENT = + """# +# This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY. +#""" diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningPlugin.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningPlugin.kt new file mode 100644 index 00000000..7863808a --- /dev/null +++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningPlugin.kt @@ -0,0 +1,83 @@ +package dev.msfjarvis.aps.gradle.versioning + +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.VariantOutputConfiguration +import com.android.build.gradle.internal.plugins.AppPlugin +import com.vdurmont.semver4j.Semver +import java.util.Properties +import java.util.concurrent.atomic.AtomicBoolean +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.register +import org.gradle.kotlin.dsl.withType + +/** + * A Gradle [Plugin] that takes a [Project] with the [AppPlugin] applied and dynamically sets the + * versionCode and versionName properties based on values read from a [VERSIONING_PROP_FILE] file in + * the [Project.getBuildDir] directory. It also adds Gradle tasks to bump the major, minor, and + * patch versions along with one to prepare the next snapshot. + */ +@Suppress("Unused") +class VersioningPlugin : Plugin { + + override fun apply(project: Project) { + with(project) { + val androidAppPluginApplied = AtomicBoolean(false) + val propFile = layout.projectDirectory.file(VERSIONING_PROP_FILE) + require(propFile.asFile.exists()) { + "A 'version.properties' file must exist in the project subdirectory to use this plugin" + } + val contents = providers.fileContents(propFile).asText + val versionProps = Properties().also { it.load(contents.get().byteInputStream()) } + val versionName = + requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) { + "version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property" + } + val versionCode = + requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_CODE).toInt()) { + "version.properties must contain a '$VERSIONING_PROP_VERSION_CODE' property" + } + project.plugins.withType { + androidAppPluginApplied.set(true) + extensions.getByType().onVariants { variant -> + val mainOutput = + variant.outputs.single { it.outputType == VariantOutputConfiguration.OutputType.SINGLE } + mainOutput.versionName.set(versionName) + mainOutput.versionCode.set(versionCode) + } + } + val version = Semver(versionName) + tasks.register("clearPreRelease") { + description = "Remove the pre-release suffix from the version" + semverString.set(version.withClearedSuffix().toString()) + propertyFile.set(propFile) + } + tasks.register("bumpMajor") { + description = "Increment the major version" + semverString.set(version.withIncMajor().withClearedSuffix().toString()) + propertyFile.set(propFile) + } + tasks.register("bumpMinor") { + description = "Increment the minor version" + semverString.set(version.withIncMinor().withClearedSuffix().toString()) + propertyFile.set(propFile) + } + tasks.register("bumpPatch") { + description = "Increment the patch version" + semverString.set(version.withIncPatch().withClearedSuffix().toString()) + propertyFile.set(propFile) + } + tasks.register("bumpSnapshot") { + description = "Increment the minor version and add the `SNAPSHOT` suffix" + semverString.set(version.withIncMinor().withSuffix("SNAPSHOT").toString()) + propertyFile.set(propFile) + } + afterEvaluate { + check(androidAppPluginApplied.get()) { + "Plugin 'com.android.application' must be applied to ${project.displayName} to use the Versioning Plugin" + } + } + } + } +} diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningTask.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningTask.kt new file mode 100644 index 00000000..3e026564 --- /dev/null +++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningTask.kt @@ -0,0 +1,45 @@ +package dev.msfjarvis.aps.gradle.versioning + +import com.vdurmont.semver4j.Semver +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +@CacheableTask +abstract class VersioningTask : DefaultTask() { + @get:Input abstract val semverString: Property + + @get:OutputFile abstract val propertyFile: RegularFileProperty + + /** Generate the Android 'versionCode' property */ + private fun Semver.androidCode(): Int { + return major * 1_00_00 + minor * 1_00 + patch + } + + private fun Semver.toPropFileText(): String { + val newVersionCode = androidCode() + val newVersionName = toString() + return buildString { + appendLine(VERSIONING_PROP_COMMENT) + append(VERSIONING_PROP_VERSION_CODE) + append('=') + appendLine(newVersionCode) + append(VERSIONING_PROP_VERSION_NAME) + append('=') + appendLine(newVersionName) + } + } + + override fun getGroup(): String { + return "versioning" + } + + @TaskAction + fun execute() { + propertyFile.get().asFile.writeText(Semver(semverString.get()).toPropFileText()) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4848a51f..c7d0f195 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,6 +52,7 @@ build-cachefix = "org.gradle.android.cache-fix:org.gradle.android.cache-fix.grad build-kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } build-kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } build-spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.10.0" +build-semver = "com.vdurmont:semver4j:3.1.0" build-vcu = "nl.littlerobots.version-catalog-update:nl.littlerobots.version-catalog-update.gradle.plugin:0.6.1" build-versions = "com.github.ben-manes:gradle-versions-plugin:0.42.0" coil = { module = "io.coil-kt:coil", version.ref = "coil" }