diff --git a/BUILD b/BUILD index 9f180b38..6f009c8c 100644 --- a/BUILD +++ b/BUILD @@ -134,3 +134,7 @@ recipe_test( recipe_test( name = "onVariants", ) + +recipe_test( + name = "legacyTaskBridging", +) diff --git a/recipes/legacyTaskBridging/README.md b/recipes/legacyTaskBridging/README.md new file mode 100644 index 00000000..c57caf4d --- /dev/null +++ b/recipes/legacyTaskBridging/README.md @@ -0,0 +1,75 @@ +# Bridging a Legacy Task with new Variant API Property<> instances + +This recipe shows how you can adapt an existing `Task` using intrinsic file types like `File` in order to be +compatible with the new Variant API. The new variant API requires using instances of `Property<>` when wiring +things up in order to carry task dependency within those property objects. This is not easy when you want to use +an old task expressing its input or output using `File` for instance. + +In this example, we add a source folder to Android's `assets`. The source folder +content is provided by a [org.gradle.api.tasks.Copy](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Copy.html) +Task which expresses its output folder using a +[File](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Copy.html#org.gradle.api.tasks.Copy:destinationDir) + +In this recipe, we use the `SourceDirectories.addGeneratedSourceDirectory` to add a new folder for `assets` +processing using Gradle's `Copy` Tasks. + +| Module | Content | +|----------------------------|------------------------------------------------------------------------------| +| [build-logic](build-logic) | Contains the Project plugin that is the core of the recipe. | +| [app](app) | An Android application that will be configured with the added source folder. | + +## Details + +### Bridging File to DirectoryProperty + +When you need to bridge a Task output expressed using a [File](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Copy.html#org.gradle.api.tasks.Copy:destinationDir) +to a Provider which is expected by the variant API, you cannot just create a `DirectoryProperty` using +Gradle's 'ObjectFactory' and call `set()` with the File instance. Although the value would be set correctly, the +Property object would not carry the Task dependency which would eventually yield to a failure : + +``` +':projectA:someTask' uses this output of task ':projectA:generatingAssetTask' without declaring an explicit or +implicit dependency. This can lead to incorrect results being produced, depending on what order the tasks are executed. +``` + +In order for gradle to set the property object correctly, you must do a [map](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#map-org.gradle.api.Transformer-) +or [flatMap](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#flatMap-org.gradle.api.Transformer-) +using the `TaskProvider` of the generating Task. + +The easiest way to do that is to subclass the original Task and override the right methods to redirect the input or +output values to properties. In our case, the subclass is simply : + +``` +abstract class AssetCreatorTask: Copy() { + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + override fun getDestinationDir(): File = + outputDirectory.get().asFile + + override fun setDestinationDir(destination: File) { + outputDirectory.set(destination) + } +} +``` + +Once the `TaskProvider` is created, you need to use `SourceDirectories.addGeneratedSourceDirectory` to register its +output as a new source folder. +``` +variant.sources.assets?.addGeneratedSourceDirectory( + assetCreationTask, + AssetCreatorTask::outputDirectory) +``` + +### Run the example + +To run the examples, you can just do: +``` +./gradlew debugVerifyAsset +``` +and the output should be: +``` +> Task :app:debugVerifyAsset +Success: Found asset in resulting APK ! +``` \ No newline at end of file diff --git a/recipes/legacyTaskBridging/app/build.gradle.kts b/recipes/legacyTaskBridging/app/build.gradle.kts new file mode 100644 index 00000000..1e2b03d8 --- /dev/null +++ b/recipes/legacyTaskBridging/app/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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.custom_plugin") +} + +android { + namespace = "com.example.android.recipes.recipe" + compileSdk = $COMPILE_SDK + defaultConfig { + minSdk = $MINIMUM_SDK + targetSdk = $COMPILE_SDK + } +} diff --git a/recipes/legacyTaskBridging/app/src/common/custom_asset.txt b/recipes/legacyTaskBridging/app/src/common/custom_asset.txt new file mode 100644 index 00000000..af569d0a --- /dev/null +++ b/recipes/legacyTaskBridging/app/src/common/custom_asset.txt @@ -0,0 +1 @@ +some asset file \ No newline at end of file diff --git a/recipes/legacyTaskBridging/app/src/main/AndroidManifest.xml b/recipes/legacyTaskBridging/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..95971dfc --- /dev/null +++ b/recipes/legacyTaskBridging/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/recipes/legacyTaskBridging/build-logic/gradle.properties b/recipes/legacyTaskBridging/build-logic/gradle.properties new file mode 100644 index 00000000..3dcf88f0 --- /dev/null +++ b/recipes/legacyTaskBridging/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/legacyTaskBridging/build-logic/gradle/libs.versions.toml b/recipes/legacyTaskBridging/build-logic/gradle/libs.versions.toml new file mode 100644 index 00000000..d362ae08 --- /dev/null +++ b/recipes/legacyTaskBridging/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" } diff --git a/recipes/legacyTaskBridging/build-logic/plugins/build.gradle.kts b/recipes/legacyTaskBridging/build-logic/plugins/build.gradle.kts new file mode 100644 index 00000000..0608834d --- /dev/null +++ b/recipes/legacyTaskBridging/build-logic/plugins/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright 2022 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.custom_plugin" + implementationClass = "CustomPlugin" + } + } +} diff --git a/recipes/legacyTaskBridging/build-logic/plugins/src/main/kotlin/CustomPlugin.kt b/recipes/legacyTaskBridging/build-logic/plugins/src/main/kotlin/CustomPlugin.kt new file mode 100644 index 00000000..6426a984 --- /dev/null +++ b/recipes/legacyTaskBridging/build-logic/plugins/src/main/kotlin/CustomPlugin.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2022 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.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.BuiltArtifactsLoader +import com.android.build.api.artifact.MultipleArtifact +import com.android.build.api.artifact.SingleArtifact +import com.android.build.gradle.AppPlugin +import java.io.File +import java.util.jar.JarFile +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.provider.Property +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.register +import java.lang.IllegalStateException + +/** + * This custom plugin will register a task output as a generated source folder for + * android Assets. + * + * It will also create a Task to verify that the generated sources are properly + * accounted for during building. + */ +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 -> + variant.sources.assets + ?.let { + // create the task that will add new source files to the asset source folder. + val assetCreationTask = + project.tasks.register("create${variant.name}Asset") + + assetCreationTask.configure { task: AssetCreatorTask -> + task.from("src/common") + task.include("**/*asset*.*") + } + + // registers the newly created Task as the provider for a new generated + // source folder for the 'assets' type. + // The task will execute only when the `assets` source folders are looked + // up at execution time (during asset merging basically). + it.addGeneratedSourceDirectory( + assetCreationTask, + AssetCreatorTask::outputDirectory + ) + } + + // create the verification task + project.tasks.register("${variant.name}VerifyAsset") { + output.set( + project.layout.buildDirectory.dir("intermediates/recipe/$it.name") + ) + // the verifying task will look at the merged assets folder and ensure + // the file added by the assetCreationTask is present. + assets.set(variant.artifacts.get(SingleArtifact.ASSETS)) + } + } + } + } +} + +/** + * This task is creating an asset that will be used as a source asset file. + * + * It is based on the Gradle's [org.gradle.api.tasks.Copy] task and bridge the + * `destinationDir` output to a `DirectoryProperty` that can be used with the + * Variant APIs. + */ +abstract class AssetCreatorTask: Copy() { + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + override fun getDestinationDir(): File = + outputDirectory.get().asFile + + override fun setDestinationDir(destination: File) { + outputDirectory.set(destination) + } +} + +/** + * This task here to verify that the API does what is says. + */ +abstract class VerifyAssetTask : DefaultTask() { + + // In order of the task to be up-to-date when the input has 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 or not + @get:OutputDirectory + abstract val output: DirectoryProperty + + @get:InputDirectory + @get:Optional + abstract val assets: DirectoryProperty + + @TaskAction + fun taskAction() { + File(assets.get().asFile, "custom_asset.txt").let { + if (it.exists()) { + println("Found ${it} in merged assets folder") + } else { + throw IllegalStateException("custom_asset.txt file not " + + "present in merged asset folder : ${assets.get().asFile}") + } + } + } +} \ No newline at end of file diff --git a/recipes/legacyTaskBridging/build-logic/settings.gradle.kts b/recipes/legacyTaskBridging/build-logic/settings.gradle.kts new file mode 100644 index 00000000..fb2f5583 --- /dev/null +++ b/recipes/legacyTaskBridging/build-logic/settings.gradle.kts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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/legacyTaskBridging/build.gradle.kts b/recipes/legacyTaskBridging/build.gradle.kts new file mode 100644 index 00000000..a30e11d3 --- /dev/null +++ b/recipes/legacyTaskBridging/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 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/legacyTaskBridging/gradle.properties b/recipes/legacyTaskBridging/gradle.properties new file mode 100644 index 00000000..55cce922 --- /dev/null +++ b/recipes/legacyTaskBridging/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/legacyTaskBridging/gradle/libs.versions.toml b/recipes/legacyTaskBridging/gradle/libs.versions.toml new file mode 100644 index 00000000..8c672bba --- /dev/null +++ b/recipes/legacyTaskBridging/gradle/libs.versions.toml @@ -0,0 +1,9 @@ +[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/legacyTaskBridging/recipe_metadata.toml b/recipes/legacyTaskBridging/recipe_metadata.toml new file mode 100644 index 00000000..0e41b69c --- /dev/null +++ b/recipes/legacyTaskBridging/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 =""" + Recipe to add some custom asset from a legacy Task using File types. + """ + +[agpVersion] +min = "8.1.0" + +# Relevant Gradle tasks to run per recipe +[gradleTasks] +tasks = [ + "debugVerifyAsset" +] + +# All the relevant metadata fields to create an index based on language/API/etc' +[indexMetadata] +index = [ + "Themes/Android Assets", + "Themes/Sources", + "APIs/SourceDirectories.addGeneratedSourceDirectory()", + "APIs/SingleArtifact.ASSETS", + "APIs/Artifacts.get()", + "APIs/AndroidComponentsExtension.onVariants()", + "APIs/Component.artifacts", + "APIs/Component.sources", + "Call chains/variant.sources.*.addGeneratedSourceDirectory()", + "Call chains/variant.artifacts.get()", + "Call chains/androidComponents.onVariants {}", + "Legacy API bridging" +] diff --git a/recipes/legacyTaskBridging/settings.gradle.kts b/recipes/legacyTaskBridging/settings.gradle.kts new file mode 100644 index 00000000..f507d070 --- /dev/null +++ b/recipes/legacyTaskBridging/settings.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright 2022 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 = "legacyTaskBridging" + +pluginManagement { + includeBuild("build-logic") + repositories { + $AGP_REPOSITORY + $PLUGIN_REPOSITORIES + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + $AGP_REPOSITORY + $DEPENDENCY_REPOSITORIES + } +} + +include(":app")