Skip to content

Commit

Permalink
Add a recipe to show how to consume a ScopedArtifact
Browse files Browse the repository at this point in the history
Bug: NA
Test: GradleRecipeTest
Change-Id: Ia3761f8b190001c5d17dab02786042d7f1b120fd
  • Loading branch information
scott-pollom committed Sep 19, 2023
1 parent a454937 commit c91d0b2
Show file tree
Hide file tree
Showing 16 changed files with 523 additions and 0 deletions.
4 changes: 4 additions & 0 deletions BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ recipe_test(
name = "getMultipleArtifact",
)

recipe_test(
name = "getScopedArtifacts",
)

recipe_test(
name = "getSingleArtifact",
)
Expand Down
52 changes: 52 additions & 0 deletions recipes/getScopedArtifacts/README.md
Original file line number Diff line number Diff line change
@@ -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`
37 changes: 37 additions & 0 deletions recipes/getScopedArtifacts/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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))
}
}
17 changes: 17 additions & 0 deletions recipes/getScopedArtifacts/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!--
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
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.
-->
<application android:label="Minimal">
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions recipes/getScopedArtifacts/build-logic/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
org.gradle.parallel=true
Original file line number Diff line number Diff line change
@@ -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" }
40 changes: 40 additions & 0 deletions recipes/getScopedArtifacts/build-logic/plugins/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Directory>

/**
* Project scope, not including dependencies.
*/
@get:InputFiles
abstract val projectJars: ListProperty<RegularFile>

/**
* Full scope, including project scope and all dependencies.
*/
@get:InputFiles
abstract val allDirectories: ListProperty<Directory>

/**
* Full scope, including project scope and all dependencies.
*/
@get:InputFiles
abstract val allJars: ListProperty<RegularFile>

/**
* 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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Project> {
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<CheckClassesTask>(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,
)
}
}
}
}
Loading

0 comments on commit c91d0b2

Please sign in to comment.