diff --git a/BUILD b/BUILD index e4eb9a55..4d4031e1 100644 --- a/BUILD +++ b/BUILD @@ -145,3 +145,7 @@ recipe_test( recipe_test( name = "asmTransformClasses", ) + +recipe_test( + name = "transformDirectory", +) diff --git a/recipes/transformDirectory/README.md b/recipes/transformDirectory/README.md new file mode 100644 index 00000000..a96c075e --- /dev/null +++ b/recipes/transformDirectory/README.md @@ -0,0 +1,36 @@ +# Transform artifacts of type 'Single' and kind 'Directory' with InAndOutDirectoryOperationRequest.toTransform() API + +This sample shows how to use the `InAndOutDirectoryOperationRequest.toTransform()` API on an artifact of type +[Artifact.Single](https://developer.android.com/reference/tools/gradle-api/current/com/android/build/api/artifact/Artifact.Single) and [Artifact.Transformable](https://developer.android.com/reference/tools/gradle-api/current/com/android/build/api/artifact/Artifact.Transformable).This method is defined in +[InAndOutDirectoryOperationRequest](https://developer.android.com/reference/tools/gradle-api/current/com/android/build/api/artifact/InAndOutDirectoryOperationRequest). + +This recipe contains the following directories: + +| Module | Content | +|----------------------------|-------------------------------------------------------------| +| [build-logic](build-logic) | Contains the Project plugin that is the core of the recipe. | +| [app](app) | An Android application that has the plugin applied. | + +The [build-logic](build-logic) sub-project contains the [`CustomPlugin`](build-logic/plugins/src/main/kotlin/CustomPlugin.kt), [`TransformAssetsTask`](build-logic/plugins/src/main/kotlin/TransformAssetsTask.kt) and +[`CheckAssetsTask`](build-logic/plugins/src/main/kotlin/CheckAssetsTask.kt) classes. + +[`CustomPlugin`](build-logic/plugins/src/main/kotlin/CustomPlugin.kt) registers an instance of `TransformAssetsTask` per variant using +`InAndOutDirectoryOperationRequest.toTransform()` via the code below. This automatically creates a dependency on this +task from any task consuming the `SingleArtifact.ASSETS` artifact. + +``` +variant.artifacts.use(transformDebugAssets) + .wiredWithDirectories( + TransformAssetsTask::inputDir, + TransformAssetsTask::outputDir) + .toTransform(SingleArtifact.ASSETS) +``` + +[`CustomPlugin`](build-logic/plugins/src/main/kotlin/CustomPlugin.kt) also registers an instance of the `CheckAssetsTask` per variant which verifies that the +transformed artifact and directory contains the expected data. In this recipe, running this task will also run +`TransformAssetsTask`, because of the dependency on `SingleArtifact.ASSETS`. + +## To Run +To execute example you need to enter command: + +`./gradlew :app:checkDebugAssets` diff --git a/recipes/transformDirectory/app/build.gradle.kts b/recipes/transformDirectory/app/build.gradle.kts new file mode 100644 index 00000000..50252f5d --- /dev/null +++ b/recipes/transformDirectory/app/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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. + */ + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + id("android.recipes.transform_directory") +} + +android { + namespace = "com.example.android.recipes.transform_directory" + compileSdk = $COMPILE_SDK + defaultConfig { + minSdk = $MINIMUM_SDK + targetSdk = $COMPILE_SDK + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} \ No newline at end of file diff --git a/recipes/transformDirectory/app/src/main/AndroidManifest.xml b/recipes/transformDirectory/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8b7fed3d --- /dev/null +++ b/recipes/transformDirectory/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/recipes/transformDirectory/app/src/main/assets/FileToTransform.txt b/recipes/transformDirectory/app/src/main/assets/FileToTransform.txt new file mode 100644 index 00000000..cbc4e2e3 --- /dev/null +++ b/recipes/transformDirectory/app/src/main/assets/FileToTransform.txt @@ -0,0 +1 @@ +initial content \ No newline at end of file diff --git a/recipes/transformDirectory/build-logic/gradle.properties b/recipes/transformDirectory/build-logic/gradle.properties new file mode 100644 index 00000000..3dcf88f0 --- /dev/null +++ b/recipes/transformDirectory/build-logic/gradle.properties @@ -0,0 +1,2 @@ +# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534 +org.gradle.parallel=true diff --git a/recipes/transformDirectory/build-logic/gradle/libs.versions.toml b/recipes/transformDirectory/build-logic/gradle/libs.versions.toml new file mode 100644 index 00000000..d009d769 --- /dev/null +++ b/recipes/transformDirectory/build-logic/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[versions] +androidGradlePlugin = $AGP_VERSION +kotlin = $KOTLIN_VERSION + +[libraries] +android-gradlePlugin-api = { group = "com.android.tools.build", name = "gradle-api", version.ref = "androidGradlePlugin" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } \ No newline at end of file diff --git a/recipes/transformDirectory/build-logic/plugins/build.gradle.kts b/recipes/transformDirectory/build-logic/plugins/build.gradle.kts new file mode 100644 index 00000000..0236f9ec --- /dev/null +++ b/recipes/transformDirectory/build-logic/plugins/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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. + */ + +plugins { + `java-gradle-plugin` + alias(libs.plugins.kotlin.jvm) +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + compileOnly(libs.android.gradlePlugin.api) + implementation(gradleKotlinDsl()) +} + +gradlePlugin { + plugins { + create("customPlugin") { + id = "android.recipes.transform_directory" + implementationClass = "CustomPlugin" + } + } +} + diff --git a/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/CheckAssetsTask.kt b/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/CheckAssetsTask.kt new file mode 100644 index 00000000..cac0dbaa --- /dev/null +++ b/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/CheckAssetsTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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. + */ + +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import java.lang.RuntimeException + +abstract class CheckAssetsTask : DefaultTask() { + + // In order for the task to be up-to-date when the inputs have not changed, + // the task must declare an output, even if it's not used. Tasks with no + // output are always run regardless of whether the inputs changed + @get:OutputDirectory + abstract val output: DirectoryProperty + + @get:InputDirectory + abstract val assetsDirectory: DirectoryProperty + + @TaskAction + fun taskAction() { + val transformedFileContents = File(assetsDirectory.get().asFile, "FileToTransform.txt").readText() + if (transformedFileContents != "transformed content") { + throw RuntimeException("Transformed file does not have expected contents.") + } + } +} \ No newline at end of file diff --git a/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/CustomPlugin.kt b/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/CustomPlugin.kt new file mode 100644 index 00000000..08b16246 --- /dev/null +++ b/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/CustomPlugin.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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. + */ + +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.gradle.AppPlugin +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.register +import org.gradle.configurationcache.extensions.capitalized + +/** + * This custom plugin will register a callback that is applied to all variants. + */ +class CustomPlugin : Plugin { + override fun apply(project: Project) { + + // Registers a callback on the application of the Android Application plugin. + // This allows the CustomPlugin to work whether it's applied before or after + // the Android Application plugin. + project.plugins.withType(AppPlugin::class.java) { + + // Queries for the extension set by the Android Application plugin. + // This is the second of two entry points into the Android Gradle plugin + val androidComponents = + project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java) + // Registers a callback to be called, when a new variant is configured + androidComponents.onVariants { variant -> + val taskProvider = + project.tasks.register("transform${variant.name.capitalized()}Assets") + + // TransformAssetsTask will change the assets directory + variant.artifacts.use(taskProvider) + .wiredWithDirectories( + TransformAssetsTask::inputDir, + TransformAssetsTask::outputDir + ).toTransform(SingleArtifact.ASSETS) + + // -- Verification -- + // the following is just to validate the recipe and is not actually part of the recipe itself + val taskName = "check${variant.name.capitalized()}Assets" + project.tasks.register(taskName) { + assetsDirectory.set( + variant.artifacts.get(SingleArtifact.ASSETS) + ) + output.set(project.layout.buildDirectory.dir("intermediates/$taskName")) + } + } + } + } +} \ No newline at end of file diff --git a/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/TransformAssetsTask.kt b/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/TransformAssetsTask.kt new file mode 100644 index 00000000..bbf81e89 --- /dev/null +++ b/recipes/transformDirectory/build-logic/plugins/src/main/kotlin/TransformAssetsTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * 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. + */ + +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.file.DirectoryProperty + +abstract class TransformAssetsTask: DefaultTask() { + + @get:InputDirectory + abstract val inputDir: DirectoryProperty + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun taskAction() { + // We must copy the contents of the input directory to the output directory before our transformation + inputDir.get().asFile.copyRecursively(outputDir.get().asFile) + + // Transform an existing file by updating its contents + val fileToTransform = File(outputDir.get().asFile, "FileToTransform.txt") + val transformedContent = fileToTransform.readText().replace("initial", "transformed") + fileToTransform.writeText(transformedContent) + } +} \ No newline at end of file diff --git a/recipes/transformDirectory/build-logic/settings.gradle.kts b/recipes/transformDirectory/build-logic/settings.gradle.kts new file mode 100644 index 00000000..e2e5e9e5 --- /dev/null +++ b/recipes/transformDirectory/build-logic/settings.gradle.kts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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. + */ + +rootProject.name = "build-logic" + +pluginManagement { + repositories { + $AGP_REPOSITORY + $PLUGIN_REPOSITORIES + } +} + +dependencyResolutionManagement { + repositories { + $AGP_REPOSITORY + $DEPENDENCY_REPOSITORIES + } +} + +include(":plugins") diff --git a/recipes/transformDirectory/build.gradle.kts b/recipes/transformDirectory/build.gradle.kts new file mode 100644 index 00000000..68e631f2 --- /dev/null +++ b/recipes/transformDirectory/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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. + */ + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} diff --git a/recipes/transformDirectory/gradle.properties b/recipes/transformDirectory/gradle.properties new file mode 100644 index 00000000..55cce922 --- /dev/null +++ b/recipes/transformDirectory/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/recipes/transformDirectory/gradle/libs.versions.toml b/recipes/transformDirectory/gradle/libs.versions.toml new file mode 100644 index 00000000..4b265f77 --- /dev/null +++ b/recipes/transformDirectory/gradle/libs.versions.toml @@ -0,0 +1,8 @@ +[versions] +androidGradlePlugin = $AGP_VERSION +kotlin = $KOTLIN_VERSION + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } + diff --git a/recipes/transformDirectory/recipe_metadata.toml b/recipes/transformDirectory/recipe_metadata.toml new file mode 100644 index 00000000..73f5122f --- /dev/null +++ b/recipes/transformDirectory/recipe_metadata.toml @@ -0,0 +1,34 @@ +# optional (if present and non-blank) name to use in the index +indexName = "" +# optional (if present and non-blank) folder name to use when converting recipe in RELEASE mode +destinationFolder = "" + +description =""" + This sample shows how to use the InAndOutDirectoryOperationRequest.toTransform() API on an object of type + Artifact.Single and Artifact.Transformable + """ + +[agpVersion] +min = "8.1.0" + +# Relevant Gradle tasks to run per recipe +[gradleTasks] +tasks = [ + ":app:checkDebugAssets" +] + +# All the relevant metadata fields to create an index based on language/API/etc +[indexMetadata] +index = [ + "Themes/Artifact API", + "Call chains/variant.artifacts.get()", + "Call chains/variant.artifacts.use().wiredWithDirectories().toTransform()", + "Call chains/androidComponents.onVariants {}", + "APIs/InAndOutDirectoryOperationRequest.toTransform()", + "APIs/Artifacts.use()", + "APIs/Artifacts.get()", + "APIs/SingleArtifact.ASSETS", + "APIs/Component.artifacts", + "APIs/TaskBasedOperation.wiredWithDirectories()", + "APIs/AndroidComponentsExtension.onVariants()" +] diff --git a/recipes/transformDirectory/settings.gradle.kts b/recipes/transformDirectory/settings.gradle.kts new file mode 100644 index 00000000..8a724739 --- /dev/null +++ b/recipes/transformDirectory/settings.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 + * + * https://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. + */ + +rootProject.name = "transformDirectory" + +pluginManagement { + includeBuild("build-logic") + repositories { + $AGP_REPOSITORY + $PLUGIN_REPOSITORIES + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + $AGP_REPOSITORY + $DEPENDENCY_REPOSITORIES + } +} + +include(":app")