From afd0eebdd3287d932c53879fa1ac88f430080ef5 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 2 Dec 2022 01:57:02 +0530 Subject: refactor(build-logic): move all code to a single top-level project --- build-logic/android-plugins/build.gradle.kts | 64 ---------- .../app/passwordstore/gradle/AndroidCommon.kt | 55 -------- .../app/passwordstore/gradle/ApplicationPlugin.kt | 81 ------------ .../app/passwordstore/gradle/LibraryPlugin.kt | 15 --- .../gradle/PublishedAndroidLibraryPlugin.kt | 47 ------- .../passwordstore/gradle/RenameArtifactsPlugin.kt | 40 ------ .../app/passwordstore/gradle/SentryPlugin.kt | 44 ------- .../gradle/artifacts/CollectApksTask.kt | 44 ------- .../gradle/artifacts/CollectBundleTask.kt | 33 ----- .../passwordstore/gradle/flavors/ProductFlavors.kt | 15 --- .../app/passwordstore/gradle/flavors/SlimTests.kt | 44 ------- .../app/passwordstore/gradle/signing/AppSigning.kt | 36 ------ .../gradle/snapshot/SnapshotExtension.kt | 10 -- .../passwordstore/gradle/versioning/Constants.kt | 14 --- .../gradle/versioning/VersioningPlugin.kt | 88 ------------- .../gradle/versioning/VersioningTask.kt | 50 -------- build-logic/automation-plugins/build.gradle.kts | 48 ------- .../app/passwordstore/gradle/GitHooksPlugin.kt | 18 --- .../gradle/crowdin/CrowdinExtension.kt | 20 --- .../passwordstore/gradle/crowdin/CrowdinPlugin.kt | 140 --------------------- .../app/passwordstore/gradle/psl/PSLUpdateTask.kt | 116 ----------------- .../gradle/psl/PublicSuffixListPlugin.kt | 20 --- .../app/passwordstore/gradle/tasks/GitHooks.kt | 54 -------- build-logic/build.gradle.kts | 112 +++++++++++++++++ build-logic/kotlin-plugins/build.gradle.kts | 67 ---------- .../gradle/DependencyUpdatesPlugin.kt | 33 ----- .../passwordstore/gradle/KotlinAndroidPlugin.kt | 22 ---- .../app/passwordstore/gradle/KotlinCommonPlugin.kt | 68 ---------- .../app/passwordstore/gradle/KotlinKaptPlugin.kt | 55 -------- .../passwordstore/gradle/KotlinLibraryPlugin.kt | 27 ---- .../kotlin/app/passwordstore/gradle/KtfmtPlugin.kt | 32 ----- .../passwordstore/gradle/ktfmt/KtfmtCheckTask.kt | 68 ---------- .../passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt | 3 - .../app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt | 35 ------ .../passwordstore/gradle/ktfmt/KtfmtFormatTask.kt | 56 --------- build-logic/settings.gradle.kts | 6 - .../app/passwordstore/gradle/AndroidCommon.kt | 55 ++++++++ .../app/passwordstore/gradle/ApplicationPlugin.kt | 81 ++++++++++++ .../gradle/DependencyUpdatesPlugin.kt | 33 +++++ .../app/passwordstore/gradle/GitHooksPlugin.kt | 18 +++ .../passwordstore/gradle/KotlinAndroidPlugin.kt | 22 ++++ .../app/passwordstore/gradle/KotlinCommonPlugin.kt | 68 ++++++++++ .../app/passwordstore/gradle/KotlinKaptPlugin.kt | 55 ++++++++ .../passwordstore/gradle/KotlinLibraryPlugin.kt | 27 ++++ .../kotlin/app/passwordstore/gradle/KtfmtPlugin.kt | 32 +++++ .../app/passwordstore/gradle/LibraryPlugin.kt | 15 +++ .../gradle/PublishedAndroidLibraryPlugin.kt | 47 +++++++ .../passwordstore/gradle/RenameArtifactsPlugin.kt | 40 ++++++ .../app/passwordstore/gradle/SentryPlugin.kt | 44 +++++++ .../gradle/artifacts/CollectApksTask.kt | 44 +++++++ .../gradle/artifacts/CollectBundleTask.kt | 33 +++++ .../gradle/crowdin/CrowdinExtension.kt | 20 +++ .../passwordstore/gradle/crowdin/CrowdinPlugin.kt | 140 +++++++++++++++++++++ .../passwordstore/gradle/flavors/ProductFlavors.kt | 15 +++ .../app/passwordstore/gradle/flavors/SlimTests.kt | 44 +++++++ .../passwordstore/gradle/ktfmt/KtfmtCheckTask.kt | 68 ++++++++++ .../passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt | 3 + .../app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt | 35 ++++++ .../passwordstore/gradle/ktfmt/KtfmtFormatTask.kt | 56 +++++++++ .../app/passwordstore/gradle/psl/PSLUpdateTask.kt | 116 +++++++++++++++++ .../gradle/psl/PublicSuffixListPlugin.kt | 20 +++ .../app/passwordstore/gradle/signing/AppSigning.kt | 36 ++++++ .../gradle/snapshot/SnapshotExtension.kt | 10 ++ .../app/passwordstore/gradle/tasks/GitHooks.kt | 54 ++++++++ .../passwordstore/gradle/versioning/Constants.kt | 14 +++ .../gradle/versioning/VersioningPlugin.kt | 88 +++++++++++++ .../gradle/versioning/VersioningTask.kt | 50 ++++++++ 67 files changed, 1495 insertions(+), 1568 deletions(-) delete mode 100644 build-logic/android-plugins/build.gradle.kts delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/AndroidCommon.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/ApplicationPlugin.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/LibraryPlugin.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/PublishedAndroidLibraryPlugin.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/RenameArtifactsPlugin.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/SentryPlugin.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectApksTask.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectBundleTask.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/flavors/ProductFlavors.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/flavors/SlimTests.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/signing/AppSigning.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/snapshot/SnapshotExtension.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/Constants.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningPlugin.kt delete mode 100644 build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningTask.kt delete mode 100644 build-logic/automation-plugins/build.gradle.kts delete mode 100644 build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/GitHooksPlugin.kt delete mode 100644 build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt delete mode 100644 build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt delete mode 100644 build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/psl/PSLUpdateTask.kt delete mode 100644 build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/psl/PublicSuffixListPlugin.kt delete mode 100644 build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/tasks/GitHooks.kt create mode 100644 build-logic/build.gradle.kts delete mode 100644 build-logic/kotlin-plugins/build.gradle.kts delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/DependencyUpdatesPlugin.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinAndroidPlugin.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinCommonPlugin.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinKaptPlugin.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinLibraryPlugin.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt delete mode 100644 build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/AndroidCommon.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/ApplicationPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/DependencyUpdatesPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/GitHooksPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinAndroidPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinCommonPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinKaptPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinLibraryPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/LibraryPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/PublishedAndroidLibraryPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/RenameArtifactsPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/SentryPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectApksTask.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectBundleTask.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/flavors/ProductFlavors.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/flavors/SlimTests.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/psl/PSLUpdateTask.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/psl/PublicSuffixListPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/signing/AppSigning.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/snapshot/SnapshotExtension.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/tasks/GitHooks.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/Constants.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningPlugin.kt create mode 100644 build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningTask.kt (limited to 'build-logic') diff --git a/build-logic/android-plugins/build.gradle.kts b/build-logic/android-plugins/build.gradle.kts deleted file mode 100644 index 23a14da0..00000000 --- a/build-logic/android-plugins/build.gradle.kts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") - -import org.gradle.api.JavaVersion -import org.gradle.api.tasks.compile.JavaCompile -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { `kotlin-dsl` } - -afterEvaluate { - tasks.withType().configureEach { - sourceCompatibility = JavaVersion.VERSION_11.toString() - targetCompatibility = JavaVersion.VERSION_11.toString() - } - - tasks.withType().configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - freeCompilerArgs = freeCompilerArgs + "-Xsam-conversions=class" - } - } -} - -gradlePlugin { - plugins { - register("versioning") { - id = "com.github.android-password-store.versioning-plugin" - implementationClass = "app.passwordstore.gradle.versioning.VersioningPlugin" - } - register("android-application") { - id = "com.github.android-password-store.android-application" - implementationClass = "app.passwordstore.gradle.ApplicationPlugin" - } - register("android-library") { - id = "com.github.android-password-store.android-library" - implementationClass = "app.passwordstore.gradle.LibraryPlugin" - } - register("published-android-library") { - id = "com.github.android-password-store.published-android-library" - implementationClass = "app.passwordstore.gradle.PublishedAndroidLibraryPlugin" - } - register("rename-artifacts") { - id = "com.github.android-password-store.rename-artifacts" - implementationClass = "app.passwordstore.gradle.RenameArtifactsPlugin" - } - register("sentry") { - id = "com.github.android-password-store.sentry" - implementationClass = "app.passwordstore.gradle.SentryPlugin" - } - } -} - -dependencies { - implementation(libs.build.agp) - implementation(libs.build.mavenpublish) - implementation(libs.build.metalava) - implementation(libs.build.r8) - implementation(libs.build.semver) - implementation(libs.build.sentry) -} diff --git a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/AndroidCommon.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/AndroidCommon.kt deleted file mode 100644 index 5e38b6eb..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/AndroidCommon.kt +++ /dev/null @@ -1,55 +0,0 @@ -package app.passwordstore.gradle - -import app.passwordstore.gradle.flavors.configureSlimTests -import com.android.build.gradle.TestedExtension -import org.gradle.api.JavaVersion -import org.gradle.api.Project -import org.gradle.api.tasks.testing.Test -import org.gradle.kotlin.dsl.configure -import org.gradle.kotlin.dsl.withType - -@Suppress("UnstableApiUsage") -object AndroidCommon { - fun configure(project: Project) { - project.extensions.configure { - setCompileSdkVersion(33) - 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.tasks.withType { - jvmArgs( - "--add-opens=java.base/java.lang=ALL-UNNAMED", - "--add-opens=java.base/java.util=ALL-UNNAMED", - ) - } - - project.configureSlimTests() - } - } -} diff --git a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/ApplicationPlugin.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/ApplicationPlugin.kt deleted file mode 100644 index bf759b3c..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/ApplicationPlugin.kt +++ /dev/null @@ -1,81 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -package app.passwordstore.gradle - -import app.passwordstore.gradle.flavors.FlavorDimensions -import app.passwordstore.gradle.flavors.ProductFlavors -import app.passwordstore.gradle.signing.configureBuildSigning -import app.passwordstore.gradle.snapshot.SnapshotExtension -import com.android.build.gradle.AppPlugin -import com.android.build.gradle.internal.dsl.BaseAppModuleExtension -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 { - - override fun apply(project: Project) { - project.pluginManager.apply(AppPlugin::class) - AndroidCommon.configure(project) - project.extensions.getByType().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().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/app/passwordstore/gradle/LibraryPlugin.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/LibraryPlugin.kt deleted file mode 100644 index 22cc8ca3..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/LibraryPlugin.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.passwordstore.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 { - - override fun apply(project: Project) { - project.pluginManager.apply(LibraryPlugin::class) - AndroidCommon.configure(project) - } -} diff --git a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/PublishedAndroidLibraryPlugin.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/PublishedAndroidLibraryPlugin.kt deleted file mode 100644 index 542bfeb6..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/PublishedAndroidLibraryPlugin.kt +++ /dev/null @@ -1,47 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -package app.passwordstore.gradle - -import com.vanniktech.maven.publish.MavenPublishBaseExtension -import com.vanniktech.maven.publish.MavenPublishPlugin -import com.vanniktech.maven.publish.SonatypeHost -import me.tylerbwong.gradle.metalava.Documentation -import me.tylerbwong.gradle.metalava.extension.MetalavaExtension -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 { - - override fun apply(project: Project) { - project.plugins.run { - apply(LibraryPlugin::class) - apply(MavenPublishPlugin::class) - apply(SigningPlugin::class) - apply("me.tylerbwong.gradle.metalava") - } - project.extensions.getByType().run { - publishToMavenCentral(SonatypeHost.DEFAULT, true) - signAllPublications() - } - project.afterEvaluate { - project.extensions.getByType().run { - val signingKey: String? by project - val signingPassword: String? by project - useInMemoryPgpKeys(signingKey, signingPassword) - } - } - project.extensions.getByType().run { - documentation.set(Documentation.PUBLIC) - inputKotlinNulls.set(true) - outputKotlinNulls.set(true) - reportLintsAsErrors.set(true) - reportWarningsAsErrors.set(true) - } - } -} diff --git a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/RenameArtifactsPlugin.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/RenameArtifactsPlugin.kt deleted file mode 100644 index 6807b6ef..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/RenameArtifactsPlugin.kt +++ /dev/null @@ -1,40 +0,0 @@ -package app.passwordstore.gradle - -import app.passwordstore.gradle.artifacts.CollectApksTask -import app.passwordstore.gradle.artifacts.CollectBundleTask -import com.android.build.api.artifact.SingleArtifact -import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import com.android.build.api.variant.VariantOutputConfiguration -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 { - - override fun apply(project: Project) { - project.pluginManager.withPlugin("com.android.application") { - project.extensions.getByType().run { - onVariants { variant -> - project.tasks.register("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("collect${variant.name.capitalize()}Bundle") { - val mainOutput = - variant.outputs.single { - it.outputType == VariantOutputConfiguration.OutputType.SINGLE - } - variantName.set(variant.name) - versionName.set(mainOutput.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/app/passwordstore/gradle/SentryPlugin.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/SentryPlugin.kt deleted file mode 100644 index 034c62f9..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/SentryPlugin.kt +++ /dev/null @@ -1,44 +0,0 @@ -package app.passwordstore.gradle - -import app.passwordstore.gradle.flavors.FlavorDimensions -import app.passwordstore.gradle.flavors.ProductFlavors -import com.android.build.api.variant.ApplicationAndroidComponentsExtension -import io.sentry.android.gradle.SentryPlugin -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 { - - override fun apply(project: Project) { - project.pluginManager.withPlugin("com.android.application") { - project.extensions.getByType().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().run { - autoUploadProguardMapping.set( - project.providers.gradleProperty(SENTRY_UPLOAD_MAPPINGS_PROPERTY).isPresent - ) - ignoredBuildTypes.set(setOf("debug")) - ignoredFlavors.set(setOf(ProductFlavors.FREE)) - tracingInstrumentation { enabled.set(false) } - 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/app/passwordstore/gradle/artifacts/CollectApksTask.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectApksTask.kt deleted file mode 100644 index 4f74136c..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectApksTask.kt +++ /dev/null @@ -1,44 +0,0 @@ -package app.passwordstore.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 - - @get:Internal abstract val builtArtifactsLoader: Property - - @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/app/passwordstore/gradle/artifacts/CollectBundleTask.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectBundleTask.kt deleted file mode 100644 index b627a674..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectBundleTask.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.passwordstore.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 - - @get:Input abstract val versionName: Property - - @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/app/passwordstore/gradle/flavors/ProductFlavors.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/flavors/ProductFlavors.kt deleted file mode 100644 index e4b5c739..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/flavors/ProductFlavors.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.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/app/passwordstore/gradle/flavors/SlimTests.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/flavors/SlimTests.kt deleted file mode 100644 index 8755a872..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/flavors/SlimTests.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.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()?.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()?.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/app/passwordstore/gradle/signing/AppSigning.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/signing/AppSigning.kt deleted file mode 100644 index 4b5c7130..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/signing/AppSigning.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.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 { - 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/app/passwordstore/gradle/snapshot/SnapshotExtension.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/snapshot/SnapshotExtension.kt deleted file mode 100644 index 191620d1..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/snapshot/SnapshotExtension.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.gradle.snapshot - -abstract class SnapshotExtension { - abstract var snapshot: Boolean -} diff --git a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/Constants.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/Constants.kt deleted file mode 100644 index 52746c37..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/Constants.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.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/app/passwordstore/gradle/versioning/VersioningPlugin.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningPlugin.kt deleted file mode 100644 index 5fda1a25..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningPlugin.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.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/app/passwordstore/gradle/versioning/VersioningTask.kt b/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningTask.kt deleted file mode 100644 index 7eb19a89..00000000 --- a/build-logic/android-plugins/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningTask.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.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/build-logic/automation-plugins/build.gradle.kts b/build-logic/automation-plugins/build.gradle.kts deleted file mode 100644 index 0aa6b3ac..00000000 --- a/build-logic/automation-plugins/build.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") - -import org.gradle.api.JavaVersion -import org.gradle.api.tasks.compile.JavaCompile -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { `kotlin-dsl` } - -afterEvaluate { - tasks.withType().configureEach { - sourceCompatibility = JavaVersion.VERSION_11.toString() - targetCompatibility = JavaVersion.VERSION_11.toString() - } - - tasks.withType().configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - freeCompilerArgs = freeCompilerArgs + "-Xsam-conversions=class" - } - } -} - -gradlePlugin { - plugins { - register("crowdin") { - id = "com.github.android-password-store.crowdin-plugin" - implementationClass = "app.passwordstore.gradle.crowdin.CrowdinDownloadPlugin" - } - register("psl") { - id = "com.github.android-password-store.psl-plugin" - implementationClass = "app.passwordstore.gradle.psl.PublicSuffixListPlugin" - } - register("git-hooks") { - id = "com.github.android-password-store.git-hooks" - implementationClass = "app.passwordstore.gradle.GitHooksPlugin" - } - } -} - -dependencies { - implementation(libs.build.download) - implementation(libs.build.okhttp) -} diff --git a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/GitHooksPlugin.kt b/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/GitHooksPlugin.kt deleted file mode 100644 index f1b7c39d..00000000 --- a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/GitHooksPlugin.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.passwordstore.gradle - -import app.passwordstore.gradle.tasks.GitHooks -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.register - -@Suppress("Unused") -class GitHooksPlugin : Plugin { - - override fun apply(project: Project) { - project.tasks.register("installGitHooks") { - val projectDirectory = project.layout.projectDirectory - hookSource.set(projectDirectory.file("scripts/pre-push-hook.sh")) - hookOutput.set(projectDirectory.file(".git/hooks/pre-push")) - } - } -} diff --git a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt b/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt deleted file mode 100644 index 3d45aebc..00000000 --- a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.gradle.crowdin - -/** Extension for configuring [CrowdinDownloadPlugin] */ -interface CrowdinExtension { - - /** Configure the project name on Crowdin */ - var projectName: String - - /** - * Don't delete downloaded and extracted translation archives from build directory. - * - * Useful for debugging. - */ - var skipCleanup: Boolean -} diff --git a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt b/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt deleted file mode 100644 index 98882af5..00000000 --- a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.gradle.crowdin - -import de.undercouch.gradle.tasks.download.Download -import java.io.File -import java.util.concurrent.TimeUnit -import javax.xml.parsers.DocumentBuilderFactory -import okhttp3.OkHttpClient -import okhttp3.Request -import org.gradle.api.GradleException -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.tasks.Copy -import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.register -import org.w3c.dom.Document - -private const val EXCEPTION_MESSAGE = - """Applying `crowdin-plugin` requires a projectName to be configured via the "crowdin" extension.""" -private const val CROWDIN_BUILD_API_URL = - "https://api.crowdin.com/api/project/%s/export?login=%s&account-key=%s" - -@Suppress("Unused") -class CrowdinDownloadPlugin : Plugin { - - override fun apply(project: Project) { - with(project) { - val buildDirectory = layout.buildDirectory.asFile.get() - val extension = extensions.create("crowdin") - afterEvaluate { - val projectName = extension.projectName - if (projectName.isEmpty()) { - throw GradleException(EXCEPTION_MESSAGE) - } - val buildOnApi = - tasks.register("buildOnApi") { - doLast { - val login = providers.environmentVariable("CROWDIN_LOGIN") - val key = providers.environmentVariable("CROWDIN_PROJECT_KEY") - if (!login.isPresent) { - throw GradleException("CROWDIN_LOGIN environment variable must be set") - } - if (!key.isPresent) { - throw GradleException("CROWDIN_PROJECT_KEY environment variable must be set") - } - val client = - OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.MINUTES) - .writeTimeout(5, TimeUnit.MINUTES) - .readTimeout(5, TimeUnit.MINUTES) - .callTimeout(10, TimeUnit.MINUTES) - .build() - val url = CROWDIN_BUILD_API_URL.format(projectName, login.get(), key.get()) - val request = Request.Builder().url(url).get().build() - client.newCall(request).execute().close() - } - } - val downloadCrowdin = - tasks.register("downloadCrowdin") { - dependsOn(buildOnApi) - src("https://crowdin.com/backend/download/project/$projectName.zip") - dest("$buildDirectory/translations.zip") - overwrite(true) - } - val extractCrowdin = - tasks.register("extractCrowdin") { - dependsOn(downloadCrowdin) - doFirst { File(buildDir, "translations").deleteRecursively() } - from(zipTree("$buildDirectory/translations.zip")) - into("$buildDirectory/translations") - } - val extractStrings = - tasks.register("extractStrings") { - dependsOn(extractCrowdin) - from("$buildDirectory/translations/") - into("${projectDir}/src/") - setFinalizedBy(setOf("removeIncompleteStrings")) - } - tasks.register("removeIncompleteStrings") { - doLast { - val sourceSets = arrayOf("main", "nonFree") - for (sourceSet in sourceSets) { - val fileTreeWalk = projectDir.resolve("src/$sourceSet").walkTopDown() - val valuesDirectories = - fileTreeWalk.filter { it.isDirectory }.filter { it.name.startsWith("values") } - val stringFiles = fileTreeWalk.filter { it.name == "strings.xml" } - val sourceFile = - stringFiles.firstOrNull { it.path.endsWith("values/strings.xml") } - ?: throw GradleException("No root strings.xml found in '$sourceSet' sourceSet") - val sourceDoc = parseDocument(sourceFile) - val baselineStringCount = countStrings(sourceDoc) - val threshold = 0.80 * baselineStringCount - stringFiles.forEach { file -> - if (file != sourceFile) { - val doc = parseDocument(file) - val stringCount = countStrings(doc) - if (stringCount < threshold) { - file.delete() - } - } - } - valuesDirectories.forEach { dir -> - if (dir.listFiles().isNullOrEmpty()) { - dir.delete() - } - } - } - } - } - tasks.register("crowdin") { - dependsOn(extractStrings) - if (!extension.skipCleanup) { - doLast { - File("$buildDirectory/translations").deleteRecursively() - File("$buildDirectory/nonFree-translations").deleteRecursively() - File("$buildDirectory/translations.zip").delete() - } - } - } - } - } - } - - private fun parseDocument(file: File): Document { - val dbFactory = DocumentBuilderFactory.newInstance() - val documentBuilder = dbFactory.newDocumentBuilder() - return documentBuilder.parse(file) - } - - private fun countStrings(document: Document): Int { - // Normalization is beneficial for us - // https://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work - document.documentElement.normalize() - return document.getElementsByTagName("string").length - } -} diff --git a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/psl/PSLUpdateTask.kt b/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/psl/PSLUpdateTask.kt deleted file mode 100644 index a5de3d49..00000000 --- a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/psl/PSLUpdateTask.kt +++ /dev/null @@ -1,116 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -package app.passwordstore.gradle.psl - -import java.util.TreeSet -import okhttp3.OkHttpClient -import okhttp3.Request -import okio.ByteString -import okio.ByteString.Companion.encodeUtf8 -import okio.buffer -import okio.sink -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFile -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.TaskAction - -/** - * Based on PublicSuffixListGenerator from OkHttp: - * https://github.com/square/okhttp/blob/3ad1912f783e108b3d0ad2c4a5b1b89b827e4db9/okhttp/src/jvmTest/java/okhttp3/internal/publicsuffix/PublicSuffixListGenerator.java - */ -abstract class PSLUpdateTask : DefaultTask() { - @get:OutputFile abstract val outputFile: RegularFileProperty - - @TaskAction - fun updatePSL() { - val pslData = fetchPublicSuffixList() - writeListToDisk(outputFile.get(), pslData) - } - - private fun fetchPublicSuffixList(): PublicSuffixListData { - val client = OkHttpClient.Builder().build() - - val request = - Request.Builder().url("https://publicsuffix.org/list/public_suffix_list.dat").build() - - client.newCall(request).execute().use { response -> - val source = requireNotNull(response.body).source() - - val data = PublicSuffixListData() - - while (!source.exhausted()) { - val line = source.readUtf8LineStrict() - - if (line.trim { it <= ' ' }.isEmpty() || line.startsWith("//")) { - continue - } - - if (line.contains(WILDCARD_CHAR)) { - assertWildcardRule(line) - } - - var rule = line.encodeUtf8() - - if (rule.startsWith(EXCEPTION_RULE_MARKER)) { - rule = rule.substring(1) - // We use '\n' for end of value. - data.totalExceptionRuleBytes += rule.size + 1 - data.sortedExceptionRules.add(rule) - } else { - data.totalRuleBytes += rule.size + 1 // We use '\n' for end of value. - data.sortedRules.add(rule) - } - } - return data - } - } - - @Suppress("TooGenericExceptionThrown", "ThrowsCount") - private fun assertWildcardRule(rule: String) { - if (rule.indexOf(WILDCARD_CHAR) != 0) { - throw RuntimeException("Wildcard is not not in leftmost position") - } - - if (rule.indexOf(WILDCARD_CHAR, 1) != -1) { - throw RuntimeException("Rule contains multiple wildcards") - } - - if (rule.length == 1) { - throw RuntimeException("Rule wildcards the first level") - } - } - - private fun writeListToDisk(destination: RegularFile, data: PublicSuffixListData) { - val fileSink = destination.asFile.sink() - - fileSink.buffer().use { sink -> - sink.writeInt(data.totalRuleBytes) - - for (domain in data.sortedRules) { - sink.write(domain).writeByte('\n'.toInt()) - } - - sink.writeInt(data.totalExceptionRuleBytes) - - for (domain in data.sortedExceptionRules) { - sink.write(domain).writeByte('\n'.toInt()) - } - } - } - - data class PublicSuffixListData( - var totalRuleBytes: Int = 0, - var totalExceptionRuleBytes: Int = 0, - val sortedRules: TreeSet = TreeSet(), - val sortedExceptionRules: TreeSet = TreeSet() - ) - - private companion object { - private const val WILDCARD_CHAR = "*" - private val EXCEPTION_RULE_MARKER = "!".encodeUtf8() - } -} diff --git a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/psl/PublicSuffixListPlugin.kt b/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/psl/PublicSuffixListPlugin.kt deleted file mode 100644 index 2efeb4dd..00000000 --- a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/psl/PublicSuffixListPlugin.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.gradle.psl - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.register - -/** Gradle plugin to update the public suffix list used by the `autofill-parser` library. */ -@Suppress("Unused") -class PublicSuffixListPlugin : Plugin { - override fun apply(project: Project) { - project.tasks.register("updatePSL") { - outputFile.set(project.layout.projectDirectory.file("src/main/assets/publicsuffixes")) - } - } -} diff --git a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/tasks/GitHooks.kt b/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/tasks/GitHooks.kt deleted file mode 100644 index 3ffb4c71..00000000 --- a/build-logic/automation-plugins/src/main/kotlin/app/passwordstore/gradle/tasks/GitHooks.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.gradle.tasks - -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE -import java.nio.file.attribute.PosixFilePermission.GROUP_READ -import java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE -import java.nio.file.attribute.PosixFilePermission.OTHERS_READ -import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE -import java.nio.file.attribute.PosixFilePermission.OWNER_READ -import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE -import org.gradle.api.DefaultTask -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.tasks.CacheableTask -import org.gradle.api.tasks.InputFile -import org.gradle.api.tasks.OutputFile -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.TaskAction - -@CacheableTask -abstract class GitHooks : DefaultTask() { - @get:InputFile - @get:PathSensitive(PathSensitivity.NONE) - abstract val hookSource: RegularFileProperty - - @get:OutputFile abstract val hookOutput: RegularFileProperty - - @TaskAction - fun install() { - Files.copy( - hookSource.asFile.get().toPath(), - hookOutput.asFile.get().toPath(), - StandardCopyOption.REPLACE_EXISTING, - ) - Files.setPosixFilePermissions( - hookOutput.asFile.get().toPath(), - setOf( - OWNER_READ, - OWNER_WRITE, - OWNER_EXECUTE, - GROUP_READ, - GROUP_EXECUTE, - OTHERS_READ, - OTHERS_EXECUTE, - ) - ) - } -} diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 00000000..0740d609 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,112 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +@file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") + +import org.gradle.api.JavaVersion +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { `kotlin-dsl` } + +afterEvaluate { + tasks.withType().configureEach { + sourceCompatibility = JavaVersion.VERSION_11.toString() + targetCompatibility = JavaVersion.VERSION_11.toString() + } + + tasks.withType().configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + freeCompilerArgs = freeCompilerArgs + listOf( + "-Xsam-conversions=class", + "-opt-in=kotlin.RequiresOptIn", + ) + } + } +} + +gradlePlugin { + plugins { + register("android-application") { + id = "com.github.android-password-store.android-application" + implementationClass = "app.passwordstore.gradle.ApplicationPlugin" + } + register("android-library") { + id = "com.github.android-password-store.android-library" + implementationClass = "app.passwordstore.gradle.LibraryPlugin" + } + register("crowdin") { + id = "com.github.android-password-store.crowdin-plugin" + implementationClass = "app.passwordstore.gradle.crowdin.CrowdinDownloadPlugin" + } + register("git-hooks") { + id = "com.github.android-password-store.git-hooks" + implementationClass = "app.passwordstore.gradle.GitHooksPlugin" + } + register("kotlin-android") { + id = "com.github.android-password-store.kotlin-android" + implementationClass = "app.passwordstore.gradle.KotlinAndroidPlugin" + } + register("kotlin-common") { + id = "com.github.android-password-store.kotlin-common" + implementationClass = "app.passwordstore.gradle.KotlinCommonPlugin" + } + register("kotlin-kapt") { + id = "com.github.android-password-store.kotlin-kapt" + implementationClass = "app.passwordstore.gradle.KotlinKaptPlugin" + } + register("kotlin-library") { + id = "com.github.android-password-store.kotlin-library" + implementationClass = "app.passwordstore.gradle.KotlinLibraryPlugin" + } + register("ktfmt") { + id = "com.github.android-password-store.ktfmt" + implementationClass = "app.passwordstore.gradle.KtfmtPlugin" + } + register("published-android-library") { + id = "com.github.android-password-store.published-android-library" + implementationClass = "app.passwordstore.gradle.PublishedAndroidLibraryPlugin" + } + register("psl") { + id = "com.github.android-password-store.psl-plugin" + implementationClass = "app.passwordstore.gradle.psl.PublicSuffixListPlugin" + } + register("rename-artifacts") { + id = "com.github.android-password-store.rename-artifacts" + implementationClass = "app.passwordstore.gradle.RenameArtifactsPlugin" + } + register("sentry") { + id = "com.github.android-password-store.sentry" + implementationClass = "app.passwordstore.gradle.SentryPlugin" + } + register("versioning") { + id = "com.github.android-password-store.versioning-plugin" + implementationClass = "app.passwordstore.gradle.versioning.VersioningPlugin" + } + register("versions") { + id = "com.github.android-password-store.versions" + implementationClass = "app.passwordstore.gradle.DependencyUpdatesPlugin" + } + } +} + +dependencies { + implementation(libs.build.agp) + implementation(libs.build.detekt) + implementation(libs.build.diffutils) + implementation(libs.build.download) + implementation(libs.build.kotlin) + implementation(libs.build.ktfmt) + implementation(libs.build.mavenpublish) + implementation(libs.build.metalava) + implementation(libs.build.okhttp) + implementation(libs.build.r8) + implementation(libs.build.semver) + implementation(libs.build.sentry) + implementation(libs.build.vcu) + implementation(libs.build.versions) + implementation(libs.kotlin.coroutines.core) +} diff --git a/build-logic/kotlin-plugins/build.gradle.kts b/build-logic/kotlin-plugins/build.gradle.kts deleted file mode 100644 index fc2147c9..00000000 --- a/build-logic/kotlin-plugins/build.gradle.kts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") - -import org.gradle.api.JavaVersion -import org.gradle.api.tasks.compile.JavaCompile -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { `kotlin-dsl` } - -afterEvaluate { - tasks.withType().configureEach { - sourceCompatibility = JavaVersion.VERSION_11.toString() - targetCompatibility = JavaVersion.VERSION_11.toString() - } - - tasks.withType().configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - freeCompilerArgs = freeCompilerArgs + "-Xsam-conversions=class" + "-opt-in=kotlin.RequiresOptIn" - } - } -} - -gradlePlugin { - plugins { - register("kotlin-android") { - id = "com.github.android-password-store.kotlin-android" - implementationClass = "app.passwordstore.gradle.KotlinAndroidPlugin" - } - register("kotlin-common") { - id = "com.github.android-password-store.kotlin-common" - implementationClass = "app.passwordstore.gradle.KotlinCommonPlugin" - } - register("kotlin-kapt") { - id = "com.github.android-password-store.kotlin-kapt" - implementationClass = "app.passwordstore.gradle.KotlinKaptPlugin" - } - register("kotlin-library") { - id = "com.github.android-password-store.kotlin-library" - implementationClass = "app.passwordstore.gradle.KotlinLibraryPlugin" - } - register("ktfmt") { - id = "com.github.android-password-store.ktfmt" - implementationClass = "app.passwordstore.gradle.KtfmtPlugin" - } - register("versions") { - id = "com.github.android-password-store.versions" - implementationClass = "app.passwordstore.gradle.DependencyUpdatesPlugin" - } - } -} - -dependencies { - implementation(libs.build.agp) - implementation(libs.build.detekt) - implementation(libs.build.diffutils) - implementation(libs.build.kotlin) - implementation(libs.build.ktfmt) - implementation(libs.build.r8) - implementation(libs.build.vcu) - implementation(libs.build.versions) - implementation(libs.kotlin.coroutines.core) -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/DependencyUpdatesPlugin.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/DependencyUpdatesPlugin.kt deleted file mode 100644 index 6dff57a1..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/DependencyUpdatesPlugin.kt +++ /dev/null @@ -1,33 +0,0 @@ -package app.passwordstore.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 { - override fun apply(project: Project) { - project.pluginManager.apply(VersionsPlugin::class) - project.pluginManager.apply(VersionCatalogUpdatePlugin::class) - project.tasks.withType().configureEach { - rejectVersionIf { - when (candidate.group) { - "commons-codec", - "com.android.tools.build", - "org.eclipse.jgit" -> true - else -> false - } - } - checkForGradleUpdate = false - } - project.extensions.getByType().run { - keep.keepUnusedLibraries.set(true) - } - } -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinAndroidPlugin.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinAndroidPlugin.kt deleted file mode 100644 index 49f207a1..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinAndroidPlugin.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.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 { - - override fun apply(project: Project) { - project.pluginManager.run { - apply(KotlinAndroidPluginWrapper::class) - apply(KotlinCommonPlugin::class) - } - } -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinCommonPlugin.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinCommonPlugin.kt deleted file mode 100644 index 34d4675b..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinCommonPlugin.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.gradle - -import io.gitlab.arturbosch.detekt.DetektPlugin -import io.gitlab.arturbosch.detekt.extensions.DetektExtension -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.configure -import org.gradle.kotlin.dsl.withType -import org.gradle.language.base.plugins.LifecycleBasePlugin -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -@Suppress("Unused") -class KotlinCommonPlugin : Plugin { - - override fun apply(project: Project) { - project.pluginManager.apply(DetektPlugin::class.java) - project.extensions.configure { - parallel = true - ignoredBuildTypes = listOf("release") - ignoredFlavors = listOf("free") - basePath = project.layout.projectDirectory.toString() - baseline = - project.rootProject.layout.projectDirectory - .dir("detekt-baselines") - .file("${project.name}.xml") - .asFile - } - project.tasks.run { - project.pluginManager.withPlugin("base") { - named(LifecycleBasePlugin.CHECK_TASK_NAME).configure { this.dependsOn(named("detekt")) } - } - withType().configureEach { - sourceCompatibility = JavaVersion.VERSION_11.toString() - targetCompatibility = JavaVersion.VERSION_11.toString() - } - withType().configureEach { - kotlinOptions { - allWarningsAsErrors = true - jvmTarget = JavaVersion.VERSION_11.toString() - freeCompilerArgs = freeCompilerArgs + ADDITIONAL_COMPILER_ARGS - languageVersion = "1.5" - } - } - withType().configureEach { - maxParallelForks = Runtime.getRuntime().availableProcessors() * 2 - testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) } - } - } - } - - private companion object { - private val ADDITIONAL_COMPILER_ARGS = - listOf( - "-opt-in=kotlin.RequiresOptIn", - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.7.22", - ) - } -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinKaptPlugin.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinKaptPlugin.kt deleted file mode 100644 index a798ec8e..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinKaptPlugin.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.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 { - - override fun apply(project: Project) { - project.pluginManager.run { - apply(KotlinAndroidPluginWrapper::class) - apply(Kapt3GradleSubplugin::class) - } - project.afterEvaluate { - project.extensions.getByType().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") - // Share test components for when we start leveraging Hilt for tests - // https://github.com/google/dagger/releases/tag/dagger-2.34 - option("-Adagger.hilt.shareTestComponents=true") - // 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 == "hilt-compiler" } - } - } -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinLibraryPlugin.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinLibraryPlugin.kt deleted file mode 100644 index ad6ffc0b..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KotlinLibraryPlugin.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.gradle - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -@Suppress("Unused") -class KotlinLibraryPlugin : Plugin { - - override fun apply(project: Project) { - project.pluginManager.apply(KotlinCommonPlugin::class) - project.tasks.withType().configureEach { - kotlinOptions { - if (!name.contains("test", ignoreCase = true)) { - freeCompilerArgs = freeCompilerArgs + listOf("-Xexplicit-api=strict") - } - } - } - } -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt deleted file mode 100644 index 74e8ba95..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.passwordstore.gradle - -import app.passwordstore.gradle.ktfmt.KtfmtCheckTask -import app.passwordstore.gradle.ktfmt.KtfmtFormatTask -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.register - -class KtfmtPlugin : Plugin { - - override fun apply(target: Project) { - target.tasks.register("ktfmtFormat") { - source = - project.layout.projectDirectory.asFileTree - .filter { file -> - file.extension == "kt" || - file.extension == "kts" && !file.canonicalPath.contains("build") - } - .asFileTree - } - target.tasks.register("ktfmtCheck") { - source = - project.layout.projectDirectory.asFileTree - .filter { file -> - file.extension == "kt" || - file.extension == "kts" && !file.canonicalPath.contains("build") - } - .asFileTree - projectDirectory.set(target.layout.projectDirectory) - } - } -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt deleted file mode 100644 index a50c8494..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt +++ /dev/null @@ -1,68 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -import com.facebook.ktfmt.format.Formatter -import com.facebook.ktfmt.format.FormattingOptions -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.runBlocking -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.file.FileCollection -import org.gradle.api.tasks.IgnoreEmptyDirectories -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.SourceTask -import org.gradle.api.tasks.TaskAction - -@OptIn(ExperimentalCoroutinesApi::class) -abstract class KtfmtCheckTask : SourceTask() { - - @get:PathSensitive(PathSensitivity.RELATIVE) - @get:InputFiles - @get:IgnoreEmptyDirectories - protected val inputFiles: FileCollection - get() = super.getSource() - - @get:Internal abstract val projectDirectory: DirectoryProperty - - @TaskAction - fun execute() { - runBlocking(Dispatchers.IO.limitedParallelism(PARALLEL_TASK_LIMIT)) { - coroutineScope { - val results = inputFiles.map { async { checkFile(it) } }.awaitAll() - if (results.any { (notFormatted, _) -> notFormatted }) { - results - .map { (_, diffs) -> diffs } - .forEach { diffs -> KtfmtDiffer.printDiff(diffs, logger) } - error("[ktfmt] Found unformatted files") - } - } - } - } - - private fun checkFile(input: File): Pair> { - val originCode = input.readText() - val formattedCode = - Formatter.format( - FormattingOptions( - style = FormattingOptions.Style.GOOGLE, - maxWidth = 100, - continuationIndent = 2, - ), - originCode - ) - val pathNormalizer = { file: File -> file.toRelativeString(projectDirectory.asFile.get()) } - return (originCode != formattedCode) to - KtfmtDiffer.computeDiff(input, formattedCode, pathNormalizer) - } - - companion object { - - private const val PARALLEL_TASK_LIMIT = 4 - } -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt deleted file mode 100644 index 44d1a967..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt +++ /dev/null @@ -1,3 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -data class KtfmtDiffEntry(val input: String, val lineNumber: Int, val message: String) diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt deleted file mode 100644 index 936596cd..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -import com.github.difflib.DiffUtils -import com.github.difflib.patch.ChangeDelta -import com.github.difflib.patch.DeleteDelta -import com.github.difflib.patch.InsertDelta -import java.io.File -import org.gradle.api.logging.Logger - -object KtfmtDiffer { - fun computeDiff( - inputFile: File, - formattedCode: String, - pathNormalizer: (File) -> String - ): List { - val originCode = inputFile.readText() - return DiffUtils.diff(originCode, formattedCode, null).deltas.map { - val line = it.source.position + 1 - val message: String = - when (it) { - is ChangeDelta -> "Line changed: ${it.source.lines.first()}" - is DeleteDelta -> "Line deleted" - is InsertDelta -> "Line added" - else -> "" - } - KtfmtDiffEntry(pathNormalizer(inputFile), line, message) - } - } - - fun printDiff(entries: List, logger: Logger) { - entries.forEach { entry -> - logger.error("${entry.input}:${entry.lineNumber} - ${entry.message}") - } - } -} diff --git a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt b/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt deleted file mode 100644 index 82ce4ca3..00000000 --- a/build-logic/kotlin-plugins/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt +++ /dev/null @@ -1,56 +0,0 @@ -package app.passwordstore.gradle.ktfmt - -import com.facebook.ktfmt.format.Formatter -import com.facebook.ktfmt.format.FormattingOptions -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.runBlocking -import org.gradle.api.file.FileCollection -import org.gradle.api.tasks.IgnoreEmptyDirectories -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.PathSensitive -import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.SourceTask -import org.gradle.api.tasks.TaskAction - -@OptIn(ExperimentalCoroutinesApi::class) -abstract class KtfmtFormatTask : SourceTask() { - - @get:PathSensitive(PathSensitivity.RELATIVE) - @get:InputFiles - @get:IgnoreEmptyDirectories - protected val inputFiles: FileCollection - get() = super.getSource() - - @TaskAction - fun execute() { - runBlocking(Dispatchers.IO.limitedParallelism(PARALLEL_TASK_LIMIT)) { - coroutineScope { inputFiles.map { async { formatFile(it) } }.awaitAll() } - } - } - - private fun formatFile(input: File) { - val originCode = input.readText() - val formattedCode = - Formatter.format( - FormattingOptions( - style = FormattingOptions.Style.GOOGLE, - maxWidth = 100, - continuationIndent = 2, - ), - originCode - ) - if (originCode != formattedCode) { - input.writeText(formattedCode) - } - } - - companion object { - - private const val PARALLEL_TASK_LIMIT = 4 - } -} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 07992f1a..84b36fa8 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -61,9 +61,3 @@ dependencyResolutionManagement { } } } - -include("android-plugins") - -include("automation-plugins") - -include("kotlin-plugins") diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/AndroidCommon.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/AndroidCommon.kt new file mode 100644 index 00000000..5e38b6eb --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/AndroidCommon.kt @@ -0,0 +1,55 @@ +package app.passwordstore.gradle + +import app.passwordstore.gradle.flavors.configureSlimTests +import com.android.build.gradle.TestedExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType + +@Suppress("UnstableApiUsage") +object AndroidCommon { + fun configure(project: Project) { + project.extensions.configure { + setCompileSdkVersion(33) + 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.tasks.withType { + jvmArgs( + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=java.base/java.util=ALL-UNNAMED", + ) + } + + project.configureSlimTests() + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ApplicationPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ApplicationPlugin.kt new file mode 100644 index 00000000..bf759b3c --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/ApplicationPlugin.kt @@ -0,0 +1,81 @@ +@file:Suppress("UnstableApiUsage") + +package app.passwordstore.gradle + +import app.passwordstore.gradle.flavors.FlavorDimensions +import app.passwordstore.gradle.flavors.ProductFlavors +import app.passwordstore.gradle.signing.configureBuildSigning +import app.passwordstore.gradle.snapshot.SnapshotExtension +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.internal.dsl.BaseAppModuleExtension +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 { + + override fun apply(project: Project) { + project.pluginManager.apply(AppPlugin::class) + AndroidCommon.configure(project) + project.extensions.getByType().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().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/src/main/kotlin/app/passwordstore/gradle/DependencyUpdatesPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/DependencyUpdatesPlugin.kt new file mode 100644 index 00000000..6dff57a1 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/DependencyUpdatesPlugin.kt @@ -0,0 +1,33 @@ +package app.passwordstore.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 { + override fun apply(project: Project) { + project.pluginManager.apply(VersionsPlugin::class) + project.pluginManager.apply(VersionCatalogUpdatePlugin::class) + project.tasks.withType().configureEach { + rejectVersionIf { + when (candidate.group) { + "commons-codec", + "com.android.tools.build", + "org.eclipse.jgit" -> true + else -> false + } + } + checkForGradleUpdate = false + } + project.extensions.getByType().run { + keep.keepUnusedLibraries.set(true) + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/GitHooksPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/GitHooksPlugin.kt new file mode 100644 index 00000000..f1b7c39d --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/GitHooksPlugin.kt @@ -0,0 +1,18 @@ +package app.passwordstore.gradle + +import app.passwordstore.gradle.tasks.GitHooks +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.register + +@Suppress("Unused") +class GitHooksPlugin : Plugin { + + override fun apply(project: Project) { + project.tasks.register("installGitHooks") { + val projectDirectory = project.layout.projectDirectory + hookSource.set(projectDirectory.file("scripts/pre-push-hook.sh")) + hookOutput.set(projectDirectory.file(".git/hooks/pre-push")) + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinAndroidPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinAndroidPlugin.kt new file mode 100644 index 00000000..49f207a1 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinAndroidPlugin.kt @@ -0,0 +1,22 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.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 { + + override fun apply(project: Project) { + project.pluginManager.run { + apply(KotlinAndroidPluginWrapper::class) + apply(KotlinCommonPlugin::class) + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinCommonPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinCommonPlugin.kt new file mode 100644 index 00000000..34d4675b --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinCommonPlugin.kt @@ -0,0 +1,68 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.gradle + +import io.gitlab.arturbosch.detekt.DetektPlugin +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +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.configure +import org.gradle.kotlin.dsl.withType +import org.gradle.language.base.plugins.LifecycleBasePlugin +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +@Suppress("Unused") +class KotlinCommonPlugin : Plugin { + + override fun apply(project: Project) { + project.pluginManager.apply(DetektPlugin::class.java) + project.extensions.configure { + parallel = true + ignoredBuildTypes = listOf("release") + ignoredFlavors = listOf("free") + basePath = project.layout.projectDirectory.toString() + baseline = + project.rootProject.layout.projectDirectory + .dir("detekt-baselines") + .file("${project.name}.xml") + .asFile + } + project.tasks.run { + project.pluginManager.withPlugin("base") { + named(LifecycleBasePlugin.CHECK_TASK_NAME).configure { this.dependsOn(named("detekt")) } + } + withType().configureEach { + sourceCompatibility = JavaVersion.VERSION_11.toString() + targetCompatibility = JavaVersion.VERSION_11.toString() + } + withType().configureEach { + kotlinOptions { + allWarningsAsErrors = true + jvmTarget = JavaVersion.VERSION_11.toString() + freeCompilerArgs = freeCompilerArgs + ADDITIONAL_COMPILER_ARGS + languageVersion = "1.5" + } + } + withType().configureEach { + maxParallelForks = Runtime.getRuntime().availableProcessors() * 2 + testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) } + } + } + } + + private companion object { + private val ADDITIONAL_COMPILER_ARGS = + listOf( + "-opt-in=kotlin.RequiresOptIn", + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.7.22", + ) + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinKaptPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinKaptPlugin.kt new file mode 100644 index 00000000..a798ec8e --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinKaptPlugin.kt @@ -0,0 +1,55 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.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 { + + override fun apply(project: Project) { + project.pluginManager.run { + apply(KotlinAndroidPluginWrapper::class) + apply(Kapt3GradleSubplugin::class) + } + project.afterEvaluate { + project.extensions.getByType().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") + // Share test components for when we start leveraging Hilt for tests + // https://github.com/google/dagger/releases/tag/dagger-2.34 + option("-Adagger.hilt.shareTestComponents=true") + // 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 == "hilt-compiler" } + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinLibraryPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinLibraryPlugin.kt new file mode 100644 index 00000000..ad6ffc0b --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/KotlinLibraryPlugin.kt @@ -0,0 +1,27 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +@Suppress("Unused") +class KotlinLibraryPlugin : Plugin { + + override fun apply(project: Project) { + project.pluginManager.apply(KotlinCommonPlugin::class) + project.tasks.withType().configureEach { + kotlinOptions { + if (!name.contains("test", ignoreCase = true)) { + freeCompilerArgs = freeCompilerArgs + listOf("-Xexplicit-api=strict") + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt new file mode 100644 index 00000000..74e8ba95 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/KtfmtPlugin.kt @@ -0,0 +1,32 @@ +package app.passwordstore.gradle + +import app.passwordstore.gradle.ktfmt.KtfmtCheckTask +import app.passwordstore.gradle.ktfmt.KtfmtFormatTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.register + +class KtfmtPlugin : Plugin { + + override fun apply(target: Project) { + target.tasks.register("ktfmtFormat") { + source = + project.layout.projectDirectory.asFileTree + .filter { file -> + file.extension == "kt" || + file.extension == "kts" && !file.canonicalPath.contains("build") + } + .asFileTree + } + target.tasks.register("ktfmtCheck") { + source = + project.layout.projectDirectory.asFileTree + .filter { file -> + file.extension == "kt" || + file.extension == "kts" && !file.canonicalPath.contains("build") + } + .asFileTree + projectDirectory.set(target.layout.projectDirectory) + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/LibraryPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/LibraryPlugin.kt new file mode 100644 index 00000000..22cc8ca3 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/LibraryPlugin.kt @@ -0,0 +1,15 @@ +package app.passwordstore.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 { + + override fun apply(project: Project) { + project.pluginManager.apply(LibraryPlugin::class) + AndroidCommon.configure(project) + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/PublishedAndroidLibraryPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/PublishedAndroidLibraryPlugin.kt new file mode 100644 index 00000000..542bfeb6 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/PublishedAndroidLibraryPlugin.kt @@ -0,0 +1,47 @@ +@file:Suppress("UnstableApiUsage") + +package app.passwordstore.gradle + +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.MavenPublishPlugin +import com.vanniktech.maven.publish.SonatypeHost +import me.tylerbwong.gradle.metalava.Documentation +import me.tylerbwong.gradle.metalava.extension.MetalavaExtension +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 { + + override fun apply(project: Project) { + project.plugins.run { + apply(LibraryPlugin::class) + apply(MavenPublishPlugin::class) + apply(SigningPlugin::class) + apply("me.tylerbwong.gradle.metalava") + } + project.extensions.getByType().run { + publishToMavenCentral(SonatypeHost.DEFAULT, true) + signAllPublications() + } + project.afterEvaluate { + project.extensions.getByType().run { + val signingKey: String? by project + val signingPassword: String? by project + useInMemoryPgpKeys(signingKey, signingPassword) + } + } + project.extensions.getByType().run { + documentation.set(Documentation.PUBLIC) + inputKotlinNulls.set(true) + outputKotlinNulls.set(true) + reportLintsAsErrors.set(true) + reportWarningsAsErrors.set(true) + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/RenameArtifactsPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/RenameArtifactsPlugin.kt new file mode 100644 index 00000000..6807b6ef --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/RenameArtifactsPlugin.kt @@ -0,0 +1,40 @@ +package app.passwordstore.gradle + +import app.passwordstore.gradle.artifacts.CollectApksTask +import app.passwordstore.gradle.artifacts.CollectBundleTask +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.VariantOutputConfiguration +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 { + + override fun apply(project: Project) { + project.pluginManager.withPlugin("com.android.application") { + project.extensions.getByType().run { + onVariants { variant -> + project.tasks.register("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("collect${variant.name.capitalize()}Bundle") { + val mainOutput = + variant.outputs.single { + it.outputType == VariantOutputConfiguration.OutputType.SINGLE + } + variantName.set(variant.name) + versionName.set(mainOutput.versionName) + bundleFile.set(variant.artifacts.get(SingleArtifact.BUNDLE)) + outputDirectory.set(project.layout.projectDirectory.dir("outputs")) + } + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/SentryPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/SentryPlugin.kt new file mode 100644 index 00000000..034c62f9 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/SentryPlugin.kt @@ -0,0 +1,44 @@ +package app.passwordstore.gradle + +import app.passwordstore.gradle.flavors.FlavorDimensions +import app.passwordstore.gradle.flavors.ProductFlavors +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import io.sentry.android.gradle.SentryPlugin +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 { + + override fun apply(project: Project) { + project.pluginManager.withPlugin("com.android.application") { + project.extensions.getByType().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().run { + autoUploadProguardMapping.set( + project.providers.gradleProperty(SENTRY_UPLOAD_MAPPINGS_PROPERTY).isPresent + ) + ignoredBuildTypes.set(setOf("debug")) + ignoredFlavors.set(setOf(ProductFlavors.FREE)) + tracingInstrumentation { enabled.set(false) } + 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/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectApksTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectApksTask.kt new file mode 100644 index 00000000..4f74136c --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectApksTask.kt @@ -0,0 +1,44 @@ +package app.passwordstore.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 + + @get:Internal abstract val builtArtifactsLoader: Property + + @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/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectBundleTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectBundleTask.kt new file mode 100644 index 00000000..b627a674 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/artifacts/CollectBundleTask.kt @@ -0,0 +1,33 @@ +package app.passwordstore.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 + + @get:Input abstract val versionName: Property + + @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/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt new file mode 100644 index 00000000..3d45aebc --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinExtension.kt @@ -0,0 +1,20 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.gradle.crowdin + +/** Extension for configuring [CrowdinDownloadPlugin] */ +interface CrowdinExtension { + + /** Configure the project name on Crowdin */ + var projectName: String + + /** + * Don't delete downloaded and extracted translation archives from build directory. + * + * Useful for debugging. + */ + var skipCleanup: Boolean +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt new file mode 100644 index 00000000..98882af5 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/crowdin/CrowdinPlugin.kt @@ -0,0 +1,140 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.gradle.crowdin + +import de.undercouch.gradle.tasks.download.Download +import java.io.File +import java.util.concurrent.TimeUnit +import javax.xml.parsers.DocumentBuilderFactory +import okhttp3.OkHttpClient +import okhttp3.Request +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.Copy +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.register +import org.w3c.dom.Document + +private const val EXCEPTION_MESSAGE = + """Applying `crowdin-plugin` requires a projectName to be configured via the "crowdin" extension.""" +private const val CROWDIN_BUILD_API_URL = + "https://api.crowdin.com/api/project/%s/export?login=%s&account-key=%s" + +@Suppress("Unused") +class CrowdinDownloadPlugin : Plugin { + + override fun apply(project: Project) { + with(project) { + val buildDirectory = layout.buildDirectory.asFile.get() + val extension = extensions.create("crowdin") + afterEvaluate { + val projectName = extension.projectName + if (projectName.isEmpty()) { + throw GradleException(EXCEPTION_MESSAGE) + } + val buildOnApi = + tasks.register("buildOnApi") { + doLast { + val login = providers.environmentVariable("CROWDIN_LOGIN") + val key = providers.environmentVariable("CROWDIN_PROJECT_KEY") + if (!login.isPresent) { + throw GradleException("CROWDIN_LOGIN environment variable must be set") + } + if (!key.isPresent) { + throw GradleException("CROWDIN_PROJECT_KEY environment variable must be set") + } + val client = + OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.MINUTES) + .writeTimeout(5, TimeUnit.MINUTES) + .readTimeout(5, TimeUnit.MINUTES) + .callTimeout(10, TimeUnit.MINUTES) + .build() + val url = CROWDIN_BUILD_API_URL.format(projectName, login.get(), key.get()) + val request = Request.Builder().url(url).get().build() + client.newCall(request).execute().close() + } + } + val downloadCrowdin = + tasks.register("downloadCrowdin") { + dependsOn(buildOnApi) + src("https://crowdin.com/backend/download/project/$projectName.zip") + dest("$buildDirectory/translations.zip") + overwrite(true) + } + val extractCrowdin = + tasks.register("extractCrowdin") { + dependsOn(downloadCrowdin) + doFirst { File(buildDir, "translations").deleteRecursively() } + from(zipTree("$buildDirectory/translations.zip")) + into("$buildDirectory/translations") + } + val extractStrings = + tasks.register("extractStrings") { + dependsOn(extractCrowdin) + from("$buildDirectory/translations/") + into("${projectDir}/src/") + setFinalizedBy(setOf("removeIncompleteStrings")) + } + tasks.register("removeIncompleteStrings") { + doLast { + val sourceSets = arrayOf("main", "nonFree") + for (sourceSet in sourceSets) { + val fileTreeWalk = projectDir.resolve("src/$sourceSet").walkTopDown() + val valuesDirectories = + fileTreeWalk.filter { it.isDirectory }.filter { it.name.startsWith("values") } + val stringFiles = fileTreeWalk.filter { it.name == "strings.xml" } + val sourceFile = + stringFiles.firstOrNull { it.path.endsWith("values/strings.xml") } + ?: throw GradleException("No root strings.xml found in '$sourceSet' sourceSet") + val sourceDoc = parseDocument(sourceFile) + val baselineStringCount = countStrings(sourceDoc) + val threshold = 0.80 * baselineStringCount + stringFiles.forEach { file -> + if (file != sourceFile) { + val doc = parseDocument(file) + val stringCount = countStrings(doc) + if (stringCount < threshold) { + file.delete() + } + } + } + valuesDirectories.forEach { dir -> + if (dir.listFiles().isNullOrEmpty()) { + dir.delete() + } + } + } + } + } + tasks.register("crowdin") { + dependsOn(extractStrings) + if (!extension.skipCleanup) { + doLast { + File("$buildDirectory/translations").deleteRecursively() + File("$buildDirectory/nonFree-translations").deleteRecursively() + File("$buildDirectory/translations.zip").delete() + } + } + } + } + } + } + + private fun parseDocument(file: File): Document { + val dbFactory = DocumentBuilderFactory.newInstance() + val documentBuilder = dbFactory.newDocumentBuilder() + return documentBuilder.parse(file) + } + + private fun countStrings(document: Document): Int { + // Normalization is beneficial for us + // https://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-java-how-does-it-work + document.documentElement.normalize() + return document.getElementsByTagName("string").length + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/flavors/ProductFlavors.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/flavors/ProductFlavors.kt new file mode 100644 index 00000000..e4b5c739 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/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 app.passwordstore.gradle.flavors + +object FlavorDimensions { + const val FREE = "free" +} + +object ProductFlavors { + const val FREE = "free" + const val NON_FREE = "nonFree" +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/flavors/SlimTests.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/flavors/SlimTests.kt new file mode 100644 index 00000000..8755a872 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/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 app.passwordstore.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()?.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()?.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/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt new file mode 100644 index 00000000..a50c8494 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtCheckTask.kt @@ -0,0 +1,68 @@ +package app.passwordstore.gradle.ktfmt + +import com.facebook.ktfmt.format.Formatter +import com.facebook.ktfmt.format.FormattingOptions +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SourceTask +import org.gradle.api.tasks.TaskAction + +@OptIn(ExperimentalCoroutinesApi::class) +abstract class KtfmtCheckTask : SourceTask() { + + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFiles + @get:IgnoreEmptyDirectories + protected val inputFiles: FileCollection + get() = super.getSource() + + @get:Internal abstract val projectDirectory: DirectoryProperty + + @TaskAction + fun execute() { + runBlocking(Dispatchers.IO.limitedParallelism(PARALLEL_TASK_LIMIT)) { + coroutineScope { + val results = inputFiles.map { async { checkFile(it) } }.awaitAll() + if (results.any { (notFormatted, _) -> notFormatted }) { + results + .map { (_, diffs) -> diffs } + .forEach { diffs -> KtfmtDiffer.printDiff(diffs, logger) } + error("[ktfmt] Found unformatted files") + } + } + } + } + + private fun checkFile(input: File): Pair> { + val originCode = input.readText() + val formattedCode = + Formatter.format( + FormattingOptions( + style = FormattingOptions.Style.GOOGLE, + maxWidth = 100, + continuationIndent = 2, + ), + originCode + ) + val pathNormalizer = { file: File -> file.toRelativeString(projectDirectory.asFile.get()) } + return (originCode != formattedCode) to + KtfmtDiffer.computeDiff(input, formattedCode, pathNormalizer) + } + + companion object { + + private const val PARALLEL_TASK_LIMIT = 4 + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt new file mode 100644 index 00000000..44d1a967 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffEntry.kt @@ -0,0 +1,3 @@ +package app.passwordstore.gradle.ktfmt + +data class KtfmtDiffEntry(val input: String, val lineNumber: Int, val message: String) diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt new file mode 100644 index 00000000..936596cd --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtDiffer.kt @@ -0,0 +1,35 @@ +package app.passwordstore.gradle.ktfmt + +import com.github.difflib.DiffUtils +import com.github.difflib.patch.ChangeDelta +import com.github.difflib.patch.DeleteDelta +import com.github.difflib.patch.InsertDelta +import java.io.File +import org.gradle.api.logging.Logger + +object KtfmtDiffer { + fun computeDiff( + inputFile: File, + formattedCode: String, + pathNormalizer: (File) -> String + ): List { + val originCode = inputFile.readText() + return DiffUtils.diff(originCode, formattedCode, null).deltas.map { + val line = it.source.position + 1 + val message: String = + when (it) { + is ChangeDelta -> "Line changed: ${it.source.lines.first()}" + is DeleteDelta -> "Line deleted" + is InsertDelta -> "Line added" + else -> "" + } + KtfmtDiffEntry(pathNormalizer(inputFile), line, message) + } + } + + fun printDiff(entries: List, logger: Logger) { + entries.forEach { entry -> + logger.error("${entry.input}:${entry.lineNumber} - ${entry.message}") + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt new file mode 100644 index 00000000..82ce4ca3 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/ktfmt/KtfmtFormatTask.kt @@ -0,0 +1,56 @@ +package app.passwordstore.gradle.ktfmt + +import com.facebook.ktfmt.format.Formatter +import com.facebook.ktfmt.format.FormattingOptions +import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runBlocking +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.IgnoreEmptyDirectories +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SourceTask +import org.gradle.api.tasks.TaskAction + +@OptIn(ExperimentalCoroutinesApi::class) +abstract class KtfmtFormatTask : SourceTask() { + + @get:PathSensitive(PathSensitivity.RELATIVE) + @get:InputFiles + @get:IgnoreEmptyDirectories + protected val inputFiles: FileCollection + get() = super.getSource() + + @TaskAction + fun execute() { + runBlocking(Dispatchers.IO.limitedParallelism(PARALLEL_TASK_LIMIT)) { + coroutineScope { inputFiles.map { async { formatFile(it) } }.awaitAll() } + } + } + + private fun formatFile(input: File) { + val originCode = input.readText() + val formattedCode = + Formatter.format( + FormattingOptions( + style = FormattingOptions.Style.GOOGLE, + maxWidth = 100, + continuationIndent = 2, + ), + originCode + ) + if (originCode != formattedCode) { + input.writeText(formattedCode) + } + } + + companion object { + + private const val PARALLEL_TASK_LIMIT = 4 + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/psl/PSLUpdateTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/psl/PSLUpdateTask.kt new file mode 100644 index 00000000..a5de3d49 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/psl/PSLUpdateTask.kt @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package app.passwordstore.gradle.psl + +import java.util.TreeSet +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.ByteString +import okio.ByteString.Companion.encodeUtf8 +import okio.buffer +import okio.sink +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +/** + * Based on PublicSuffixListGenerator from OkHttp: + * https://github.com/square/okhttp/blob/3ad1912f783e108b3d0ad2c4a5b1b89b827e4db9/okhttp/src/jvmTest/java/okhttp3/internal/publicsuffix/PublicSuffixListGenerator.java + */ +abstract class PSLUpdateTask : DefaultTask() { + @get:OutputFile abstract val outputFile: RegularFileProperty + + @TaskAction + fun updatePSL() { + val pslData = fetchPublicSuffixList() + writeListToDisk(outputFile.get(), pslData) + } + + private fun fetchPublicSuffixList(): PublicSuffixListData { + val client = OkHttpClient.Builder().build() + + val request = + Request.Builder().url("https://publicsuffix.org/list/public_suffix_list.dat").build() + + client.newCall(request).execute().use { response -> + val source = requireNotNull(response.body).source() + + val data = PublicSuffixListData() + + while (!source.exhausted()) { + val line = source.readUtf8LineStrict() + + if (line.trim { it <= ' ' }.isEmpty() || line.startsWith("//")) { + continue + } + + if (line.contains(WILDCARD_CHAR)) { + assertWildcardRule(line) + } + + var rule = line.encodeUtf8() + + if (rule.startsWith(EXCEPTION_RULE_MARKER)) { + rule = rule.substring(1) + // We use '\n' for end of value. + data.totalExceptionRuleBytes += rule.size + 1 + data.sortedExceptionRules.add(rule) + } else { + data.totalRuleBytes += rule.size + 1 // We use '\n' for end of value. + data.sortedRules.add(rule) + } + } + return data + } + } + + @Suppress("TooGenericExceptionThrown", "ThrowsCount") + private fun assertWildcardRule(rule: String) { + if (rule.indexOf(WILDCARD_CHAR) != 0) { + throw RuntimeException("Wildcard is not not in leftmost position") + } + + if (rule.indexOf(WILDCARD_CHAR, 1) != -1) { + throw RuntimeException("Rule contains multiple wildcards") + } + + if (rule.length == 1) { + throw RuntimeException("Rule wildcards the first level") + } + } + + private fun writeListToDisk(destination: RegularFile, data: PublicSuffixListData) { + val fileSink = destination.asFile.sink() + + fileSink.buffer().use { sink -> + sink.writeInt(data.totalRuleBytes) + + for (domain in data.sortedRules) { + sink.write(domain).writeByte('\n'.toInt()) + } + + sink.writeInt(data.totalExceptionRuleBytes) + + for (domain in data.sortedExceptionRules) { + sink.write(domain).writeByte('\n'.toInt()) + } + } + } + + data class PublicSuffixListData( + var totalRuleBytes: Int = 0, + var totalExceptionRuleBytes: Int = 0, + val sortedRules: TreeSet = TreeSet(), + val sortedExceptionRules: TreeSet = TreeSet() + ) + + private companion object { + private const val WILDCARD_CHAR = "*" + private val EXCEPTION_RULE_MARKER = "!".encodeUtf8() + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/psl/PublicSuffixListPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/psl/PublicSuffixListPlugin.kt new file mode 100644 index 00000000..2efeb4dd --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/psl/PublicSuffixListPlugin.kt @@ -0,0 +1,20 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.gradle.psl + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.register + +/** Gradle plugin to update the public suffix list used by the `autofill-parser` library. */ +@Suppress("Unused") +class PublicSuffixListPlugin : Plugin { + override fun apply(project: Project) { + project.tasks.register("updatePSL") { + outputFile.set(project.layout.projectDirectory.file("src/main/assets/publicsuffixes")) + } + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/signing/AppSigning.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/signing/AppSigning.kt new file mode 100644 index 00000000..4b5c7130 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/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 app.passwordstore.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 { + 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/src/main/kotlin/app/passwordstore/gradle/snapshot/SnapshotExtension.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/snapshot/SnapshotExtension.kt new file mode 100644 index 00000000..191620d1 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/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 app.passwordstore.gradle.snapshot + +abstract class SnapshotExtension { + abstract var snapshot: Boolean +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/tasks/GitHooks.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/tasks/GitHooks.kt new file mode 100644 index 00000000..3ffb4c71 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/tasks/GitHooks.kt @@ -0,0 +1,54 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.gradle.tasks + +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE +import java.nio.file.attribute.PosixFilePermission.GROUP_READ +import java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE +import java.nio.file.attribute.PosixFilePermission.OTHERS_READ +import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE +import java.nio.file.attribute.PosixFilePermission.OWNER_READ +import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction + +@CacheableTask +abstract class GitHooks : DefaultTask() { + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val hookSource: RegularFileProperty + + @get:OutputFile abstract val hookOutput: RegularFileProperty + + @TaskAction + fun install() { + Files.copy( + hookSource.asFile.get().toPath(), + hookOutput.asFile.get().toPath(), + StandardCopyOption.REPLACE_EXISTING, + ) + Files.setPosixFilePermissions( + hookOutput.asFile.get().toPath(), + setOf( + OWNER_READ, + OWNER_WRITE, + OWNER_EXECUTE, + GROUP_READ, + GROUP_EXECUTE, + OTHERS_READ, + OTHERS_EXECUTE, + ) + ) + } +} diff --git a/build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/Constants.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/Constants.kt new file mode 100644 index 00000000..52746c37 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/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 app.passwordstore.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/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningPlugin.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningPlugin.kt new file mode 100644 index 00000000..5fda1a25 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningPlugin.kt @@ -0,0 +1,88 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.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/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningTask.kt b/build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningTask.kt new file mode 100644 index 00000000..7eb19a89 --- /dev/null +++ b/build-logic/src/main/kotlin/app/passwordstore/gradle/versioning/VersioningTask.kt @@ -0,0 +1,50 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.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()) + } +} -- cgit v1.2.3