com.palantir.baseline.plugins.BaselineExactDependencies Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gradle-baseline-java Show documentation
Show all versions of gradle-baseline-java Show documentation
A Gradle plugin for applying Baseline-recommended build and IDE settings
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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.palantir.baseline.plugins;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.palantir.baseline.tasks.CheckImplicitDependenciesParentTask;
import com.palantir.baseline.tasks.CheckImplicitDependenciesTask;
import com.palantir.baseline.tasks.CheckUnusedDependenciesParentTask;
import com.palantir.baseline.tasks.CheckUnusedDependenciesTask;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.shared.dependency.analyzer.ClassAnalyzer;
import org.apache.maven.shared.dependency.analyzer.DefaultClassAnalyzer;
import org.apache.maven.shared.dependency.analyzer.DependencyAnalyzer;
import org.apache.maven.shared.dependency.analyzer.asm.ASMDependencyAnalyzer;
import org.gradle.api.NamedDomainObjectProvider;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ExcludeRule;
import org.gradle.api.artifacts.ModuleVersionIdentifier;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.artifacts.ResolvedDependency;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ProjectComponentIdentifier;
import org.gradle.api.attributes.LibraryElements;
import org.gradle.api.attributes.Usage;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.util.GUtil;
/** Validates that java projects declare exactly the dependencies they rely on, no more and no less. */
public final class BaselineExactDependencies implements Plugin {
private static final ClassAnalyzer JAR_ANALYZER = new DefaultClassAnalyzer();
private static final DependencyAnalyzer CLASS_FILE_ANALYZER = new ASMDependencyAnalyzer();
// All applications of this plugin share a single static 'Indexes' instance, because the classes
// contained in a particular jar are immutable.
public static final Indexes INDEXES = new Indexes();
public static final ImmutableSet VALID_ARTIFACT_EXTENSIONS = ImmutableSet.of("jar", "");
@Override
public void apply(Project project) {
project.getPluginManager().withPlugin("java", plugin -> {
TaskProvider checkUnusedDependencies =
project.getTasks().register("checkUnusedDependencies", CheckUnusedDependenciesParentTask.class);
TaskProvider checkImplicitDependencies =
project.getTasks().register("checkImplicitDependencies", CheckImplicitDependenciesParentTask.class);
project.getConvention()
.getPlugin(JavaPluginConvention.class)
.getSourceSets()
.configureEach(sourceSet ->
configureSourceSet(project, sourceSet, checkUnusedDependencies, checkImplicitDependencies));
});
}
private static void configureSourceSet(
Project project,
SourceSet sourceSet,
TaskProvider checkUnusedDependencies,
TaskProvider checkImplicitDependencies) {
NamedDomainObjectProvider implementation =
project.getConfigurations().named(sourceSet.getImplementationConfigurationName());
NamedDomainObjectProvider compileClasspath =
project.getConfigurations().named(sourceSet.getCompileClasspathConfigurationName());
NamedDomainObjectProvider explicitCompile = project.getConfigurations()
.register("baseline-exact-dependencies-" + sourceSet.getName(), conf -> {
conf.setDescription(String.format(
"Tracks the explicit (not inherited) dependencies added to either %s "
+ "or compile (deprecated)",
sourceSet.getImplementationConfigurationName()));
conf.setVisible(false);
conf.setCanBeConsumed(false);
conf.attributes(attributes -> {
// This ensures we resolve 'compile' variants rather than 'runtime'
// This is the same attribute that's being set on compileClasspath
attributes.attribute(
Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_API));
// Ensure we resolve the classes directory for local projects where possible, rather than the
// 'jar' file.
attributes.attribute(
LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
project.getObjects().named(LibraryElements.class, LibraryElements.CLASSES));
});
conf.withDependencies(deps -> {
// Pick up GCV locks. We're making an internal assumption that this configuration exists,
// but we can rely on this since we control GCV.
// Alternatively, we could tell GCV to lock this configuration, at the cost of a slightly more
// expensive 'unifiedClasspath' resolution during lock computation.
if (project.getRootProject().getPluginManager().hasPlugin("com.palantir.versions-lock")) {
conf.extendsFrom(project.getConfigurations().getByName("lockConstraints"));
}
// Inherit the excludes from compileClasspath too (that get aggregated from all its
// super-configurations).
compileClasspath.get().getExcludeRules().forEach(rule -> conf.exclude(excludeRuleAsMap(rule)));
});
// Since we are copying configurations before resolving 'explicitCompile', make double sure that
// it's not
// being resolved (or dependencies realized via `.getIncoming().getDependencies()`) too early.
AtomicBoolean projectsEvaluated = new AtomicBoolean();
project.getGradle().projectsEvaluated(g -> projectsEvaluated.set(true));
conf.getIncoming()
.beforeResolve(ir -> Preconditions.checkState(
projectsEvaluated.get()
|| (project.getGradle()
.getStartParameter()
.isConfigureOnDemand()
&& project.getState().getExecuted()),
"Tried to resolve %s too early.",
conf));
});
// Figure out what our compile dependencies are while ignoring dependencies we've inherited from other source
// sets. For example, if we are `test`, some of our configurations extend from the `main` source set:
// testImplementation extendsFrom(implementation)
// \-- testCompile extendsFrom(compile)
// We therefore want to look at only the dependencies _directly_ declared in the implementation and compile
// configurations (belonging to our source set)
project.afterEvaluate(p -> {
Configuration implCopy = implementation.get().copy();
// Without these, explicitCompile will successfully resolve 0 files and you'll waste 1 hour trying
// to figure out why.
project.getConfigurations().add(implCopy);
explicitCompile.get().extendsFrom(implCopy);
Optional maybeCompile =
Optional.ofNullable(project.getConfigurations().findByName(getCompileConfigurationName(sourceSet)));
// For Gradle 6 and below, the compile configuration might still be used.
maybeCompile.ifPresent(compile -> {
Configuration compileCopy = compile.copy();
// Ensure it's not resolvable, otherwise plugins that resolve all configurations might have
// a bad time resolving this with GCV, if you have direct dependencies without corresponding entries in
// versions.props, but instead rely on getting a version for them from the lock file.
compileCopy.setCanBeResolved(false);
compileCopy.setCanBeConsumed(false);
project.getConfigurations().add(compileCopy);
explicitCompile.get().extendsFrom(compileCopy);
});
});
TaskProvider sourceSetUnusedDependencies = project.getTasks()
.register(
checkUnusedDependenciesNameForSourceSet(sourceSet), CheckUnusedDependenciesTask.class, task -> {
task.dependsOn(sourceSet.getClassesTaskName());
task.setSourceClasses(sourceSet.getOutput().getClassesDirs());
task.getDependenciesConfigurations().add(explicitCompile);
// ignore intra-project dependencies, which are typically added automatically for things
// like test fixtures
task.ignore(project.provider(() ->
Set.of(ignoreCoordinate(project.getGroup().toString(), project.getName()))));
// this is liberally applied to ease the Java8 -> 11 transition
task.ignore("javax.annotation", "javax.annotation-api");
// this is typically used instead of junit-jupiter-api to simplify configuration
task.ignore("org.junit.jupiter", "junit-jupiter");
// pick up ignores configured globally on the parent task
task.ignore(checkUnusedDependencies.get().getIgnore());
});
checkUnusedDependencies.configure(task -> task.dependsOn(sourceSetUnusedDependencies));
TaskProvider sourceSetCheckImplicitDependencies = project.getTasks()
.register(
"checkImplicitDependencies" + StringUtils.capitalize(sourceSet.getName()),
CheckImplicitDependenciesTask.class,
task -> {
task.dependsOn(sourceSet.getClassesTaskName());
task.setSourceClasses(sourceSet.getOutput().getClassesDirs());
task.getDependenciesConfigurations().add(compileClasspath);
task.suggestionConfigurationName(sourceSet.getImplementationConfigurationName());
task.ignore("org.slf4j", "slf4j-api");
// pick up ignores configured globally on the parent task
task.ignore(checkImplicitDependencies.get().getIgnore());
});
checkImplicitDependencies.configure(task -> task.dependsOn(sourceSetCheckImplicitDependencies));
}
static String checkUnusedDependenciesNameForSourceSet(SourceSet sourceSet) {
return "checkUnusedDependencies" + StringUtils.capitalize(sourceSet.getName());
}
/**
* The {@code SourceSet#getCompileConfigurationName()} method got removed in Gradle 7. Because we want to stay
* compatible with Gradle 6 but can't compile this method, we reimplement it temporarily.
* TODO(fwindheuser): Remove after dropping support for Gradle 6.
*/
private static String getCompileConfigurationName(SourceSet sourceSet) {
String baseName = sourceSet.getName().equals(SourceSet.MAIN_SOURCE_SET_NAME)
? ""
: GUtil.toCamelCase(sourceSet.getName());
return StringUtils.uncapitalize(baseName + StringUtils.capitalize("compile"));
}
private static Map excludeRuleAsMap(ExcludeRule rule) {
// Both 'ExcludeRule#getGroup' and 'ExcludeRule#getModule' can return null.
Builder excludeRule = ImmutableMap.builder();
if (rule.getGroup() != null) {
excludeRule.put("group", rule.getGroup());
}
if (rule.getModule() != null) {
excludeRule.put("module", rule.getModule());
}
return excludeRule.build();
}
/** Given a {@code com/palantir/product/Foo.class} file, what other classes does it import/reference. */
public static Stream referencedClasses(File classFile) {
try {
return BaselineExactDependencies.CLASS_FILE_ANALYZER
.analyze(classFile.toURI().toURL())
.stream();
} catch (IOException e) {
throw new RuntimeException("Unable to analyze " + classFile, e);
}
}
public static String asString(ResolvedArtifact artifact) {
ModuleVersionIdentifier moduleVersionId = artifact.getModuleVersion().getId();
StringBuilder builder = new StringBuilder()
.append(moduleVersionId.getGroup())
.append(":")
.append(moduleVersionId.getName());
if (artifact.getClassifier() != null) {
builder.append("::").append(artifact.getClassifier());
}
return builder.toString();
}
public static String asDependencyStringWithName(ResolvedArtifact artifact) {
return asDependencyString(artifact, true);
}
public static String asDependencyStringWithoutName(ResolvedArtifact artifact) {
return asDependencyString(artifact, false);
}
private static String asDependencyString(ResolvedArtifact artifact, boolean withName) {
ComponentIdentifier componentId = artifact.getId().getComponentIdentifier();
if (componentId instanceof ProjectComponentIdentifier) {
ProjectComponentIdentifier projectComponentId = (ProjectComponentIdentifier) componentId;
StringBuilder builder = new StringBuilder()
.append("project('")
.append(projectComponentId.getProjectPath())
.append("')");
if (withName) {
builder.append(" <-- ").append(artifact.getName());
}
return builder.toString();
}
return asString(artifact);
}
@ThreadSafe
public static final class Indexes {
private final Map> classToDependency = new ConcurrentHashMap<>();
private final Map> classesFromArtifact = new ConcurrentHashMap<>();
private final Map artifactsFromDependency = new ConcurrentHashMap<>();
public void populateIndexes(Set declaredDependencies) {
Set allArtifacts = declaredDependencies.stream()
.flatMap(dependency -> dependency.getAllModuleArtifacts().stream())
.filter(dependency -> VALID_ARTIFACT_EXTENSIONS.contains(dependency.getExtension()))
.collect(Collectors.toSet());
allArtifacts.forEach(artifact -> {
try {
File jar = artifact.getFile();
Set classesInArtifact =
JAR_ANALYZER.analyze(jar.toURI().toURL());
classesFromArtifact.put(artifact, classesInArtifact);
classesInArtifact.forEach(clazz -> classToDependency
.computeIfAbsent(clazz, _ignored -> ConcurrentHashMap.newKeySet())
.add(artifact));
} catch (IOException e) {
throw new RuntimeException("Unable to analyze artifact", e);
}
});
declaredDependencies.forEach(dependency -> dependency
.getModuleArtifacts()
.forEach(artifact -> artifactsFromDependency.put(artifact, dependency)));
}
/** Given a class, what dependency brought it in. */
public Stream classToArtifacts(String clazz) {
return classToDependency.getOrDefault(clazz, ImmutableSet.of()).stream();
}
/** Given an artifact, what classes does it contain. */
public Stream classesFromArtifact(ResolvedArtifact resolvedArtifact) {
return Preconditions.checkNotNull(
classesFromArtifact.get(resolvedArtifact), "Unable to find resolved artifact")
.stream();
}
public ResolvedDependency artifactsFromDependency(ResolvedArtifact resolvedArtifact) {
return Preconditions.checkNotNull(
artifactsFromDependency.get(resolvedArtifact), "Unable to find resolved artifact");
}
}
public static String ignoreCoordinate(String group, String name) {
return group + ":" + name;
}
}