diff --git a/BUILD b/BUILD index 973da853..72b19620 100644 --- a/BUILD +++ b/BUILD @@ -59,6 +59,10 @@ recipe_test( name = "getMultipleArtifact", ) +recipe_test( + name = "getScopedArtifacts", +) + recipe_test( name = "getSingleArtifact", ) diff --git a/recipes/getScopedArtifacts/README.md b/recipes/getScopedArtifacts/README.md new file mode 100644 index 00000000..d7225de0 --- /dev/null +++ b/recipes/getScopedArtifacts/README.md @@ -0,0 +1,52 @@ +# Consuming scoped artifacts + +This recipe shows how to add a task per variant to get and check a +[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. + +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 an instance of the +`CheckClassesTask` per variant and sets its `CLASSES` inputs via the code below, +which automatically adds a dependency on any tasks producing `CLASSES` artifacts. When +getting the final value of a scoped artifact, a Task must provide two input fields per scope, one +for a list of jars and the other for a list of directories. + +``` +variant.artifacts + .forScope(ScopedArtifacts.Scope.PROJECT) + .use(taskProvider) + .toGet( + ScopedArtifact.CLASSES, + CheckClassesTask::projectJars, + CheckClassesTask::projectDirectories, + ) + +variant.artifacts + .forScope(ScopedArtifacts.Scope.ALL) + .use(taskProvider) + .toGet( + ScopedArtifact.CLASSES, + CheckClassesTask::allJars, + CheckClassesTask::allDirectories, + ) +``` + +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/getScopedArtifacts/app/build.gradle.kts b/recipes/getScopedArtifacts/app/build.gradle.kts new file mode 100644 index 00000000..e8ef948b --- /dev/null +++ b/recipes/getScopedArtifacts/app/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright 2023 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.getscopedartifacts" + compileSdk = $COMPILE_SDK + defaultConfig { + minSdk = $MINIMUM_SDK + targetSdk = $COMPILE_SDK + versionCode = 1 + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} diff --git a/recipes/getScopedArtifacts/app/src/main/AndroidManifest.xml b/recipes/getScopedArtifacts/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..15813815 --- /dev/null +++ b/recipes/getScopedArtifacts/app/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/recipes/getScopedArtifacts/app/src/main/kotlin/com/example/android/recipes/getscopedartifacts/MainActivity.kt b/recipes/getScopedArtifacts/app/src/main/kotlin/com/example/android/recipes/getscopedartifacts/MainActivity.kt new file mode 100644 index 00000000..de7b0153 --- /dev/null +++ b/recipes/getScopedArtifacts/app/src/main/kotlin/com/example/android/recipes/getscopedartifacts/MainActivity.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 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. + */ +package com.example.android.recipes.getscopedartifacts + +import android.app.Activity +import android.os.Bundle +import android.widget.TextView + +class MainActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val label = TextView(this) + label.setText("Hello World!") + setContentView(label) + } +} \ No newline at end of file diff --git a/recipes/getScopedArtifacts/build-logic/gradle.properties b/recipes/getScopedArtifacts/build-logic/gradle.properties new file mode 100644 index 00000000..3dcf88f0 --- /dev/null +++ b/recipes/getScopedArtifacts/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/getScopedArtifacts/build-logic/gradle/libs.versions.toml b/recipes/getScopedArtifacts/build-logic/gradle/libs.versions.toml new file mode 100644 index 00000000..d362ae08 --- /dev/null +++ b/recipes/getScopedArtifacts/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/getScopedArtifacts/build-logic/plugins/build.gradle.kts b/recipes/getScopedArtifacts/build-logic/plugins/build.gradle.kts new file mode 100644 index 00000000..87871ad6 --- /dev/null +++ b/recipes/getScopedArtifacts/build-logic/plugins/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt b/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt new file mode 100644 index 00000000..b19bd3a6 --- /dev/null +++ b/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CheckClassesTask.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2023 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.io.File +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 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 == "MainActivity.class" }) { + throw RuntimeException("Expected MainActivity.class in projectDirectories") + } + } + + // Check projectJars. We expect projectJars to include the project's R.jar but not jars + // from dependencies (e.g., the kotlin stdlib jar) + val projectJarFileNames = projectJars.get().map { it.asFile.name } + if (!projectJarFileNames.contains("R.jar")) { + 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") + } + + // Check allDirectories + if (allDirectories.get().isEmpty()) { + throw RuntimeException("Expected allDirectories not to be empty") + } + allDirectories.get().firstOrNull()?.let { + if (!it.asFile.walk().toList().any { file -> file.name == "MainActivity.class" }) { + throw RuntimeException("Expected MainActivity.class in allDirectories") + } + } + + // Check allJars. We expect allJars to include jars from the project *and* its dependencies + // (e.g., the kotlin stdlib jar). + val allJarFileNames = allJars.get().map { it.asFile.name } + if (!allJarFileNames.contains("R.jar")) { + throw RuntimeException("Expected allJars to contain R.jar") + } + if (!allJarFileNames.any { it.startsWith("kotlin-stdlib") }) { + throw RuntimeException("Expected allJars to contain kotlin stdlib") + } + } +} \ No newline at end of file diff --git a/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CustomPlugin.kt b/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CustomPlugin.kt new file mode 100644 index 00000000..e8595525 --- /dev/null +++ b/recipes/getScopedArtifacts/build-logic/plugins/src/main/kotlin/CustomPlugin.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2023 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.AndroidComponentsExtension +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 + +/** + * This custom plugin creates a task per variant that checks the variant's classes + */ +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(AndroidComponentsExtension::class.java) + + // Registers a callback to be called, when a new variant is configured + androidComponents.onVariants { variant -> + + // Registers a new task to verify the app classes. + val taskName = "check${variant.name}Classes" + val taskProvider = 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(taskProvider) + .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(taskProvider) + .toGet( + ScopedArtifact.CLASSES, + CheckClassesTask::allJars, + CheckClassesTask::allDirectories, + ) + } + } + } +} \ No newline at end of file diff --git a/recipes/getScopedArtifacts/build-logic/settings.gradle.kts b/recipes/getScopedArtifacts/build-logic/settings.gradle.kts new file mode 100644 index 00000000..5d700b9c --- /dev/null +++ b/recipes/getScopedArtifacts/build-logic/settings.gradle.kts @@ -0,0 +1,33 @@ +/* + * Copyright 2023 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/getScopedArtifacts/build.gradle.kts b/recipes/getScopedArtifacts/build.gradle.kts new file mode 100644 index 00000000..1fe3be5e --- /dev/null +++ b/recipes/getScopedArtifacts/build.gradle.kts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 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/getScopedArtifacts/gradle.properties b/recipes/getScopedArtifacts/gradle.properties new file mode 100644 index 00000000..55cce922 --- /dev/null +++ b/recipes/getScopedArtifacts/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/getScopedArtifacts/gradle/libs.versions.toml b/recipes/getScopedArtifacts/gradle/libs.versions.toml new file mode 100644 index 00000000..8c672bba --- /dev/null +++ b/recipes/getScopedArtifacts/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/getScopedArtifacts/recipe_metadata.toml b/recipes/getScopedArtifacts/recipe_metadata.toml new file mode 100644 index 00000000..f69f9462 --- /dev/null +++ b/recipes/getScopedArtifacts/recipe_metadata.toml @@ -0,0 +1,24 @@ +title = "Get Scoped Artifacts" + +description =""" + Recipe that gets scoped artifacts (classes) and verifies them in a task. + """ + +[agpVersion] +min = "8.0.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 = [ + "onVariant", + "variant.artifacts.forScope", + "ScopedArtifacts.Scope.ALL", + "ScopedArtifacts.Scope.PROJECT", + "ScopedArtifact.CLASSES", +] diff --git a/recipes/getScopedArtifacts/settings.gradle.kts b/recipes/getScopedArtifacts/settings.gradle.kts new file mode 100644 index 00000000..9c9c91f9 --- /dev/null +++ b/recipes/getScopedArtifacts/settings.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright 2023 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 = "get-scoped-artifact" + +pluginManagement { + includeBuild("build-logic") + repositories { + $AGP_REPOSITORY + $PLUGIN_REPOSITORIES + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + $AGP_REPOSITORY + $DEPENDENCY_REPOSITORIES + } +} + +include(":app")