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")