From 5b8301d0b014ed52daa13153a0ffd11c276533ad Mon Sep 17 00:00:00 2001 From: Tim Froehlich Date: Mon, 9 Mar 2026 19:44:20 +0000 Subject: [PATCH 1/2] feat: make plugin tasks cacheable and relocatable --- .../oss/licenses/plugin/ArtifactFiles.groovy | 11 +++ .../oss/licenses/plugin/DependencyTask.groovy | 8 +- .../oss/licenses/plugin/DependencyUtil.groovy | 79 ++++++++++--------- .../oss/licenses/plugin/LicensesTask.groovy | 8 +- .../licenses/plugin/OssLicensesPlugin.groovy | 8 +- .../plugin/DependencyResolutionTest.java | 4 +- 6 files changed, 70 insertions(+), 48 deletions(-) diff --git a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/ArtifactFiles.groovy b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/ArtifactFiles.groovy index bfba7194..3208aa93 100644 --- a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/ArtifactFiles.groovy +++ b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/ArtifactFiles.groovy @@ -16,13 +16,24 @@ package com.google.android.gms.oss.licenses.plugin +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity import java.io.Serializable /** * Data class to hold the resolved physical files for a single dependency. */ class ArtifactFiles implements Serializable { + @InputFile + @PathSensitive(PathSensitivity.NONE) + @Optional File pomFile + + @InputFile + @PathSensitive(PathSensitivity.NONE) + @Optional File libraryFile ArtifactFiles(File pomFile, File libraryFile) { diff --git a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/DependencyTask.groovy b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/DependencyTask.groovy index e62df28f..493a2a8a 100644 --- a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/DependencyTask.groovy +++ b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/DependencyTask.groovy @@ -21,8 +21,12 @@ import groovy.json.JsonBuilder import groovy.json.JsonGenerator import org.gradle.api.DefaultTask import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.slf4j.LoggerFactory @@ -37,6 +41,7 @@ import static com.android.tools.build.libraries.metadata.Library.LibraryOneofCas * If the protobuf is not present (e.g. debug variants) it writes a single * dependency on the {@link DependencyUtil#ABSENT_ARTIFACT}. */ +@CacheableTask abstract class DependencyTask extends DefaultTask { private static final logger = LoggerFactory.getLogger(DependencyTask.class) @@ -44,7 +49,8 @@ abstract class DependencyTask extends DefaultTask { abstract RegularFileProperty getDependenciesJson() @InputFile - @org.gradle.api.tasks.Optional + @PathSensitive(PathSensitivity.NONE) + @Optional abstract RegularFileProperty getLibraryDependenciesReport() @TaskAction diff --git a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/DependencyUtil.groovy b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/DependencyUtil.groovy index fb7006dc..0808b0b8 100644 --- a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/DependencyUtil.groovy +++ b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/DependencyUtil.groovy @@ -20,6 +20,7 @@ import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.component.ModuleComponentIdentifier import org.gradle.api.artifacts.result.ResolvedArtifactResult +import org.gradle.api.provider.Provider import org.gradle.maven.MavenModule import org.gradle.maven.MavenPomArtifact @@ -45,52 +46,54 @@ class DependencyUtil { * * @param project The Gradle project used to create the resolution query. * @param runtimeConfiguration The configuration whose dependencies should be resolved. - * @return A map of GAV coordinates to their resolved ArtifactFiles. + * @return A provider for a map of GAV coordinates to their resolved ArtifactFiles. */ - static Map resolveArtifacts(Project project, Configuration runtimeConfiguration) { - // We create an ArtifactView to gather the component identifiers and library files. - // We specifically target external Maven dependencies (ModuleComponentIdentifiers). - def runtimeArtifactView = runtimeConfiguration.incoming.artifactView { - it.componentFilter { id -> id instanceof ModuleComponentIdentifier } - } - - def artifactsMap = [:] - - // 1. Gather library files directly from the view - runtimeArtifactView.artifacts.each { artifact -> - def id = artifact.id.componentIdentifier - if (id instanceof ModuleComponentIdentifier) { - String key = "${id.group}:${id.module}:${id.version}".toString() - artifactsMap[key] = new ArtifactFiles(null, artifact.file) + static Provider> resolveArtifacts(Project project, Configuration runtimeConfiguration) { + return project.provider { + // We create an ArtifactView to gather the component identifiers and library files. + // We specifically target external Maven dependencies (ModuleComponentIdentifiers). + def runtimeArtifactView = runtimeConfiguration.incoming.artifactView { + it.componentFilter { id -> id instanceof ModuleComponentIdentifier } + } + + def artifactsMap = [:] + + // 1. Gather library files directly from the view + runtimeArtifactView.artifacts.each { artifact -> + def id = artifact.id.componentIdentifier + if (id instanceof ModuleComponentIdentifier) { + String key = "${id.group}:${id.module}:${id.version}".toString() + artifactsMap[key] = new ArtifactFiles(null, artifact.file) + } } - } - // 2. Fetch corresponding POM files using ArtifactResolutionQuery - def componentIds = runtimeArtifactView.artifacts.collect { it.id.componentIdentifier } - - if (!componentIds.isEmpty()) { - def result = project.dependencies.createArtifactResolutionQuery() - .forComponents(componentIds) - .withArtifacts(MavenModule, MavenPomArtifact) - .execute() + // 2. Fetch corresponding POM files using ArtifactResolutionQuery + def componentIds = runtimeArtifactView.artifacts.collect { it.id.componentIdentifier } + + if (!componentIds.isEmpty()) { + def result = project.dependencies.createArtifactResolutionQuery() + .forComponents(componentIds) + .withArtifacts(MavenModule, MavenPomArtifact) + .execute() - result.resolvedComponents.each { component -> - component.getArtifacts(MavenPomArtifact).each { artifact -> - if (artifact instanceof ResolvedArtifactResult) { - def id = component.id - String key = "${id.group}:${id.module}:${id.version}".toString() - - // Update the existing entry with the POM file - if (artifactsMap.containsKey(key)) { - artifactsMap[key].pomFile = artifact.file - } else { - artifactsMap[key] = new ArtifactFiles(artifact.file, null) + result.resolvedComponents.each { component -> + component.getArtifacts(MavenPomArtifact).each { artifact -> + if (artifact instanceof ResolvedArtifactResult) { + def id = component.id + String key = "${id.group}:${id.module}:${id.version}".toString() + + // Update the existing entry with the POM file + if (artifactsMap.containsKey(key)) { + artifactsMap[key].pomFile = artifact.file + } else { + artifactsMap[key] = new ArtifactFiles(artifact.file, null) + } } } } } + + return artifactsMap } - - return artifactsMap } } diff --git a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy index e79f0da7..6b243167 100644 --- a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy +++ b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesTask.groovy @@ -21,10 +21,14 @@ import groovy.xml.XmlSlurper import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Nested import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.slf4j.LoggerFactory @@ -38,6 +42,7 @@ import java.util.zip.ZipFile * mappings (POMs and Library artifacts) are provided as lazy input properties, * making the task a pure function of its inputs. */ +@CacheableTask abstract class LicensesTask extends DefaultTask { private static final String UTF_8 = "UTF-8" private static final byte[] LINE_SEPARATOR = System @@ -66,10 +71,11 @@ abstract class LicensesTask extends DefaultTask { * A map of GAV coordinates (group:name:version) to their resolved POM and Library files. * Populated by OssLicensesPlugin during configuration. */ - @org.gradle.api.tasks.Input + @Nested abstract org.gradle.api.provider.MapProperty getArtifactFiles() @InputFile + @PathSensitive(PathSensitivity.NONE) abstract RegularFileProperty getDependenciesJson() @OutputDirectory diff --git a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/OssLicensesPlugin.groovy b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/OssLicensesPlugin.groovy index df70fe90..985c6fb1 100644 --- a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/OssLicensesPlugin.groovy +++ b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/OssLicensesPlugin.groovy @@ -58,11 +58,10 @@ class OssLicensesPlugin implements Plugin { // Task 1: Dependency Identification // This task reads the AGP METADATA_LIBRARY_DEPENDENCIES_REPORT protobuf. - def dependenciesJson = baseDir.map { it.file("dependencies.json") } TaskProvider dependencyTask = project.tasks.register( "${variant.name}OssDependencyTask", DependencyTask.class) { - it.dependenciesJson.set(dependenciesJson) + it.dependenciesJson.set(baseDir.map { it.file("dependencies.json") }) it.libraryDependenciesReport.set(variant.artifacts.get(SingleArtifact.METADATA_LIBRARY_DEPENDENCIES_REPORT.INSTANCE)) } project.logger.debug("Registered task ${dependencyTask.name}") @@ -73,10 +72,7 @@ class OssLicensesPlugin implements Plugin { "${variant.name}OssLicensesTask", LicensesTask.class) { it.dependenciesJson.set(dependencyTask.flatMap { it.dependenciesJson }) - - it.artifactFiles.set(project.provider { - DependencyUtil.resolveArtifacts(project, variant.runtimeConfiguration) - }) + it.artifactFiles.set(DependencyUtil.resolveArtifacts(project, variant.runtimeConfiguration)) } project.logger.debug("Registered task ${licenseTask.name}") diff --git a/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/DependencyResolutionTest.java b/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/DependencyResolutionTest.java index 220eaab6..d79def40 100644 --- a/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/DependencyResolutionTest.java +++ b/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/DependencyResolutionTest.java @@ -92,7 +92,7 @@ public void testComplexDependencyGraphResolution() throws IOException { runtimeClasspath.resolve(); // Execute resolution logic - Map artifactFiles = DependencyUtil.resolveArtifacts(appProject, runtimeClasspath); + Map artifactFiles = DependencyUtil.resolveArtifacts(appProject, runtimeClasspath).get(); // Assertions // - Guava resolved to the higher version @@ -121,7 +121,7 @@ public void testPomResolution() throws IOException { Configuration runtimeClasspath = appProject.getConfigurations().getByName("runtimeClasspath"); runtimeClasspath.resolve(); - Map artifactFiles = DependencyUtil.resolveArtifacts(appProject, runtimeClasspath); + Map artifactFiles = DependencyUtil.resolveArtifacts(appProject, runtimeClasspath).get(); assertThat(artifactFiles).containsKey("com.google.guava:guava:33.0.0-jre"); assertThat(artifactFiles.get("com.google.guava:guava:33.0.0-jre").getPomFile()).isNotNull(); From 361c8cfaafb7770113618af7a73f395aa4ef0f7b Mon Sep 17 00:00:00 2001 From: Tim Froehlich Date: Mon, 9 Mar 2026 19:44:14 +0000 Subject: [PATCH 2/2] fix: remove redundant LicensesCleanUpTask to fix implicit dependencies on Gradle 9.x --- .../plugin/LicensesCleanUpTask.groovy | 44 ------------- .../licenses/plugin/OssLicensesPlugin.groovy | 13 ---- .../plugin/LicensesCleanUpTaskTest.java | 65 ------------------- 3 files changed, 122 deletions(-) delete mode 100644 oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesCleanUpTask.groovy delete mode 100644 oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/LicensesCleanUpTaskTest.java diff --git a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesCleanUpTask.groovy b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesCleanUpTask.groovy deleted file mode 100644 index 704cc985..00000000 --- a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/LicensesCleanUpTask.groovy +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright 2018 Google LLC - * - * 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. - */ - -package com.google.android.gms.oss.licenses.plugin - -import org.gradle.api.DefaultTask -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.SkipWhenEmpty -import org.gradle.api.tasks.TaskAction -import org.gradle.work.DisableCachingByDefault - -/** - * Task to clean up the generated files. - */ -@DisableCachingByDefault(because = "Local deletion operation") -abstract class LicensesCleanUpTask extends DefaultTask { - @Optional - @SkipWhenEmpty - @InputDirectory - abstract DirectoryProperty getGeneratedDirectory() - - @TaskAction - void action() { - File directoryToDelete = getGeneratedDirectory().get().asFile - if (directoryToDelete.exists()) { - directoryToDelete.deleteDir() - } - } -} diff --git a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/OssLicensesPlugin.groovy b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/OssLicensesPlugin.groovy index 985c6fb1..77e0c3a0 100644 --- a/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/OssLicensesPlugin.groovy +++ b/oss-licenses-plugin/src/main/groovy/com/google/android/gms/oss/licenses/plugin/OssLicensesPlugin.groovy @@ -78,19 +78,6 @@ class OssLicensesPlugin implements Plugin { // Register the LicensesTask output as a generated resource folder for AGP. variant.sources.res.addGeneratedSourceDirectory(licenseTask, LicensesTask::getGeneratedDirectory) - - // Task 3: Cleanup - // Ensures generated license files are deleted when running the clean task. - TaskProvider cleanupTask = project.tasks.register( - "${variant.name}OssLicensesCleanUp", - LicensesCleanUpTask.class) { - it.generatedDirectory.set(baseDir) - } - project.logger.debug("Registered task ${cleanupTask.name}") - - project.tasks.named("clean").configure { - it.dependsOn(cleanupTask) - } } } diff --git a/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/LicensesCleanUpTaskTest.java b/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/LicensesCleanUpTaskTest.java deleted file mode 100644 index 36698fcf..00000000 --- a/oss-licenses-plugin/src/test/java/com/google/android/gms/oss/licenses/plugin/LicensesCleanUpTaskTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Copyright 2018 Google LLC - * - * 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. - */ - -package com.google.android.gms.oss.licenses.plugin; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.io.File; -import java.io.IOException; -import org.gradle.api.Project; -import org.gradle.testfixtures.ProjectBuilder; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Tests for {@link LicensesCleanUpTask} */ -@RunWith(JUnit4.class) -public class LicensesCleanUpTaskTest { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void testAction() throws IOException { - File testDir = temporaryFolder.newFolder(); - - // Set up a generated directory with normal expected contents - File generatedDir = new File(testDir, "generated"); - File dependencyDir = new File(generatedDir, "dependency"); - assertTrue(dependencyDir.mkdirs()); - File dependencyFile = new File(dependencyDir, "dependency.json"); - assertTrue(dependencyFile.createNewFile()); - - File licensesDir = new File(generatedDir, "res/raw"); - assertTrue(licensesDir.mkdirs()); - assertTrue(new File(licensesDir, "third_party_licenses").createNewFile()); - assertTrue(new File(licensesDir, "third_party_license_metadata").createNewFile()); - - // Create a licenses clean up task - Project project = ProjectBuilder.builder().withProjectDir(testDir).build(); - LicensesCleanUpTask task = - project.getTasks().create("licensesCleanUp", LicensesCleanUpTask.class); - task.getGeneratedDirectory().set(generatedDir); - - // Run the task action - task.action(); - - // Ensure the directory is deleted - assertFalse(generatedDir.exists()); - } -}