aboutsummaryrefslogtreecommitdiff
path: root/build-logic/android-plugins/src/main/kotlin/dev
diff options
context:
space:
mode:
Diffstat (limited to 'build-logic/android-plugins/src/main/kotlin/dev')
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/AndroidCommon.kt47
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/ApplicationPlugin.kt81
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/LibraryPlugin.kt15
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/PublishedAndroidLibraryPlugin.kt40
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/RenameArtifactsPlugin.kt37
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/SentryPlugin.kt48
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/artifacts/CollectApksTask.kt44
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/artifacts/CollectBundleTask.kt33
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/flavors/ProductFlavors.kt15
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/flavors/SlimTests.kt44
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/signing/AppSigning.kt36
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/snapshot/SnapshotExtension.kt10
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/Constants.kt14
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningPlugin.kt71
-rw-r--r--build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningTask.kt46
15 files changed, 581 insertions, 0 deletions
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/AndroidCommon.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/AndroidCommon.kt
new file mode 100644
index 00000000..63679850
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/AndroidCommon.kt
@@ -0,0 +1,47 @@
+package dev.msfjarvis.aps.gradle
+
+import com.android.build.gradle.TestedExtension
+import dev.msfjarvis.aps.gradle.flavors.configureSlimTests
+import org.gradle.api.JavaVersion
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+
+@Suppress("UnstableApiUsage")
+object AndroidCommon {
+ fun configure(project: Project) {
+ project.extensions.configure<TestedExtension> {
+ setCompileSdkVersion(31)
+ defaultConfig {
+ minSdk = 23
+ targetSdk = 31
+ }
+
+ sourceSets {
+ named("main") { java.srcDirs("src/main/kotlin") }
+ named("test") { java.srcDirs("src/test/kotlin") }
+ named("androidTest") { java.srcDirs("src/androidTest/kotlin") }
+ }
+
+ 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.configureSlimTests()
+ }
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/ApplicationPlugin.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/ApplicationPlugin.kt
new file mode 100644
index 00000000..79c3a4a0
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/ApplicationPlugin.kt
@@ -0,0 +1,81 @@
+@file:Suppress("UnstableApiUsage")
+
+package dev.msfjarvis.aps.gradle
+
+import com.android.build.gradle.AppPlugin
+import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
+import dev.msfjarvis.aps.gradle.flavors.FlavorDimensions
+import dev.msfjarvis.aps.gradle.flavors.ProductFlavors
+import dev.msfjarvis.aps.gradle.signing.configureBuildSigning
+import dev.msfjarvis.aps.gradle.snapshot.SnapshotExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.the
+
+@Suppress("Unused")
+class ApplicationPlugin : Plugin<Project> {
+
+ override fun apply(project: Project) {
+ project.pluginManager.apply(AppPlugin::class)
+ AndroidCommon.configure(project)
+ project.extensions.getByType<BaseAppModuleExtension>().run {
+ val minifySwitch = project.providers.environmentVariable("DISABLE_MINIFY")
+
+ adbOptions.installOptions("--user 0")
+
+ dependenciesInfo {
+ includeInBundle = false
+ includeInApk = false
+ }
+
+ buildFeatures {
+ viewBinding = true
+ buildConfig = true
+ }
+
+ buildTypes {
+ named("release") {
+ isMinifyEnabled = !minifySwitch.isPresent
+ setProguardFiles(
+ listOf(
+ "proguard-android-optimize.txt",
+ "proguard-rules.pro",
+ "proguard-rules-missing-classes.pro",
+ )
+ )
+ buildConfigField("boolean", "ENABLE_DEBUG_FEATURES", "${project.isSnapshot()}")
+ }
+ named("debug") {
+ applicationIdSuffix = ".debug"
+ versionNameSuffix = "-debug"
+ isMinifyEnabled = false
+ buildConfigField("boolean", "ENABLE_DEBUG_FEATURES", "true")
+ }
+ }
+
+ flavorDimensions.add(FlavorDimensions.FREE)
+ productFlavors {
+ register(ProductFlavors.FREE) {}
+ register(ProductFlavors.NON_FREE) {}
+ }
+
+ project.configureBuildSigning()
+ }
+
+ project.dependencies {
+ extensions.add("snapshot", SnapshotExtension::class.java)
+ the<SnapshotExtension>().snapshot = project.isSnapshot()
+ }
+ }
+
+ private fun Project.isSnapshot(): Boolean {
+ with(project.providers) {
+ val workflow = environmentVariable("GITHUB_WORKFLOW")
+ val snapshot = environmentVariable("SNAPSHOT")
+ return workflow.isPresent || snapshot.isPresent
+ }
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/LibraryPlugin.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/LibraryPlugin.kt
new file mode 100644
index 00000000..4d2c7ee3
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/LibraryPlugin.kt
@@ -0,0 +1,15 @@
+package dev.msfjarvis.aps.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)
+ AndroidCommon.configure(project)
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/PublishedAndroidLibraryPlugin.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/PublishedAndroidLibraryPlugin.kt
new file mode 100644
index 00000000..6e9c3d56
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/PublishedAndroidLibraryPlugin.kt
@@ -0,0 +1,40 @@
+@file:Suppress("UnstableApiUsage")
+
+package dev.msfjarvis.aps.gradle
+
+import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
+import com.vanniktech.maven.publish.MavenPublishBaseExtension
+import com.vanniktech.maven.publish.MavenPublishBasePlugin
+import com.vanniktech.maven.publish.SonatypeHost
+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.provideDelegate
+import org.gradle.plugins.signing.SigningExtension
+import org.gradle.plugins.signing.SigningPlugin
+
+@Suppress("Unused")
+class PublishedAndroidLibraryPlugin : Plugin<Project> {
+
+ override fun apply(project: Project) {
+ project.plugins.run {
+ apply(LibraryPlugin::class)
+ apply(MavenPublishBasePlugin::class)
+ apply(SigningPlugin::class)
+ }
+ project.extensions.getByType<MavenPublishBaseExtension>().run {
+ publishToMavenCentral(SonatypeHost.DEFAULT)
+ signAllPublications()
+ configure(AndroidSingleVariantLibrary())
+ pomFromGradleProperties()
+ }
+ project.afterEvaluate {
+ project.extensions.getByType<SigningExtension>().run {
+ val signingKey: String? by project
+ val signingPassword: String? by project
+ useInMemoryPgpKeys(signingKey, signingPassword)
+ }
+ }
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/RenameArtifactsPlugin.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/RenameArtifactsPlugin.kt
new file mode 100644
index 00000000..971e3245
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/RenameArtifactsPlugin.kt
@@ -0,0 +1,37 @@
+package dev.msfjarvis.aps.gradle
+
+import com.android.build.api.artifact.SingleArtifact
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
+import dev.msfjarvis.aps.gradle.artifacts.CollectApksTask
+import dev.msfjarvis.aps.gradle.artifacts.CollectBundleTask
+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") {
+ val android = project.extensions.getByType<BaseAppModuleExtension>()
+ project.extensions.getByType<ApplicationAndroidComponentsExtension>().run {
+ onVariants { variant ->
+ project.tasks.register<CollectApksTask>("collect${variant.name.capitalize()}Apks") {
+ variantName.set(variant.name)
+ apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
+ builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
+ outputDirectory.set(project.layout.projectDirectory.dir("outputs"))
+ }
+ project.tasks.register<CollectBundleTask>("collect${variant.name.capitalize()}Bundle") {
+ variantName.set(variant.name)
+ versionName.set(android.defaultConfig.versionName)
+ bundleFile.set(variant.artifacts.get(SingleArtifact.BUNDLE))
+ outputDirectory.set(project.layout.projectDirectory.dir("outputs"))
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/SentryPlugin.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/SentryPlugin.kt
new file mode 100644
index 00000000..2e5991e4
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/SentryPlugin.kt
@@ -0,0 +1,48 @@
+package dev.msfjarvis.aps.gradle
+
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import dev.msfjarvis.aps.gradle.flavors.FlavorDimensions
+import dev.msfjarvis.aps.gradle.flavors.ProductFlavors
+import io.sentry.android.gradle.SentryPlugin
+import io.sentry.android.gradle.extensions.InstrumentationFeature
+import io.sentry.android.gradle.extensions.SentryPluginExtension
+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 SentryPlugin : Plugin<Project> {
+
+ override fun apply(project: Project) {
+ project.pluginManager.withPlugin("com.android.application") {
+ project.extensions.getByType<ApplicationAndroidComponentsExtension>().run {
+ onVariants(selector().withFlavor(FlavorDimensions.FREE to ProductFlavors.NON_FREE)) {
+ variant ->
+ val sentryDsn = project.providers.environmentVariable(SENTRY_DSN_PROPERTY)
+ if (sentryDsn.isPresent) {
+ variant.manifestPlaceholders.put("sentryDsn", sentryDsn.get())
+ }
+ }
+ }
+ project.plugins.apply(SentryPlugin::class)
+ project.extensions.getByType<SentryPluginExtension>().run {
+ autoUploadProguardMapping.set(
+ project.providers.gradleProperty(SENTRY_UPLOAD_MAPPINGS_PROPERTY).isPresent
+ )
+ ignoredBuildTypes.set(setOf("debug"))
+ ignoredFlavors.set(setOf(ProductFlavors.FREE))
+ tracingInstrumentation {
+ enabled.set(true)
+ features.set(setOf(InstrumentationFeature.FILE_IO))
+ }
+ autoInstallation.enabled.set(false)
+ }
+ }
+ }
+
+ private companion object {
+ private const val SENTRY_DSN_PROPERTY = "SENTRY_DSN"
+ private const val SENTRY_UPLOAD_MAPPINGS_PROPERTY = "sentryUploadMappings"
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/artifacts/CollectApksTask.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/artifacts/CollectApksTask.kt
new file mode 100644
index 00000000..39c95a9e
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/artifacts/CollectApksTask.kt
@@ -0,0 +1,44 @@
+package dev.msfjarvis.aps.gradle.artifacts
+
+import com.android.build.api.variant.BuiltArtifactsLoader
+import java.nio.file.Files
+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.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+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: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()
+ outputDir.mkdirs()
+ val builtArtifacts =
+ builtArtifactsLoader.get().load(apkFolder.get()) ?: throw RuntimeException("Cannot load APKs")
+ builtArtifacts.elements.forEach { artifact ->
+ Files.copy(
+ Paths.get(artifact.outputFile),
+ outputDir.resolve("APS-${variantName.get()}-${artifact.versionName}.apk").toPath(),
+ StandardCopyOption.REPLACE_EXISTING,
+ )
+ }
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/artifacts/CollectBundleTask.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/artifacts/CollectBundleTask.kt
new file mode 100644
index 00000000..c454d49f
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/artifacts/CollectBundleTask.kt
@@ -0,0 +1,33 @@
+package dev.msfjarvis.aps.gradle.artifacts
+
+import java.nio.file.Files
+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: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()
+ outputDir.mkdirs()
+ Files.copy(
+ bundleFile.get().asFile.toPath(),
+ outputDir.resolve("APS-${variantName.get()}-${versionName.get()}.aab").toPath(),
+ StandardCopyOption.REPLACE_EXISTING,
+ )
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/flavors/ProductFlavors.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/flavors/ProductFlavors.kt
new file mode 100644
index 00000000..be474bce
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/flavors/ProductFlavors.kt
@@ -0,0 +1,15 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.gradle.flavors
+
+object FlavorDimensions {
+ const val FREE = "free"
+}
+
+object ProductFlavors {
+ const val FREE = "free"
+ const val NON_FREE = "nonFree"
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/flavors/SlimTests.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/flavors/SlimTests.kt
new file mode 100644
index 00000000..d89d0ba7
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/flavors/SlimTests.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.gradle.flavors
+
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.findByType
+import org.gradle.language.nativeplatform.internal.BuildType
+
+/**
+ * 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.
+ */
+internal 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(BuildType.RELEASE.name)) {
+ it.enableUnitTest = false
+ it.enableAndroidTest = false
+ }
+ }
+
+ // disable unit test tasks on the release build type and free flavor for Android Application
+ // projects.
+ extensions.findByType<ApplicationAndroidComponentsExtension>()?.run {
+ beforeVariants(selector().withBuildType(BuildType.RELEASE.name)) { it.enableUnitTest = false }
+ beforeVariants(selector().withFlavor(FlavorDimensions.FREE to ProductFlavors.NON_FREE)) {
+ it.enableUnitTest = false
+ it.enableAndroidTest = false
+ }
+ }
+ }
+}
+
+private const val SLIM_TESTS_PROPERTY = "slimTests"
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/signing/AppSigning.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/signing/AppSigning.kt
new file mode 100644
index 00000000..8da96230
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/signing/AppSigning.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.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) }
+ }
+ }
+}
diff --git a/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/snapshot/SnapshotExtension.kt b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/snapshot/SnapshotExtension.kt
new file mode 100644
index 00000000..ba6265cd
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/snapshot/SnapshotExtension.kt
@@ -0,0 +1,10 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.gradle.snapshot
+
+abstract class SnapshotExtension {
+ abstract var snapshot: Boolean
+}
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..0d6f5c67
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/Constants.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+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..6b604e75
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningPlugin.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.gradle.versioning
+
+import com.android.build.gradle.internal.plugins.AppPlugin
+import com.vdurmont.semver4j.Semver
+import java.util.Properties
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.register
+
+/**
+ * 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 appPlugin =
+ requireNotNull(plugins.findPlugin(AppPlugin::class.java)) {
+ "Plugin 'com.android.application' must be applied to use this plugin"
+ }
+ 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"
+ }
+ appPlugin.extension.defaultConfig.versionName = versionName
+ appPlugin.extension.defaultConfig.versionCode = versionCode
+ afterEvaluate {
+ val version = Semver(versionName)
+ tasks.register<VersioningTask>("clearPreRelease") {
+ semverString.set(version.withClearedSuffix().toString())
+ propertyFile.set(propFile)
+ }
+ tasks.register<VersioningTask>("bumpMajor") {
+ semverString.set(version.withIncMajor().withClearedSuffix().toString())
+ propertyFile.set(propFile)
+ }
+ tasks.register<VersioningTask>("bumpMinor") {
+ semverString.set(version.withIncMinor().withClearedSuffix().toString())
+ propertyFile.set(propFile)
+ }
+ tasks.register<VersioningTask>("bumpPatch") {
+ semverString.set(version.withIncPatch().withClearedSuffix().toString())
+ propertyFile.set(propFile)
+ }
+ tasks.register<VersioningTask>("bumpSnapshot") {
+ semverString.set(version.withIncMinor().withSuffix("SNAPSHOT").toString())
+ propertyFile.set(propFile)
+ }
+ }
+ }
+ }
+}
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..762cf57b
--- /dev/null
+++ b/build-logic/android-plugins/src/main/kotlin/dev/msfjarvis/aps/gradle/versioning/VersioningTask.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+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<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)
+ }
+ }
+
+ @TaskAction
+ fun execute() {
+ propertyFile.get().asFile.writeText(Semver(semverString.get()).toPropFileText())
+ }
+}