refactor(build-logic): make everything top-level

This commit is contained in:
Harsh Shandilya 2022-12-04 20:13:08 +05:30
parent f48ef0206f
commit 1267cf81e3
No known key found for this signature in database
21 changed files with 48 additions and 88 deletions

View file

@ -0,0 +1,102 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
@file:Suppress("UnstableApiUsage")
package dev.msfjarvis.claw.gradle
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.dsl.Lint
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import org.gradle.android.AndroidCacheFixPlugin
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.findByType
private const val SLIM_TESTS_PROPERTY = "slimTests"
class AndroidCommonPlugin : Plugin<Project> {
private companion object {
const val COMPILE_SDK = 33
const val MIN_SDK = 26
const val TARGET_SDK = 33
}
override fun apply(project: Project) {
project.configureSlimTests()
project.pluginManager.apply(AndroidCacheFixPlugin::class)
project.extensions.findByType<BaseExtension>()?.run {
compileSdkVersion(COMPILE_SDK)
defaultConfig {
minSdk = MIN_SDK
targetSdk = TARGET_SDK
}
packagingOptions {
resources.excludes.add("**/*.version")
resources.excludes.add("**/*.txt")
resources.excludes.add("**/*.kotlin_module")
resources.excludes.add("**/plugin.properties")
resources.excludes.add("**/META-INF/AL2.0")
resources.excludes.add("**/META-INF/LGPL2.1")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
testOptions {
animationsDisabled = true
unitTests.isReturnDefaultValues = true
}
}
project.extensions.findByType<BaseAppModuleExtension>()?.run { lint.configureLint(project) }
project.extensions.findByType<LibraryExtension>()?.run { lint.configureLint(project) }
}
}
private fun Lint.configureLint(project: Project) {
abortOnError = false
checkReleaseBuilds = false
warningsAsErrors = false
disable.add("DialogFragmentCallbacksDetector")
baseline = project.file("lint-baseline.xml")
}
/**
* When the "slimTests" project property is provided, disable the unit test tasks on `release` build
* type and `nonFree` product flavor to avoid running the same tests repeatedly in different build
* variants.
*
* Examples: `./gradlew test -PslimTests` will run unit tests for `nonFreeDebug` and `debug` build
* variants in Android App and Library projects, and all tests in JVM projects.
*/
private fun Project.configureSlimTests() {
if (providers.gradleProperty(SLIM_TESTS_PROPERTY).isPresent) {
// Disable unit test tasks on the release build type for Android Library projects
extensions.findByType<LibraryAndroidComponentsExtension>()?.run {
beforeVariants(selector().withBuildType("release")) {
it.enableUnitTest = false
it.enableAndroidTest = false
}
}
// Disable unit test tasks on the release build type for Android Application projects.
extensions.findByType<ApplicationAndroidComponentsExtension>()?.run {
beforeVariants(selector().withBuildType("release")) {
it.enableUnitTest = false
it.enableAndroidTest = false
}
}
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
@file:Suppress("UnstableApiUsage")
package dev.msfjarvis.claw.gradle
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import dev.msfjarvis.claw.gradle.signing.configureBuildSigning
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
@Suppress("Unused")
class ApplicationPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply(AppPlugin::class)
project.pluginManager.apply(AndroidCommonPlugin::class)
project.extensions.getByType<BaseAppModuleExtension>().run {
adbOptions.installOptions("--user 0")
dependenciesInfo {
includeInBundle = false
includeInApk = false
}
buildFeatures {
viewBinding = true
buildConfig = true
}
buildTypes {
named("release") {
isMinifyEnabled = true
setProguardFiles(
listOf(
"proguard-android-optimize.pro",
"proguard-rules.pro",
"proguard-rules-missing-classes.pro",
)
)
}
named("debug") {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
isMinifyEnabled = false
}
}
project.configureBuildSigning()
}
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import com.github.benmanes.gradle.versions.VersionsPlugin
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
import nl.littlerobots.vcu.plugin.VersionCatalogUpdateExtension
import nl.littlerobots.vcu.plugin.VersionCatalogUpdatePlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
@Suppress("Unused")
class DependencyUpdatesPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply(VersionsPlugin::class)
project.pluginManager.apply(VersionCatalogUpdatePlugin::class)
project.tasks.withType<DependencyUpdatesTask>().configureEach {
rejectVersionIf {
when (candidate.group) {
"com.squareup.okhttp3",
"org.jetbrains.kotlin" -> true
else -> false
}
}
checkForGradleUpdate = true
}
project.extensions.getByType<VersionCatalogUpdateExtension>().run {
keep.keepUnusedLibraries.set(true)
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import io.gitlab.arturbosch.detekt.DetektPlugin
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
object Detekt {
private const val TWITTER_RULES_VERSION = "0.0.26"
fun apply(project: Project) {
project.pluginManager.apply(DetektPlugin::class.java)
project.extensions.configure<DetektExtension> {
debug = project.providers.gradleProperty("debugDetekt").isPresent
parallel = true
ignoredBuildTypes = listOf("benchmark", "release")
basePath = project.layout.projectDirectory.toString()
baseline =
project.rootProject.layout.projectDirectory
.dir("detekt-baselines")
.file("${project.name}.xml")
.asFile
}
project.dependencies.add(
"detektPlugins",
"com.twitter.compose.rules:detekt:$TWITTER_RULES_VERSION",
)
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
@Suppress("Unused")
class KotlinAndroidPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.run {
apply(KotlinAndroidPluginWrapper::class)
apply(KotlinCommonPlugin::class)
}
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@Suppress("Unused")
class KotlinCommonPlugin : Plugin<Project> {
override fun apply(project: Project) {
Detekt.apply(project)
project.tasks.run {
withType<JavaCompile>().configureEach {
sourceCompatibility = JavaVersion.VERSION_11.toString()
targetCompatibility = JavaVersion.VERSION_11.toString()
}
withType<KotlinCompile>().configureEach {
kotlinOptions {
allWarningsAsErrors =
false // project.providers.environmentVariable("GITHUB_WORKFLOW").isPresent
jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs = freeCompilerArgs + ADDITIONAL_COMPILER_ARGS
languageVersion = "1.7"
}
}
withType<Test>().configureEach {
maxParallelForks = Runtime.getRuntime().availableProcessors() * 2
testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
useJUnitPlatform()
}
}
}
private companion object {
private val ADDITIONAL_COMPILER_ARGS =
listOf(
"-opt-in=kotlin.RequiresOptIn",
)
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright © 2021-2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.internal.Kapt3GradleSubplugin
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper
@Suppress("Unused")
class KotlinKaptPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.run {
apply(KotlinAndroidPluginWrapper::class)
apply(Kapt3GradleSubplugin::class)
}
project.afterEvaluate {
project.extensions.getByType<KaptExtension>().run {
javacOptions {
if (hasDaggerCompilerDependency()) {
// https://dagger.dev/dev-guide/compiler-options#fastinit-mode
option("-Adagger.fastInit=enabled")
// Enable the better, experimental error messages
// https://github.com/google/dagger/commit/0d2505a727b54f47b8677f42dd4fc5c1924e37f5
option("-Adagger.experimentalDaggerErrorMessages=enabled")
// KAPT nests errors causing real issues to be suppressed in CI logs
option("-Xmaxerrs", 500)
// Enables per-module validation for faster error detection
// https://github.com/google/dagger/commit/325b516ac6a53d3fc973d247b5231fafda9870a2
option("-Adagger.moduleBindingValidation=ERROR")
}
}
}
}
project.tasks
.matching { it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") }
.configureEach { enabled = false }
}
private fun Project.hasDaggerCompilerDependency(): Boolean {
return configurations.any {
it.dependencies.any { dependency -> dependency.name == "dagger-compiler" }
}
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
@Suppress("Unused")
class KotlinLibraryPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply(KotlinCommonPlugin::class)
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import com.android.build.gradle.LibraryPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
@Suppress("Unused")
class LibraryPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply(LibraryPlugin::class)
project.pluginManager.apply(AndroidCommonPlugin::class)
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.VariantOutputConfiguration
import dev.msfjarvis.claw.gradle.artifacts.CollectApksTask
import dev.msfjarvis.claw.gradle.artifacts.CollectBundleTask
import java.util.Locale
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.register
@Suppress("Unused")
class RenameArtifactsPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.withPlugin("com.android.application") {
project.extensions.getByType<ApplicationAndroidComponentsExtension>().run {
onVariants { variant ->
val taskPrefix = "collect${variant.name.capitalize(Locale.ROOT)}"
project.tasks.register<CollectApksTask>("${taskPrefix}Apks") {
variantName.set(variant.name)
apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
mappingFile.set(variant.artifacts.get(SingleArtifact.OBFUSCATION_MAPPING_FILE))
builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
outputDirectory.set(project.layout.projectDirectory.dir("apk"))
}
project.tasks.register<CollectBundleTask>("${taskPrefix}Bundle") {
val mainOutput =
variant.outputs.single {
it.outputType == VariantOutputConfiguration.OutputType.SINGLE
}
variantName.set(variant.name)
versionName.set(mainOutput.versionName)
mappingFile.set(variant.artifacts.get(SingleArtifact.OBFUSCATION_MAPPING_FILE))
bundleFile.set(variant.artifacts.get(SingleArtifact.BUNDLE))
outputDirectory.set(project.layout.projectDirectory.dir("bundle"))
}
}
}
}
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle
import com.diffplug.gradle.spotless.SpotlessExtension
import com.diffplug.gradle.spotless.SpotlessPlugin
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
@Suppress("Unused")
class SpotlessPlugin : Plugin<Project> {
override fun apply(project: Project) {
if (project.rootProject != project) {
throw GradleException("Spotless plugin must only be applied to the root project.")
}
project.pluginManager.apply(SpotlessPlugin::class)
project.extensions.getByType<SpotlessExtension>().run {
kotlin {
ktfmt(KTFMT_VERSION).googleStyle()
target("**/*.kt")
targetExclude("**/build/", "/spotless/")
licenseHeaderFile(project.file("spotless/license.kt"))
}
kotlinGradle {
ktfmt(KTFMT_VERSION).googleStyle()
target("**/*.kts")
targetExclude("**/build/")
licenseHeaderFile(project.file("spotless/license.kt"), "import|plugins|@file")
}
format("xml") {
target("**/*.xml")
targetExclude("**/build/", ".idea/")
trimTrailingWhitespace()
indentWithSpaces()
endWithNewline()
}
}
}
private companion object {
private const val KTFMT_VERSION = "0.41"
}
}

View file

@ -0,0 +1,67 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle.artifacts
import com.android.build.api.variant.BuiltArtifactsLoader
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
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.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
/** Task to collect APKs in a given [outputDirectory]. */
@CacheableTask
abstract class CollectApksTask : DefaultTask() {
@get:InputFiles @get:PathSensitive(PathSensitivity.NONE) abstract val apkFolder: DirectoryProperty
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val mappingFile: RegularFileProperty
@get:Input abstract val variantName: Property<String>
@get:Internal abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
@TaskAction
fun run() {
val outputDir = outputDirectory.asFile.get()
val outputDirStream =
Files.walk(outputDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile)
outputDirStream.forEach(File::delete)
outputDirStream.close()
outputDir.mkdirs()
val builtArtifacts =
builtArtifactsLoader.get().load(apkFolder.get()) ?: error("Cannot load APKs")
builtArtifacts.elements.forEach { artifact ->
Files.copy(
Paths.get(artifact.outputFile),
outputDir.resolve("Claw-${variantName.get()}-${artifact.versionName}.apk").toPath(),
StandardCopyOption.REPLACE_EXISTING,
)
}
Files.copy(
mappingFile.get().asFile.toPath(),
outputDir.resolve("mapping.txt").toPath(),
StandardCopyOption.REPLACE_EXISTING,
)
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle.artifacts
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
abstract class CollectBundleTask : DefaultTask() {
@get:InputFile abstract val bundleFile: RegularFileProperty
@get:InputFile abstract val mappingFile: RegularFileProperty
@get:Input abstract val variantName: Property<String>
@get:Input abstract val versionName: Property<String>
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
@TaskAction
fun taskAction() {
val outputDir = outputDirectory.asFile.get()
val outputDirStream =
Files.walk(outputDir.toPath()).sorted(Comparator.reverseOrder()).map(Path::toFile)
outputDirStream.forEach(File::delete)
outputDirStream.close()
outputDir.mkdirs()
Files.copy(
bundleFile.get().asFile.toPath(),
outputDir.resolve("Claw-${variantName.get()}-${versionName.get()}.aab").toPath(),
StandardCopyOption.REPLACE_EXISTING,
)
Files.copy(
mappingFile.get().asFile.toPath(),
outputDir.resolve("mapping.txt").toPath(),
StandardCopyOption.REPLACE_EXISTING,
)
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.gradle.signing
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import java.util.Properties
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
private const val KEYSTORE_CONFIG_PATH = "keystore.properties"
/** Configure signing for all build types. */
@Suppress("UnstableApiUsage")
internal fun Project.configureBuildSigning() {
val keystoreConfigFile = rootProject.layout.projectDirectory.file(KEYSTORE_CONFIG_PATH)
if (keystoreConfigFile.asFile.exists()) {
extensions.configure<BaseAppModuleExtension> {
val contents = providers.fileContents(keystoreConfigFile).asText
val keystoreProperties = Properties()
keystoreProperties.load(contents.get().byteInputStream())
signingConfigs {
register("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
val signingConfig = signingConfigs.getByName("release")
buildTypes.all { setSigningConfig(signingConfig) }
}
}
}

View file

@ -0,0 +1,15 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.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.
#"""

View file

@ -0,0 +1,89 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.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<Project> {
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<AppPlugin> {
androidAppPluginApplied.set(true)
extensions.getByType<ApplicationAndroidComponentsExtension>().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<VersioningTask>("clearPreRelease") {
description = "Remove the pre-release suffix from the version"
semverString.set(version.withClearedSuffix().toString())
propertyFile.set(propFile)
}
tasks.register<VersioningTask>("bumpMajor") {
description = "Increment the major version"
semverString.set(version.withIncMajor().withClearedSuffix().toString())
propertyFile.set(propFile)
}
tasks.register<VersioningTask>("bumpMinor") {
description = "Increment the minor version"
semverString.set(version.withIncMinor().withClearedSuffix().toString())
propertyFile.set(propFile)
}
tasks.register<VersioningTask>("bumpPatch") {
description = "Increment the patch version"
semverString.set(version.withIncPatch().withClearedSuffix().toString())
propertyFile.set(propFile)
}
tasks.register<VersioningTask>("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"
}
}
}
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright © 2022 Harsh Shandilya.
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/
package dev.msfjarvis.claw.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<String>
@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())
}
}