From 00cbf1ff8461bae90531d901ce7eba2623c9216e Mon Sep 17 00:00:00 2001 From: magical-obama Date: Sun, 3 Mar 2024 20:44:10 +1100 Subject: [PATCH] Update README.md to use new plugins DSL --- README.md | 33 +++- .../secrets_gradle_plugin/Extensions.kt | 99 ++++++++++ .../secrets_gradle_plugin/SecretsPlugin.kt | 100 +++++++++++ .../SecretsPluginExtension.kt | 40 +++++ .../SecretsPluginTest.kt | 170 ++++++++++++++++++ 5 files changed, 434 insertions(+), 8 deletions(-) create mode 100644 secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/Extensions.kt create mode 100644 secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPlugin.kt create mode 100644 secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPluginExtension.kt create mode 100644 secrets-gradle-plugin/bin/test/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPluginTest.kt diff --git a/README.md b/README.md index e7060f3..2134779 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,29 @@ class and in the Android manifest file. Groovy: ```groovy -buildscript { - dependencies { - classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1" - } +plugins { + id "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" version "2.0.1" apply false } ``` Kotlin: ```kotlin -buildscript { - dependencies { - classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") - } +plugins { + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" apply false +} +``` + +With a TOML version catalog: +```toml +[versions] +secrets = "2.0.1" + +[plugins] +secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } +``` +```kotlin +plugins { + alias(libs.plugins.secrets) apply false } ``` @@ -54,6 +64,13 @@ plugins { } ``` +With a TOML version catalog: +```kotlin +plugins { + alias(libs.plugins.secrets) +} +``` + This plugin also supports library module type (`com.android.library`). Just install the plugin in your library-level `build.gradle` file and keys will be visible inside that module as well. ### Snapshot Releases diff --git a/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/Extensions.kt b/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/Extensions.kt new file mode 100644 index 0000000..eee245e --- /dev/null +++ b/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/Extensions.kt @@ -0,0 +1,99 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@file:Suppress("UnstableApiUsage") + +package com.google.android.libraries.mapsplatform.secrets_gradle_plugin + +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.BuildConfigField +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.api.variant.Variant +import com.android.build.gradle.AppExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.internal.core.InternalBaseVariant +import org.gradle.api.Project +import java.io.FileNotFoundException +import java.util.Properties + +fun Project.androidAppComponent(): ApplicationAndroidComponentsExtension? = + extensions.findByType(ApplicationAndroidComponentsExtension::class.java) + +fun Project.androidLibraryComponent(): LibraryAndroidComponentsExtension? = + extensions.findByType(LibraryAndroidComponentsExtension::class.java) + +fun Project.androidProject(): AppExtension? = + extensions.findByType(AppExtension::class.java) + +fun Project.libraryProject(): LibraryExtension? = + extensions.findByType(LibraryExtension::class.java) + +fun Project.loadPropertiesFile(fileName: String): Properties { + // Load file + val propertiesFile = file(fileName) + if (!propertiesFile.exists()) { + throw FileNotFoundException( + "The file '${propertiesFile.absolutePath}' could not be found" + ) + } + + // Load contents into properties object + val properties = Properties() + properties.load(propertiesFile.inputStream()) + return properties +} + +private val javaVarRegexp = Regex(pattern = "((?![a-zA-Z_\$0-9]).)") + +fun Variant.inject(properties: Properties, ignore: List) { + val ignoreRegexs = ignore.map { Regex(pattern = it) } + properties.keys.map { key -> + key as String + }.filter { key -> + key.isNotEmpty() && !ignoreRegexs.any { it.containsMatchIn(key) } + }.forEach { key -> + val value = properties.getProperty(key).removeSurrounding("\"") + val translatedKey = key.replace(javaVarRegexp, "") + buildConfigFields.put( + translatedKey, + BuildConfigField("String", value.addParenthesisIfNeeded(), null) + ) + manifestPlaceholders.put(translatedKey, value) + } +} + +fun InternalBaseVariant.inject(properties: Properties, ignore: List) { + val ignoreRegexs = ignore.map { Regex(pattern = it) } + properties.keys.map { key -> + key as String + }.filter { key -> + key.isNotEmpty() && !ignoreRegexs.any { it.containsMatchIn(key) } + }.forEach { key -> + val value = properties.getProperty(key).removeSurrounding("\"") + val translatedKey = key.replace(javaVarRegexp, "") + buildConfigField("String", translatedKey, value.addParenthesisIfNeeded()) + mergedFlavor.manifestPlaceholders[translatedKey] = value + } +} + +fun String.addParenthesisIfNeeded(): String { + if (isEmpty()) { + return this + } + val charArray = this.toCharArray() + if (length > 1 && charArray[0] == '"' && charArray[charArray.size - 1] == '"') { + return this + } + return "\"$this\"" +} diff --git a/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPlugin.kt b/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPlugin.kt new file mode 100644 index 0000000..0ce85a1 --- /dev/null +++ b/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPlugin.kt @@ -0,0 +1,100 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +@file:Suppress("UnstableApiUsage") + +package com.google.android.libraries.mapsplatform.secrets_gradle_plugin + +import com.android.build.api.variant.Variant +import com.android.build.gradle.internal.core.InternalBaseVariant +import org.gradle.api.Plugin +import org.gradle.api.Project +import java.io.FileNotFoundException +import java.util.Properties + +/** + * Plugin that reads secrets from a properties file and injects manifest build and BuildConfig + * variables into an Android project. Since property keys are turned into Java variables, + * invalid variable characters from the property key are removed. + * + * e.g. + * A key defined as "sdk.dir" in the properties file will be converted to "sdkdir". + */ +class SecretsPlugin : Plugin { + + private val extensionName = "secrets" + + override fun apply(project: Project) { + val extension = project.extensions.create( + extensionName, + SecretsPluginExtension::class.java + ) + val supportedComponents = + listOf(project.androidAppComponent(), project.androidLibraryComponent()) + supportedComponents.forEach { component -> + component?.onVariants { variant -> + val defaultProperties = extension.defaultPropertiesFileName?.let { + project.rootProject.loadPropertiesFile(it) + } + + val properties: Properties? = try { + project.rootProject.loadPropertiesFile( + extension.propertiesFileName + ) + } catch (e: FileNotFoundException) { + defaultProperties ?: throw e + } + generateConfigKey(project, extension, defaultProperties, properties, variant) + } + } + } + + private fun generateConfigKey( + project: Project, + extension: SecretsPluginExtension, + defaultProperties: Properties?, + properties: Properties?, + variant: Variant + ) { + // Inject defaults first + defaultProperties?.let { + variant.inject(it, extension.ignoreList) + } + + properties?.let { + variant.inject(properties, extension.ignoreList) + } + + // Inject build-type specific properties + val buildTypeFileName = "${variant.buildType}.properties" + val buildTypeProperties = try { + project.rootProject.loadPropertiesFile(buildTypeFileName) + } catch (e: FileNotFoundException) { + null + } + buildTypeProperties?.let { + variant.inject(it, extension.ignoreList) + } + + // Inject flavor-specific properties + val flavorFileName = "${variant.flavorName}.properties" + val flavorProperties = try { + project.rootProject.loadPropertiesFile(flavorFileName) + } catch (e: FileNotFoundException) { + null + } + flavorProperties?.let { + variant.inject(it, extension.ignoreList) + } + } +} diff --git a/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPluginExtension.kt b/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPluginExtension.kt new file mode 100644 index 0000000..6a59123 --- /dev/null +++ b/secrets-gradle-plugin/bin/main/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPluginExtension.kt @@ -0,0 +1,40 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.android.libraries.mapsplatform.secrets_gradle_plugin + +/** + * Configuration object for [SecretsPlugin]. + */ +open class SecretsPluginExtension { + /** + * The name of the properties file containing secrets. Defaults to "$defaultPropertiesFile" + */ + var propertiesFileName: String = defaultPropertiesFile + + /** + * A list of keys this plugin should ignore and not inject. Defaults to $defaultIgnoreList + */ + var ignoreList: MutableList = defaultIgnoreList + + /** + * The name of the properties file containing secrets' default values. + */ + var defaultPropertiesFileName: String? = null + + companion object { + const val defaultPropertiesFile = "local.properties" + val defaultIgnoreList = mutableListOf("sdk.dir") + } +} \ No newline at end of file diff --git a/secrets-gradle-plugin/bin/test/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPluginTest.kt b/secrets-gradle-plugin/bin/test/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPluginTest.kt new file mode 100644 index 0000000..297470e --- /dev/null +++ b/secrets-gradle-plugin/bin/test/com/google/android/libraries/mapsplatform/secrets_gradle_plugin/SecretsPluginTest.kt @@ -0,0 +1,170 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.android.libraries.mapsplatform.secrets_gradle_plugin + +import com.android.build.gradle.internal.core.InternalBaseVariant +import com.nhaarman.mockitokotlin2.doReturn +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import org.gradle.api.Project +import org.gradle.api.ProjectConfigurationException +import org.gradle.api.internal.project.ProjectInternal +import org.gradle.testfixtures.ProjectBuilder +import org.junit.Assert +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class SecretsPluginTest { + + @Rule + @JvmField + val tempFolder = TemporaryFolder() + + lateinit var placeholders: MutableMap + lateinit var root: Project + lateinit var project: Project + lateinit var variant: InternalBaseVariant + + @Before + fun setUp() { + root = ProjectBuilder.builder() + .withProjectDir(tempFolder.root) + .withName("root") + .build() + project = ProjectBuilder.builder() + .withProjectDir(tempFolder.root.resolve("project")) + .withName("project") + .withParent(root) + .build() + placeholders = mutableMapOf() + val flavor = mock { + on { manifestPlaceholders } doReturn placeholders + } + variant = mock { + on { mergedFlavor } doReturn flavor + } + project.pluginManager.apply("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + } + + // TODO: This exception is no longer thrown since migrating to use onVariants API. Need to + // reenable this test + @Ignore + @Test(expected = ProjectConfigurationException::class) + fun `missing default properties fails`() { + project.extensions.configure(SecretsPluginExtension::class.java) { + it.propertiesFileName = "test.properties" + } + (project as ProjectInternal).evaluate() + } + + @Test + fun `properties are correctly ignored`() { + val fileName = "local.properties" + val propertiesFile = tempFolder.newFile(fileName) + propertiesFile.writeText( + """ + key="someValue" + ignoreKey="sadf" + sdk.dir="di" + sdk.foo="di" + """.trimIndent() + ) + val properties = project.rootProject.loadPropertiesFile(fileName) + val ignoreList = listOf("ignoreKey", "sdk*") + + variant.inject(properties = properties, ignore = ignoreList) + + check( + Pair("key", "someValue") + ) + checkKeysNotIn("ignoreKey", "sdk.dir", "sdk.foo") + } + + @Test + fun `properties are correctly added`() { + val fileName = "local.properties" + val propertiesFile = tempFolder.newFile(fileName) + propertiesFile.writeText( + """ + key1="someValue1" + key2="someValue2" + """.trimIndent() + ) + val properties = project.rootProject.loadPropertiesFile(fileName) + + variant.inject(properties = properties, ignore = emptyList()) + + check( + Pair("key1", "someValue1"), + Pair("key2", "someValue2"), + ) + } + + @Test + fun `invalid characters are removed`() { + val fileName = "local.properties" + val propertiesFile = tempFolder.newFile(fileName) + propertiesFile.writeText( + """ + sdk.Dir="value" + sdk.Foo!="value2" + """.trimIndent() + ) + val properties = project.rootProject.loadPropertiesFile(fileName) + + variant.inject(properties = properties, ignore = emptyList()) + + check( + Pair("sdkDir", "value"), + Pair("sdkFoo", "value2") + ) + } + + @Test + fun `parenthesis is appended if needed`() { + val fileName = "local.properties" + val propertiesFile = tempFolder.newFile(fileName) + propertiesFile.writeText( + """ + key1="value" + key2=value2 + """.trimIndent() + ) + val properties = project.rootProject.loadPropertiesFile(fileName) + + variant.inject(properties = properties, ignore = emptyList()) + + check( + Pair("key1", "value"), + Pair("key2", "value2") + ) + } + + private fun check(vararg keyValues: Pair) { + keyValues.forEach { (key, value) -> + Assert.assertEquals(value, placeholders[key]) + verify(variant).buildConfigField("String", key, value.addParenthesisIfNeeded()) + } + } + + private fun checkKeysNotIn(vararg keys: String) { + keys.forEach { + Assert.assertFalse(placeholders.containsKey(it)) + } + } +}