diff --git a/BUILD b/BUILD index 0d2afdde..6a909033 100644 --- a/BUILD +++ b/BUILD @@ -165,3 +165,7 @@ recipe_test( recipe_test( name = "listenToMultipleArtifact", ) + +recipe_test( + name = "appendToScopedArtifacts", +) diff --git a/recipes/appendToScopedArtifacts/README.md b/recipes/appendToScopedArtifacts/README.md new file mode 100644 index 00000000..88c3e870 --- /dev/null +++ b/recipes/appendToScopedArtifacts/README.md @@ -0,0 +1,47 @@ +# Appending to scoped artifacts + +This recipe shows how to add a task per variant to add a directory or file to [ScopedArtifact](https://developer.android.com/reference/tools/gradle-api/current/com/android/build/api/artifact/ScopedArtifact). +This recipe uses `ScopedArtifact.CLASSES` as an example, but the code is similar for other +`ScopedArtifact` types. The API used is `toAppend()` which is defined in [ScopedArtifactsOperation](https://developer.android.com/reference/tools/gradle-api/current/com/android/build/api/variant/ScopedArtifactsOperation) + +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) and [`CheckClassesTask`](build-logic/plugins/src/main/kotlin/CheckClassesTask.kt) classes. + +[`CustomPlugin`](build-logic/plugins/src/main/kotlin/CustomPlugin.kt) registers instances of two append tasks (`AddDirectoryClassTask` and `AddJarClassTask`) per +variant and sets its `CLASSES` inputs via the code below, which automatically adds a dependency on any tasks producing +`CLASSES` artifacts. It specifies the scope for each task using `forScope()`. Additionally, the +[`CheckClassesTask`](build-logic/plugins/src/main/kotlin/CheckClassesTask.kt) is registered per variant to validate the classes have been added to the `ScopedArtifact`. + +Below is a snippet that demonstrates the usage of the `toAppend()` API, for different scopes and both a directory and +file: + +``` +variant.artifacts + .forScope(ScopedArtifacts.Scope.PROJECT) + .use(addDirectoryClassTaskProvider) + .toAppend( + ScopedArtifact.CLASSES, + AddDirectoryClassTask::outputDirectory + ) + +variant.artifacts + .forScope(ScopedArtifacts.Scope.ALL) + .use(addJarClassTaskProvider) + .toAppend( + ScopedArtifact.CLASSES, + AddJarClassTask::outputJar + ) +``` + +In practice, a task could consider only the `PROJECT` scope or only the `ALL` scope (though +the `PROJECT` scope is a subset of the `ALL` scope). + +[`CheckClassesTask`](build-logic/plugins/src/main/kotlin/CheckClassesTask.kt) does a trivial verification of the classes. + +To run the recipe : `gradlew checkDebugClasses` diff --git a/recipes/appendToScopedArtifacts/app/build.gradle.kts b/recipes/appendToScopedArtifacts/app/build.gradle.kts new file mode 100644 index 00000000..289587fb --- /dev/null +++ b/recipes/appendToScopedArtifacts/app/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * 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.custom_plugin") +} + +android { + namespace = "com.example.android.recipes.append_to_scoped_artifacts" + compileSdk = $COMPILE_SDK + defaultConfig { + minSdk = $MINIMUM_SDK + targetSdk = $COMPILE_SDK + versionCode = 1 + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} diff --git a/recipes/appendToScopedArtifacts/app/src/main/AndroidManifest.xml b/recipes/appendToScopedArtifacts/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8b7fed3d --- /dev/null +++ b/recipes/appendToScopedArtifacts/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/recipes/appendToScopedArtifacts/build-logic/gradle.properties b/recipes/appendToScopedArtifacts/build-logic/gradle.properties new file mode 100644 index 00000000..3dcf88f0 --- /dev/null +++ b/recipes/appendToScopedArtifacts/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/appendToScopedArtifacts/build-logic/gradle/libs.versions.toml b/recipes/appendToScopedArtifacts/build-logic/gradle/libs.versions.toml new file mode 100644 index 00000000..d362ae08 --- /dev/null +++ b/recipes/appendToScopedArtifacts/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/appendToScopedArtifacts/build-logic/plugins/build.gradle.kts b/recipes/appendToScopedArtifacts/build-logic/plugins/build.gradle.kts new file mode 100644 index 00000000..4c8f1a08 --- /dev/null +++ b/recipes/appendToScopedArtifacts/build-logic/plugins/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * 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.custom_plugin" + implementationClass = "CustomPlugin" + } + } +} diff --git a/recipes/appendToScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt b/recipes/appendToScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt new file mode 100644 index 00000000..a0cb79c7 --- /dev/null +++ b/recipes/appendToScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt @@ -0,0 +1,84 @@ +/* + * 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 org.gradle.api.DefaultTask +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import java.lang.RuntimeException + +/** + * This task does a trivial check of a variant's classes. + */ +abstract class CheckClassesTask: 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 + + /** + * Project scope, not including dependencies. + */ + @get:InputFiles + abstract val projectDirectories: ListProperty + + /** + * Project scope, not including dependencies. + */ + @get:InputFiles + abstract val projectJars: ListProperty + + /** + * Full scope, including project scope and all dependencies. + */ + @get:InputFiles + abstract val allDirectories: ListProperty + + /** + * Full scope, including project scope and all dependencies. + */ + @get:InputFiles + abstract val allJars: ListProperty + + /** + * This task does a trivial check of the classes, but a similar task could be written to perform useful + * verification. + */ + @TaskAction + fun taskAction() { + // Check that the appended directory is present in projectDirectories + if (projectDirectories.get().isEmpty()) { + throw RuntimeException("Expected projectDirectories not to be empty") + } + projectDirectories.get().firstOrNull()?.let { + if (!it.asFile.walk().toList().any { file -> file.name == "New.class" }) { + throw RuntimeException("Expected New.class in projectDirectories") + } + } + + // Check that the appended jar is present in allJars + val allJarFileNames = allJars.get().map { it.asFile.name } + if (!allJarFileNames.contains("classes.jar")) { + throw RuntimeException("Expected allJars to contain classes.jar") + } + } +} \ No newline at end of file diff --git a/recipes/appendToScopedArtifacts/build-logic/plugins/src/main/kotlin/CustomPlugin.kt b/recipes/appendToScopedArtifacts/build-logic/plugins/src/main/kotlin/CustomPlugin.kt new file mode 100644 index 00000000..1b45e2ce --- /dev/null +++ b/recipes/appendToScopedArtifacts/build-logic/plugins/src/main/kotlin/CustomPlugin.kt @@ -0,0 +1,136 @@ +/* + * 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.ScopedArtifact +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.api.variant.ScopedArtifacts +import com.android.build.gradle.AppPlugin +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.register +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.configurationcache.extensions.capitalized + +/** + * This custom plugin creates tasks that append to ScopedArtifact and verifies it. + */ +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. + val androidComponents = + project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java) + + // Registers a callback to be called, when a new variant is configured + androidComponents.onVariants { variant -> + + val addDirectoryClassTaskProvider = + project.tasks.register("add${variant.name.capitalized()}DirectoryClass", AddDirectoryClassTask::class.java) + val addJarClassTaskProvider = + project.tasks.register("add${variant.name.capitalized()}JarClass", AddJarClassTask::class.java) + // Append the directory to the PROJECT scope classes + variant.artifacts + .forScope(ScopedArtifacts.Scope.PROJECT) + .use(addDirectoryClassTaskProvider) + .toAppend( + ScopedArtifact.CLASSES, + AddDirectoryClassTask::outputDirectory + ) + // Append the jar file to the ALL scope classes + variant.artifacts + .forScope(ScopedArtifacts.Scope.ALL) + .use(addJarClassTaskProvider) + .toAppend( + ScopedArtifact.CLASSES, + AddJarClassTask::outputJar + ) + + // -- Verification -- + // the following is just to validate the recipe and is not actually part of the recipe itself + val taskName = "check${variant.name.capitalized()}Classes" + val checkClassesTaskProvider = project.tasks.register(taskName) { + output.set( + project.layout.buildDirectory.dir("intermediates/$taskName") + ) + } + + // Sets the task's projectJars and projectDirectories inputs to the + // ScopeArtifacts.Scope.PROJECT ScopedArtifact.CLASSES artifacts. This automatically creates a + // dependency between this task and any tasks generating classes in the PROJECT scope. + variant.artifacts + .forScope(ScopedArtifacts.Scope.PROJECT) + .use(checkClassesTaskProvider) + .toGet( + ScopedArtifact.CLASSES, + CheckClassesTask::projectJars, + CheckClassesTask::projectDirectories, + ) + // Sets this task's allJars and allDirectories inputs to the ScopeArtifacts.Scope.ALL + // ScopedArtifact.CLASSES artifacts. This automatically creates a dependency between this task + // and any tasks generating classes. + variant.artifacts + .forScope(ScopedArtifacts.Scope.ALL) + .use(checkClassesTaskProvider) + .toGet( + ScopedArtifact.CLASSES, + CheckClassesTask::allJars, + CheckClassesTask::allDirectories, + ) + } + } + } +} + +/** + * This task appends a directory containing a class file to the ScopedArtifact. + */ +abstract class AddDirectoryClassTask: DefaultTask() { + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + @TaskAction + fun taskAction() { + // Adds a new file to the directory that is being added to the ScopedArtifact.CLASSES + File(outputDirectory.get().asFile, "New.class").createNewFile() + } +} + +/** + * This task appends a jar file to the ScopedArtifact. + */ +abstract class AddJarClassTask: DefaultTask() { + + @get:OutputFile + abstract val outputJar: RegularFileProperty + + @TaskAction + fun taskAction() { + // Create the jar file being added to the ScopedArtifact.CLASSES + outputJar.get().asFile.createNewFile() + } +} diff --git a/recipes/appendToScopedArtifacts/build-logic/settings.gradle.kts b/recipes/appendToScopedArtifacts/build-logic/settings.gradle.kts new file mode 100644 index 00000000..e2e5e9e5 --- /dev/null +++ b/recipes/appendToScopedArtifacts/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/appendToScopedArtifacts/build.gradle.kts b/recipes/appendToScopedArtifacts/build.gradle.kts new file mode 100644 index 00000000..68e631f2 --- /dev/null +++ b/recipes/appendToScopedArtifacts/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/appendToScopedArtifacts/gradle.properties b/recipes/appendToScopedArtifacts/gradle.properties new file mode 100644 index 00000000..55cce922 --- /dev/null +++ b/recipes/appendToScopedArtifacts/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/appendToScopedArtifacts/gradle/libs.versions.toml b/recipes/appendToScopedArtifacts/gradle/libs.versions.toml new file mode 100644 index 00000000..8c672bba --- /dev/null +++ b/recipes/appendToScopedArtifacts/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/appendToScopedArtifacts/recipe_metadata.toml b/recipes/appendToScopedArtifacts/recipe_metadata.toml new file mode 100644 index 00000000..b2ffd32b --- /dev/null +++ b/recipes/appendToScopedArtifacts/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 that appends to a scoped artifact and verifies in a task. + """ + +[agpVersion] +min = "8.1.0" + +# Relevant Gradle tasks to run per recipe +[gradleTasks] +tasks = [ + "checkDebugClasses" +] + +# All the relevant metadata fields to create an index based on language/API/etc' +[indexMetadata] +index = [ + "Themes/Artifact API", + "APIs/AndroidComponentsExtension.onVariants()", + "Call chains/androidComponents.onVariants {}", + "Call chains/variant.artifacts.forScope().use().toAppend()", + "APIs/Component.artifacts", + "APIs/Artifacts.forScope()", + "APIs/ScopedArtifacts.Scope.ALL", + "APIs/ScopedArtifacts.Scope.PROJECT", + "APIs/ScopedArtifacts.use()", + "APIs/ScopedArtifact.CLASSES", + "APIs/ScopedArtifactsOperation.toGet()", + "APIs/ScopedArtifactsOperation.toAppend()" +] diff --git a/recipes/appendToScopedArtifacts/settings.gradle.kts b/recipes/appendToScopedArtifacts/settings.gradle.kts new file mode 100644 index 00000000..05a73312 --- /dev/null +++ b/recipes/appendToScopedArtifacts/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 = "appendToScopedArtifacts" + +pluginManagement { + includeBuild("build-logic") + repositories { + $AGP_REPOSITORY + $PLUGIN_REPOSITORIES + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + $AGP_REPOSITORY + $DEPENDENCY_REPOSITORIES + } +} + +include(":app") diff --git a/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt b/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt index 47b6551b..ad1e3a07 100644 --- a/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt +++ b/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt @@ -90,7 +90,7 @@ abstract class CheckClassesTask: DefaultTask() { throw RuntimeException("Expected project jars to contain R.jar") } if (projectJarFileNames.any { it.startsWith("kotlin-stdlib") }) { - throw RuntimeException("Did not expected projectJars to contain kotlin stdlib") + throw RuntimeException("Did not expect projectJars to contain kotlin stdlib") } // Check allDirectories diff --git a/recipes/transformAllClasses/build-logic/plugins/src/main/kotlin/ModifyClassesTask.kt b/recipes/transformAllClasses/build-logic/plugins/src/main/kotlin/ModifyClassesTask.kt index 099426fe..b6c1ad10 100644 --- a/recipes/transformAllClasses/build-logic/plugins/src/main/kotlin/ModifyClassesTask.kt +++ b/recipes/transformAllClasses/build-logic/plugins/src/main/kotlin/ModifyClassesTask.kt @@ -62,7 +62,7 @@ abstract class ModifyClassesTask: DefaultTask() { val jarOutput = JarOutputStream(BufferedOutputStream(FileOutputStream( output.get().asFile ))) - // we just copying classes fromjar files without modification + // copy classes from jar files without modification allJars.get().forEach { file -> println("handling " + file.asFile.getAbsolutePath()) val jarFile = JarFile(file.asFile)