Skip to content

Commit

Permalink
Make custom tasks compatible with configuration cache
Browse files Browse the repository at this point in the history
Gradle is forcing a move away from using 'project' during task excution
and because of some interactions there, this is easiest by making them
real classes. That makes them start looking quite strange in the build
file, so they are now moved to buildSrc/. We could have continued using
Groovy, but it is weird in some ways that are more apparent when making
classes and not just scripting. Instead, they were converted to Java.

They are compatible with delayed configuration resolution as well.
  • Loading branch information
ejona86 committed Nov 27, 2023
1 parent dca89b2 commit 7eb24d6
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 121 deletions.
103 changes: 6 additions & 97 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ plugins {
id 'com.google.cloud.tools.jib' apply false
}

import io.grpc.gradle.CheckForUpdatesTask
import io.grpc.gradle.RequireUpperBoundDepsMatchTask
import net.ltgt.gradle.errorprone.CheckSeverity
import org.gradle.util.GUtil

subprojects {
apply plugin: "checkstyle"
Expand Down Expand Up @@ -275,15 +276,11 @@ subprojects {
plugins.withId("java-library") {
// Detect Maven Enforcer's dependencyConvergence failures. We only care
// for artifacts used as libraries by others with Maven.
tasks.register('checkUpperBoundDeps') {
inputs.files(configurations.runtimeClasspath).withNormalizer(ClasspathNormalizer)
outputs.file("${buildDir}/tmp/${name}") // Fake output for UP-TO-DATE checking
doLast {
requireUpperBoundDepsMatch(configurations.runtimeClasspath, project)
}
tasks.register('checkUpperBoundDeps', RequireUpperBoundDepsMatchTask) {
configuration = configurations.getByName('runtimeClasspath')
}
tasks.named('assemble').configure {
dependsOn checkUpperBoundDeps
dependsOn tasks.named('checkUpperBoundDeps')
}
}

Expand Down Expand Up @@ -456,64 +453,6 @@ subprojects {
}
}

class DepAndParents {
DependencyResult dep
List<String> parents
}

/**
* Make sure that Maven would select the same versions as Gradle selected.
* This is essentially the same as if we used Maven Enforcer's
* requireUpperBoundDeps for our artifacts.
*/
def requireUpperBoundDepsMatch(Configuration conf, Project project) {
// artifact name => version
Map<String,String> golden = conf.resolvedConfiguration.resolvedArtifacts.collectEntries {
ResolvedArtifact it ->
ModuleVersionIdentifier id = it.moduleVersion.id
[id.group + ":" + id.name, id.version]
}
// Breadth-first search like Maven for dependency resolution
Queue<DepAndParents> queue = new ArrayDeque<>()
conf.incoming.resolutionResult.root.dependencies.each {
queue.add(new DepAndParents(dep: it, parents: [project.displayName]))
}
Set<String> found = new HashSet<>()
while (!queue.isEmpty()) {
DepAndParents depAndParents = queue.remove()
ResolvedDependencyResult result = (ResolvedDependencyResult) depAndParents.dep
ModuleVersionIdentifier id = result.selected.moduleVersion
String artifact = id.group + ":" + id.name
if (found.contains(artifact))
continue
found.add(artifact)
String version
if (result.requested instanceof ProjectComponentSelector) {
ProjectComponentSelector selector = (ProjectComponentSelector) result.requested
version = project.findProject(selector.projectPath).version
} else {
version = ((ModuleComponentSelector) result.requested).version
}
String goldenVersion = golden[artifact]
if (goldenVersion != version && "[$goldenVersion]" != version) {
throw new RuntimeException(
"Maven version skew: $artifact ($version != $goldenVersion) "
+ "Bad version dependency path: " + depAndParents.parents
+ " Run './gradlew $project.path:dependencies --configuration $conf.name' "
+ "to diagnose")
}
result.selected.dependencies.each {
// Category.CATEGORY_ATTRIBUTE is the inappropriate Attribute because it is "desugared".
// https://github.com/gradle/gradle/issues/8854
Attribute<String> category = Attribute.of('org.gradle.category', String)
if (it.resolvedVariant.attributes.getAttribute(category) != Category.LIBRARY)
return
queue.add(new DepAndParents(
dep: it, parents: depAndParents.parents + [artifact + ":" + version]))
}
}
}

repositories {
mavenCentral()
google()
Expand Down Expand Up @@ -546,34 +485,4 @@ configurations {
}
}

// Checks every dependency in the version catalog to see if there is a newer
// version available. The 'checkForUpdates' configuration restricts the
// versions considered.
tasks.register('checkForUpdates') {
doLast {
def updateConf = project.configurations.checkForUpdates
updateConf.setVisible(false)
updateConf.setTransitive(false)
def versionCatalog = project.extensions.getByType(VersionCatalogsExtension).named("libs")
versionCatalog.libraryAliases.each { name ->
def dep = versionCatalog.findLibrary(name).get().get()

def oldConf = updateConf.copy()
def oldDep = project.dependencies.create(
group: dep.group, name: dep.name, version: dep.versionConstraint, classifier: 'pom')
oldConf.dependencies.add(oldDep)
def oldResolved = oldConf.resolvedConfiguration.resolvedArtifacts.iterator().next()

def newConf = updateConf.copy()
def newDep = project.dependencies.create(
group: dep.group, name: dep.name, version: '+', classifier: 'pom')
newConf.dependencies.add(newDep)
def newResolved = newConf.resolvedConfiguration.resolvedArtifacts.iterator().next()
if (oldResolved != newResolved) {
def oldId = oldResolved.id.componentIdentifier
def newId = newResolved.id.componentIdentifier
println("libs.${name} = ${newId.group}:${newId.module} ${oldId.version} -> ${newId.version}")
}
}
}
}
tasks.register('checkForUpdates', CheckForUpdatesTask, project.configurations.checkForUpdates, "libs")
5 changes: 5 additions & 0 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
tasks.withType(JavaCompile).configureEach {
it.options.compilerArgs += [
"-Xlint:all",
]
}
131 changes: 131 additions & 0 deletions buildSrc/src/main/java/io/grpc/gradle/CheckForUpdatesTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright 2023 The gRPC Authors
*
* 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 io.grpc.gradle;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.VersionCatalog;
import org.gradle.api.artifacts.VersionCatalogsExtension;
import org.gradle.api.artifacts.result.ResolvedComponentResult;
import org.gradle.api.artifacts.result.ResolvedDependencyResult;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.TaskAction;

/**
* Checks every dependency in the version catalog to see if there is a newer version available. The
* passed configuration restricts the versions considered.
*/
public abstract class CheckForUpdatesTask extends DefaultTask {
private final Set<Library> libraries;

@Inject
public CheckForUpdatesTask(Configuration updateConf, String catalog) {
updateConf.setVisible(false);
updateConf.setTransitive(false);
VersionCatalog versionCatalog = getProject().getExtensions().getByType(VersionCatalogsExtension.class).named(catalog);
Set<Library> libraries = new LinkedHashSet<Library>();
for (String name : versionCatalog.getLibraryAliases()) {
org.gradle.api.artifacts.MinimalExternalModuleDependency dep = versionCatalog.findLibrary(name).get().get();

Configuration oldConf = updateConf.copy();
Dependency oldDep = getProject().getDependencies().create(
depMap(dep.getGroup(), dep.getName(), dep.getVersionConstraint().toString(), "pom"));
oldConf.getDependencies().add(oldDep);

Configuration newConf = updateConf.copy();
Dependency newDep = getProject().getDependencies().create(
depMap(dep.getGroup(), dep.getName(), "+", "pom"));
newConf.getDependencies().add(newDep);

libraries.add(new Library(
name,
oldConf.getIncoming().getResolutionResult().getRootComponent(),
newConf.getIncoming().getResolutionResult().getRootComponent()));
}
this.libraries = Collections.unmodifiableSet(libraries);
}

private static Map<String, String> depMap(
String group, String name, String version, String classifier) {
Map<String, String> map = new HashMap<>();
map.put("group", group);
map.put("name", name);
map.put("version", version);
map.put("classifier", classifier);
return map;
}

@Nested
protected Set<Library> getLibraries() {
return libraries;
}

@TaskAction
public void checkForUpdates() {
for (Library lib : libraries) {
String name = lib.getName();
ModuleVersionIdentifier oldId = ((ResolvedDependencyResult) lib.getOldResult().get()
.getDependencies().iterator().next()).getSelected().getModuleVersion();
ModuleVersionIdentifier newId = ((ResolvedDependencyResult) lib.getNewResult().get()
.getDependencies().iterator().next()).getSelected().getModuleVersion();
if (oldId != newId) {
System.out.println(String.format(
"libs.%s = %s:%s %s -> %s",
name, newId.getGroup(), newId.getModule(), oldId.getVersion(), newId.getVersion()));
}
}
}

public static final class Library {
private final String name;
private final Provider<ResolvedComponentResult> oldResult;
private final Provider<ResolvedComponentResult> newResult;

public Library(
String name, Provider<ResolvedComponentResult> oldResult,
Provider<ResolvedComponentResult> newResult) {
this.name = name;
this.oldResult = oldResult;
this.newResult = newResult;
}

@Input
public String getName() {
return name;
}

@Input
public Provider<ResolvedComponentResult> getOldResult() {
return oldResult;
}

@Input
public Provider<ResolvedComponentResult> getNewResult() {
return newResult;
}
}
}
66 changes: 66 additions & 0 deletions buildSrc/src/main/java/io/grpc/gradle/CheckPackageLeakageTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2023 The gRPC Authors
*
* 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 io.grpc.gradle;

import java.io.File;
import java.io.IOException;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;

/** Verifies all class files within jar files are in a specified Java package. */
public abstract class CheckPackageLeakageTask extends DefaultTask {
public CheckPackageLeakageTask() {
// Fake output for UP-TO-DATE checking
getOutputs().file(getProject().getLayout().getBuildDirectory().file("tmp/" + getName()));
}

@Classpath
abstract ConfigurableFileCollection getFiles();

@Input
abstract Property<String> getPrefix();

@TaskAction
public void checkLeakage() throws IOException {
String jarEntryPrefixName = getPrefix().get().replace('.', '/');
boolean packageLeakDetected = false;
for (File jar : getFiles()) {
try (JarFile jf = new JarFile(jar)) {
for (Enumeration<JarEntry> e = jf.entries(); e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
if (entry.getName().endsWith(".class")
&& !entry.getName().startsWith(jarEntryPrefixName)) {
packageLeakDetected = true;
System.out.println("WARNING: package leaked, may need relocation: " + entry.getName());
}
}
}
}
if (packageLeakDetected) {
throw new TaskExecutionException(this,
new IllegalStateException("Resource leakage detected!"));
}
}
}
Loading

0 comments on commit 7eb24d6

Please sign in to comment.