// Copyright 2014 The Bazel Authors. 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.google.devtools.build.buildjar.javac.plugins.dependency;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import com.google.devtools.build.buildjar.JarOwner;
import com.google.devtools.build.buildjar.javac.plugins.BlazeJavaCompilerPlugin;
import com.google.devtools.build.lib.view.proto.Deps;
import com.google.devtools.build.lib.view.proto.Deps.Dependency.Kind;
import com.sun.tools.javac.code.Symbol.PackageSymbol;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Wrapper class for managing dependencies on top of {@link
* com.google.devtools.build.buildjar.javac.BlazeJavaCompiler}. If strict_java_deps is enabled, it
* keeps two maps between jar names (as they appear on the classpath) and their originating targets,
* one for direct dependencies and the other for transitive (indirect) dependencies, and enables the
* {@link StrictJavaDepsPlugin} to perform the actual checks. The plugin also collects dependency
* information during compilation, and DependencyModule generates a .jdeps artifact summarizing the
* discovered dependencies.
*/
public final class DependencyModule {
public static enum StrictJavaDeps {
/** Legacy behavior: Silently allow referencing transitive dependencies. */
OFF(false),
/** Warn about transitive dependencies being used directly. */
WARN(true),
/** Fail the build when transitive dependencies are used directly. */
ERROR(true);
private final boolean enabled;
StrictJavaDeps(boolean enabled) {
this.enabled = enabled;
}
/** Convenience method for just checking if it's not OFF */
public boolean isEnabled() {
return enabled;
}
}
private final StrictJavaDeps strictJavaDeps;
private final Map<String, JarOwner> directJarsToTargets;
private final Map<String, JarOwner> indirectJarsToTargets;
private final boolean strictClasspathMode;
private final Set<String> depsArtifacts;
private final String ruleKind;
private final String targetLabel;
private final String outputDepsProtoFile;
private final Set<String> usedClasspath;
private final Map<String, Deps.Dependency> explicitDependenciesMap;
private final Map<String, Deps.Dependency> implicitDependenciesMap;
private final ImmutableSet<String> platformJars;
Set<String> requiredClasspath;
private final FixMessage fixMessage;
private final Set<String> exemptGenerators;
private final Set<PackageSymbol> packages;
DependencyModule(
StrictJavaDeps strictJavaDeps,
Map<String, JarOwner> directJarsToTargets,
Map<String, JarOwner> indirectJarsToTargets,
boolean strictClasspathMode,
Set<String> depsArtifacts,
ImmutableSet<String> platformJars,
String ruleKind,
String targetLabel,
String outputDepsProtoFile,
FixMessage fixMessage,
Set<String> exemptGenerators) {
this.strictJavaDeps = strictJavaDeps;
this.directJarsToTargets = directJarsToTargets;
this.indirectJarsToTargets = indirectJarsToTargets;
this.strictClasspathMode = strictClasspathMode;
this.depsArtifacts = depsArtifacts;
this.ruleKind = ruleKind;
this.targetLabel = targetLabel;
this.outputDepsProtoFile = outputDepsProtoFile;
this.explicitDependenciesMap = new HashMap<>();
this.implicitDependenciesMap = new HashMap<>();
this.platformJars = platformJars;
this.usedClasspath = new HashSet<>();
this.fixMessage = fixMessage;
this.exemptGenerators = exemptGenerators;
this.packages = new HashSet<>();
}
/** Returns a plugin to be enabled in the compiler. */
public BlazeJavaCompilerPlugin getPlugin() {
return new StrictJavaDepsPlugin(this);
}
/**
* Writes dependency information to the deps file in proto format, if specified.
*
* <p>We collect precise dependency information to allow Blaze to analyze both strict and unused
* dependencies, as well as packages contained by the output jar.
*/
public void emitDependencyInformation(ImmutableList<String> classpath, boolean successful)
throws IOException {
if (outputDepsProtoFile == null) {
return;
}
try (BufferedOutputStream out =
new BufferedOutputStream(new FileOutputStream(outputDepsProtoFile))) {
buildDependenciesProto(classpath, successful).writeTo(out);
} catch (IOException ex) {
throw new IOException("Cannot write dependencies to " + outputDepsProtoFile, ex);
}
}
@VisibleForTesting
Deps.Dependencies buildDependenciesProto(ImmutableList<String> classpath, boolean successful) {
Deps.Dependencies.Builder deps = Deps.Dependencies.newBuilder();
if (targetLabel != null) {
deps.setRuleLabel(targetLabel);
}
deps.setSuccess(successful);
deps.addAllContainedPackage(
FluentIterable.from(packages)
.transform(
new Function<PackageSymbol, String>() {
@Override
public String apply(PackageSymbol pkg) {
return pkg.isUnnamed() ? "" : pkg.getQualifiedName().toString();
}
})
.toSortedList(Ordering.natural()));
// Filter using the original classpath, to preserve ordering.
for (String entry : classpath) {
if (explicitDependenciesMap.containsKey(entry)) {
deps.addDependency(explicitDependenciesMap.get(entry));
} else if (implicitDependenciesMap.containsKey(entry)) {
deps.addDependency(implicitDependenciesMap.get(entry));
}
}
return deps.build();
}
/** Returns whether strict dependency checks (strictJavaDeps) are enabled. */
public boolean isStrictDepsEnabled() {
return strictJavaDeps.isEnabled();
}
/**
* Returns the mapping for jars of direct dependencies. The keys are full paths (as seen on the
* classpath), and the values are build target names.
*/
public Map<String, JarOwner> getDirectMapping() {
return directJarsToTargets;
}
/**
* Returns the mapping for jars of indirect dependencies. The keys are full paths (as seen on the
* classpath), and the values are build target names.
*/
public Map<String, JarOwner> getIndirectMapping() {
return indirectJarsToTargets;
}
/** Returns the strict dependency checking (strictJavaDeps) setting. */
public StrictJavaDeps getStrictJavaDeps() {
return strictJavaDeps;
}
/** Returns the map collecting precise explicit dependency information. */
public Map<String, Deps.Dependency> getExplicitDependenciesMap() {
return explicitDependenciesMap;
}
/** Returns the map collecting precise implicit dependency information. */
public Map<String, Deps.Dependency> getImplicitDependenciesMap() {
return implicitDependenciesMap;
}
/** Returns the jars in the platform classpath. */
public ImmutableSet<String> getPlatformJars() {
return platformJars;
}
/** Adds a package to the set of packages built by this target. */
public boolean addPackage(PackageSymbol packge) {
return packages.add(packge);
}
/** Returns the type (rule kind) of the originating target. */
public String getRuleKind() {
return ruleKind;
}
/** Returns the name (label) of the originating target. */
public String getTargetLabel() {
return targetLabel;
}
/** Returns the file name collecting dependency information. */
public String getOutputDepsProtoFile() {
return outputDepsProtoFile;
}
@VisibleForTesting
Set<String> getUsedClasspath() {
return usedClasspath;
}
/** Returns a message to suggest fix when a missing indirect dependency is found. */
public FixMessage getFixMessage() {
return fixMessage;
}
/** Return a set of generator values that are exempt from strict dependencies. */
public Set<String> getExemptGenerators() {
return exemptGenerators;
}
/** Returns whether classpath reduction is enabled for this invocation. */
public boolean reduceClasspath() {
return strictClasspathMode;
}
/**
* Computes a reduced compile-time classpath from the union of direct dependencies and their
* dependencies, as listed in the associated .deps artifacts.
*/
public ImmutableList<String> computeStrictClasspath(ImmutableList<String> originalClasspath)
throws IOException {
if (!strictClasspathMode) {
return originalClasspath;
}
// Classpath = direct deps + runtime direct deps + their .deps
requiredClasspath = new HashSet<>(directJarsToTargets.keySet());
for (String depsArtifact : depsArtifacts) {
collectDependenciesFromArtifact(depsArtifact);
}
// Filter the initial classpath and keep the original order
return originalClasspath
.stream()
.filter(requiredClasspath::contains)
.collect(toImmutableList());
}
@VisibleForTesting
// TODO(cushon): use Paths instead of strings, or inject a FileSystem
void setStrictClasspath(Set<String> strictClasspath) {
this.requiredClasspath = strictClasspath;
}
/** Updates {@link #requiredClasspath} to include dependencies from the given output artifact. */
private void collectDependenciesFromArtifact(String path) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path))) {
Deps.Dependencies deps = Deps.Dependencies.parseFrom(bis);
// Sanity check to make sure we have a valid proto.
if (!deps.hasRuleLabel()) {
throw new IOException("Could not parse Deps.Dependencies message from proto.");
}
for (Deps.Dependency dep : deps.getDependencyList()) {
if (dep.getKind() == Kind.EXPLICIT
|| dep.getKind() == Kind.IMPLICIT
|| dep.getKind() == Kind.INCOMPLETE) {
requiredClasspath.add(dep.getPath());
}
}
} catch (IOException e) {
throw new IOException(String.format("error reading deps artifact: %s", path), e);
}
}
/**
* A functional that formats a message for the user about a missing dependency that they should
* add to unbreak their build.
*/
public interface FixMessage {
String get(Iterable<JarOwner> missing, String recipient, boolean useColor);
}
/** Builder for {@link DependencyModule}. */
public static class Builder {
private StrictJavaDeps strictJavaDeps = StrictJavaDeps.OFF;
private final Map<String, JarOwner> directJarsToTargets = new HashMap<>();
private final Map<String, JarOwner> indirectJarsToTargets = new HashMap<>();
private final Set<String> depsArtifacts = new HashSet<>();
private ImmutableSet<String> platformJars = ImmutableSet.of();
private String ruleKind;
private String targetLabel;
private String outputDepsProtoFile;
private boolean strictClasspathMode = false;
private FixMessage fixMessage = new DefaultFixMessage();
private final Set<String> exemptGenerators = new HashSet<>();
private static class DefaultFixMessage implements DependencyModule.FixMessage {
@Override
public String get(Iterable<JarOwner> missing, String recipient, boolean useColor) {
StringBuilder missingTargetsStr = new StringBuilder();
for (JarOwner owner : missing) {
missingTargetsStr.append(owner.label());
missingTargetsStr.append(" ");
}
return String.format(
"%s** Please add the following dependencies:%s\n %s to %s\n\n",
useColor ? "\033[35m\033[1m" : "",
useColor ? "\033[0m" : "",
missingTargetsStr.toString(),
recipient);
}
}
/**
* Constructs the DependencyModule, guaranteeing that the maps are never null (they may be
* empty), and the default strictJavaDeps setting is OFF.
*
* @return an instance of DependencyModule
*/
public DependencyModule build() {
return new DependencyModule(
strictJavaDeps,
directJarsToTargets,
indirectJarsToTargets,
strictClasspathMode,
depsArtifacts,
platformJars,
ruleKind,
targetLabel,
outputDepsProtoFile,
fixMessage,
exemptGenerators);
}
/**
* Sets the strictness level for dependency checking.
*
* @param strictJavaDeps level, as specified by {@link StrictJavaDeps}
* @return this Builder instance
*/
public Builder setStrictJavaDeps(String strictJavaDeps) {
this.strictJavaDeps = StrictJavaDeps.valueOf(strictJavaDeps);
return this;
}
/**
* Sets the type (rule kind) of the originating target.
*
* @param ruleKind kind, such as the rule kind of a RuleConfiguredTarget
* @return this Builder instance
*/
public Builder setRuleKind(String ruleKind) {
this.ruleKind = ruleKind;
return this;
}
/**
* Sets the name (label) of the originating target.
*
* @param targetLabel label, such as the label of a RuleConfiguredTarget.
* @return this Builder instance.
*/
public Builder setTargetLabel(String targetLabel) {
this.targetLabel = targetLabel;
return this;
}
/**
* Adds direct mappings to the existing map for direct dependencies.
*
* @param directMappings a map of paths of jar artifacts, as seen on classpath, to full names of
* build targets providing the jar.
* @return this Builder instance
*/
public Builder addDirectMappings(Map<String, JarOwner> directMappings) {
directJarsToTargets.putAll(directMappings);
return this;
}
/**
* Adds an indirect mapping to the existing map for indirect dependencies.
*
* @param jar path of jar artifact, as seen on classpath.
* @param target full name of build target providing the jar.
* @return this Builder instance
*/
public Builder addIndirectMapping(String jar, JarOwner target) {
indirectJarsToTargets.put(jar, target);
return this;
}
/**
* Adds indirect mappings to the existing map for indirect dependencies.
*
* @param indirectMappings a map of paths of jar artifacts, as seen on classpath, to full names
* of build targets providing the jar.
* @return this Builder instance
*/
public Builder addIndirectMappings(Map<String, JarOwner> indirectMappings) {
indirectJarsToTargets.putAll(indirectMappings);
return this;
}
/**
* Sets the name of the file that will contain dependency information in the protocol buffer
* format.
*
* @param outputDepsProtoFile output file name for dependency information
* @return this Builder instance
*/
public Builder setOutputDepsProtoFile(String outputDepsProtoFile) {
this.outputDepsProtoFile = outputDepsProtoFile;
return this;
}
/**
* Adds a collection of dependency artifacts to use when reducing the compile-time classpath.
*
* @param depsArtifacts dependency artifacts
* @return this Builder instance
*/
public Builder addDepsArtifacts(Collection<String> depsArtifacts) {
this.depsArtifacts.addAll(depsArtifacts);
return this;
}
/** Sets the platform classpath entries. */
public Builder setPlatformJars(ImmutableSet<String> platformJars) {
this.platformJars = platformJars;
return this;
}
/**
* Requests compile-time classpath reduction based on provided dependency artifacts.
*
* @return this Builder instance
*/
public Builder setReduceClasspath() {
this.strictClasspathMode = true;
return this;
}
/**
* Set the message to display when a missing indirect dependency is found.
*
* @param fixMessage the fix message
* @return this Builder instance
*/
public Builder setFixMessage(FixMessage fixMessage) {
this.fixMessage = fixMessage;
return this;
}
/**
* Add a generator to the exempt set.
*
* @param exemptGenerator the generator class name
* @return this Builder instance
*/
public Builder addExemptGenerator(String exemptGenerator) {
exemptGenerators.add(exemptGenerator);
return this;
}
}
}