// 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.lib.packages; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.LabelSyntaxException; import com.google.devtools.build.lib.cmdline.PackageIdentifier; import com.google.devtools.build.lib.collect.CollectionUtils; import com.google.devtools.build.lib.collect.ImmutableSortedKeyMap; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.packages.AttributeMap.AcceptsLabelAttribute; import com.google.devtools.build.lib.packages.License.DistributionType; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.util.SpellChecker; import com.google.devtools.build.lib.vfs.Canonicalizer; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * A package, which is a container of {@link Rule}s, each of * which contains a dictionary of named attributes. * * <p>Package instances are intended to be immutable and for all practical * purposes can be treated as such. Note, however, that some member variables * exposed via the public interface are not strictly immutable, so until their * types are guaranteed immutable we're not applying the {@code @Immutable} * annotation here. */ public class Package { /** * Common superclass for all name-conflict exceptions. */ public static class NameConflictException extends Exception { protected NameConflictException(String message) { super(message); } } /** * The repository identifier for this package. */ private final PackageIdentifier packageIdentifier; /** * The name of the package, e.g. "foo/bar". */ protected final String name; /** * Like name, but in the form of a PathFragment. */ private final PathFragment nameFragment; /** * The filename of this package's BUILD file. */ protected Path filename; /** * The directory in which this package's BUILD file resides. All InputFile * members of the packages are located relative to this directory. */ private Path packageDirectory; /** * The name of the workspace this package is in. Used as a prefix for the runfiles directory. * This can be set in the WORKSPACE file. This must be a valid target name. */ protected String workspaceName; /** * The root of the source tree in which this package was found. It is an invariant that * {@code sourceRoot.getRelative(packageId.getSourceRoot()).equals(packageDirectory)}. */ private Path sourceRoot; /** * The "Make" environment of this package, containing package-local * definitions of "Make" variables. */ private MakeEnvironment makeEnv; /** The collection of all targets defined in this package, indexed by name. */ protected ImmutableSortedKeyMap<String, Target> targets; /** * Default visibility for rules that do not specify it. */ private RuleVisibility defaultVisibility; private boolean defaultVisibilitySet; /** * Default package-level 'testonly' value for rules that do not specify it. */ private boolean defaultTestOnly = false; /** * Default package-level 'deprecation' value for rules that do not specify it. */ private String defaultDeprecation; /** * Default header strictness checking for rules that do not specify it. */ private String defaultHdrsCheck; /** Default copts for cc_* rules. The rules' individual copts will append to this value. */ private ImmutableList<String> defaultCopts; /** * The InputFile target corresponding to this package's BUILD file. */ private InputFile buildFile; /** * True iff this package's BUILD files contained lexical or grammatical * errors, or experienced errors during evaluation, or semantic errors during * the construction of any rule. * * <p>Note: A package containing errors does not necessarily prevent a build; * if all the rules needed for a given build were constructed prior to the * first error, the build may proceed. */ private boolean containsErrors; /** * The set of labels subincluded by this package. */ private Set<Label> subincludes; /** * The list of transitive closure of the Skylark file dependencies. */ private ImmutableList<Label> skylarkFileDependencies; /** * The package's default "licenses" and "distribs" attributes, as specified * in calls to licenses() and distribs() in the BUILD file. */ // These sets contain the values specified by the most recent licenses() or // distribs() declarations encountered during package parsing: private License defaultLicense; private Set<License.DistributionType> defaultDistributionSet; /** * The names of the package() attributes that declare default values for rule * {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR} and {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR} * values when not explicitly specified. */ public static final String DEFAULT_COMPATIBLE_WITH_ATTRIBUTE = "default_compatible_with"; public static final String DEFAULT_RESTRICTED_TO_ATTRIBUTE = "default_restricted_to"; private Set<Label> defaultCompatibleWith = ImmutableSet.of(); private Set<Label> defaultRestrictedTo = ImmutableSet.of(); private ImmutableSet<String> features; private ImmutableList<Event> events; /** * Package initialization, part 1 of 3: instantiates a new package with the * given name. * * <p>As part of initialization, {@link Builder} constructs {@link InputFile} * and {@link PackageGroup} instances that require a valid Package instance where * {@link Package#getNameFragment()} is accessible. That's why these settings are * applied here at the start. * * @precondition {@code name} must be a suffix of * {@code filename.getParentDirectory())}. */ protected Package(PackageIdentifier packageId, String runfilesPrefix) { this.packageIdentifier = packageId; this.workspaceName = runfilesPrefix; this.nameFragment = Canonicalizer.fragments().intern(packageId.getPackageFragment()); this.name = nameFragment.getPathString(); } /** Returns this packages' identifier. */ public PackageIdentifier getPackageIdentifier() { return packageIdentifier; } /** * Package initialization: part 2 of 3: sets this package's default header * strictness checking. * * <p>This is needed to support C++-related rule classes * which accesses {@link #getDefaultHdrsCheck} from the still-under-construction * package. */ protected void setDefaultHdrsCheck(String defaultHdrsCheck) { this.defaultHdrsCheck = defaultHdrsCheck; } /** * Set the default 'testonly' value for this package. */ protected void setDefaultTestOnly(boolean testOnly) { defaultTestOnly = testOnly; } /** * Set the default 'deprecation' value for this package. */ protected void setDefaultDeprecation(String deprecation) { defaultDeprecation = deprecation; } /** * Sets the default value to use for a rule's {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR} * attribute when not explicitly specified by the rule. */ protected void setDefaultCompatibleWith(Set<Label> environments) { defaultCompatibleWith = environments; } /** * Sets the default value to use for a rule's {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR} * attribute when not explicitly specified by the rule. */ protected void setDefaultRestrictedTo(Set<Label> environments) { defaultRestrictedTo = environments; } // This must always be consistent with Root.computeSourceRoot; otherwise computing source roots // from exec paths does not work, which can break the action cache for input-discovering actions. private static Path getSourceRoot(Path buildFile, PathFragment packageFragment) { Path current = buildFile.getParentDirectory(); for (int i = 0, len = packageFragment.segmentCount(); i < len && !packageFragment.equals(PathFragment.EMPTY_FRAGMENT); i++) { current = current.getParentDirectory(); } return current; } /** * Package initialization: part 3 of 3: applies all other settings and completes * initialization of the package. * * <p>Only after this method is called can this package be considered "complete" * and be shared publicly. */ protected void finishInit(Builder builder) { // If any error occurred during evaluation of this package, consider all // rules in the package to be "in error" also (even if they were evaluated // prior to the error). This behaviour is arguably stricter than need be, // but stopping a build only for some errors but not others creates user // confusion. if (builder.containsErrors) { for (Rule rule : builder.getTargets(Rule.class)) { rule.setContainsErrors(); } } this.filename = builder.getFilename(); this.packageDirectory = filename.getParentDirectory(); this.sourceRoot = getSourceRoot(filename, packageIdentifier.getSourceRoot()); if ((sourceRoot == null || !sourceRoot.getRelative(packageIdentifier.getSourceRoot()).equals(packageDirectory)) && !filename.getBaseName().equals("WORKSPACE")) { throw new IllegalArgumentException( "Invalid BUILD file name for package '" + packageIdentifier + "': " + filename); } this.makeEnv = builder.makeEnv.build(); this.targets = ImmutableSortedKeyMap.copyOf(builder.targets); this.defaultVisibility = builder.defaultVisibility; this.defaultVisibilitySet = builder.defaultVisibilitySet; if (builder.defaultCopts == null) { this.defaultCopts = ImmutableList.of(); } else { this.defaultCopts = ImmutableList.copyOf(builder.defaultCopts); } this.buildFile = builder.buildFile; this.containsErrors = builder.containsErrors; this.subincludes = builder.subincludes.keySet(); this.skylarkFileDependencies = builder.skylarkFileDependencies; this.defaultLicense = builder.defaultLicense; this.defaultDistributionSet = builder.defaultDistributionSet; this.features = ImmutableSortedSet.copyOf(builder.features); this.events = ImmutableList.copyOf(builder.events); } /** * Returns the list of subincluded labels on which the validity of this package depends. */ public Set<Label> getSubincludeLabels() { return subincludes; } /** * Returns the list of transitive closure of the Skylark file dependencies of this package. */ public ImmutableList<Label> getSkylarkFileDependencies() { return skylarkFileDependencies; } /** * Returns the filename of the BUILD file which defines this package. The * parent directory of the BUILD file is the package directory. */ public Path getFilename() { return filename; } /** * Returns the source root (a directory) beneath which this package's BUILD file was found. * * <p> Assumes invariant: * {@code getSourceRoot().getRelative(packageId.getSourceRoot()).equals(getPackageDirectory())} */ public Path getSourceRoot() { return sourceRoot; } /** * Returns the directory containing the package's BUILD file. */ public Path getPackageDirectory() { return packageDirectory; } /** * Returns the name of this package. If this build is using external repositories then this name * may not be unique! */ public String getName() { return name; } /** * Like {@link #getName}, but has type {@code PathFragment}. */ public PathFragment getNameFragment() { return nameFragment; } /** * Returns the "Make" value from the package's make environment whose name * is "varname", or null iff the variable is not defined in the environment. */ public String lookupMakeVariable(String varname, String platform) { return makeEnv.lookup(varname, platform); } /** * Returns the make environment. This should only ever be used for serialization -- how the * make variables are implemented is an implementation detail. */ MakeEnvironment getMakeEnvironment() { return makeEnv; } /** * Returns all make variables for a given platform. */ public Map<String, String> getAllMakeVariables(String platform) { ImmutableMap.Builder<String, String> map = ImmutableMap.builder(); for (String var : makeEnv.getBindings().keySet()) { String value = makeEnv.lookup(var, platform); if (value != null) { map.put(var, value); } } return map.build(); } /** * Returns the label of this package's BUILD file. * * <p> Typically <code>getBuildFileLabel().getName().equals("BUILD")</code> -- * though not necessarily: data in a subdirectory of a test package may use a * different filename to avoid inadvertently creating a new package. */ public Label getBuildFileLabel() { return buildFile.getLabel(); } /** * Returns the InputFile target for this package's BUILD file. */ public InputFile getBuildFile() { return buildFile; } /** * Returns true if errors were encountered during evaluation of this package. * (The package may be incomplete and its contents should not be relied upon * for critical operations. However, any Rules belonging to the package are * guaranteed to be intact, unless their <code>containsErrors()</code> flag * is set.) */ public boolean containsErrors() { return containsErrors; } public List<Event> getEvents() { return events; } /** Returns an (immutable, unordered) view of all the targets belonging to this package. */ public ImmutableSortedKeyMap<String, Target> getTargets() { return targets; } /** * Common getTargets implementation, accessible by {@link Package.Builder}. */ private static Collection<Target> getTargets(Map<String, Target> targetMap) { return Collections.unmodifiableCollection(targetMap.values()); } /** * Returns a (read-only, unordered) iterator of all the targets belonging * to this package which are instances of the specified class. */ public <T extends Target> Iterable<T> getTargets(Class<T> targetClass) { return getTargets(targets, targetClass); } /** * Common getTargets implementation, accessible by both {@link Package} and * {@link Package.Builder}. */ private static <T extends Target> Iterable<T> getTargets(Map<String, Target> targetMap, Class<T> targetClass) { return Iterables.filter(targetMap.values(), targetClass); } /** * Returns the rule that corresponds to a particular BUILD target name. Useful * for walking through the dependency graph of a target. * Fails if the target is not a Rule. */ @VisibleForTesting // Should be package-private public Rule getRule(String targetName) { return (Rule) targets.get(targetName); } /** Returns all rules in the package that match the given rule class. */ public Iterable<Rule> getRulesMatchingRuleClass(final String ruleClass) { Iterable<Rule> targets = getTargets(Rule.class); return Iterables.filter( targets, new Predicate<Rule>() { @Override public boolean apply(@Nullable Rule rule) { return rule.getRuleClass().equals(ruleClass); } }); } /** * Returns this package's workspace name. */ public String getWorkspaceName() { return workspaceName; } /** * Returns the features specified in the <code>package()</code> declaration. */ public ImmutableSet<String> getFeatures() { return features; } /** * Returns the target (a member of this package) whose name is "targetName". * First rules are searched, then output files, then input files. The target * name must be valid, as defined by {@code LabelValidator#validateTargetName}. * * @throws NoSuchTargetException if the specified target was not found. */ public Target getTarget(String targetName) throws NoSuchTargetException { Target target = targets.get(targetName); if (target != null) { return target; } // No such target. // If there's a file on the disk that's not mentioned in the BUILD file, // produce a more informative error. NOTE! this code path is only executed // on failure, which is (relatively) very rare. In the common case no // stat(2) is executed. Path filename = getPackageDirectory().getRelative(targetName); String suffix; if (!PathFragment.create(targetName).isNormalized()) { // Don't check for file existence in this case because the error message // would be confusing and wrong. If the targetName is "foo/bar/.", and // there is a directory "foo/bar", it doesn't mean that "//pkg:foo/bar/." // is a valid label. suffix = ""; } else if (filename.isDirectory()) { suffix = "; however, a source directory of this name exists. (Perhaps add " + "'exports_files([\"" + targetName + "\"])' to " + name + "/BUILD, or define a " + "filegroup?)"; } else if (filename.exists()) { suffix = "; however, a source file of this name exists. (Perhaps add " + "'exports_files([\"" + targetName + "\"])' to " + name + "/BUILD?)"; } else { suffix = SpellChecker.didYouMean(targetName, targets.keySet()); } throw makeNoSuchTargetException(targetName, suffix); } protected NoSuchTargetException makeNoSuchTargetException(String targetName, String suffix) { Label label; try { label = createLabel(targetName); } catch (LabelSyntaxException e) { throw new IllegalArgumentException(targetName); } String msg = String.format( "target '%s' not declared in package '%s'%s defined by %s", targetName, name, suffix, filename); return new NoSuchTargetException(label, msg); } /** * Creates a label for a target inside this package. * * @throws LabelSyntaxException if the {@code targetName} is invalid */ public Label createLabel(String targetName) throws LabelSyntaxException { return Label.create(packageIdentifier, targetName); } /** * Returns the default visibility for this package. */ public RuleVisibility getDefaultVisibility() { return defaultVisibility; } /** * Returns the default testonly value. */ public Boolean getDefaultTestOnly() { return defaultTestOnly; } /** * Returns the default deprecation value. */ public String getDefaultDeprecation() { return defaultDeprecation; } /** Gets the default header checking mode. */ public String getDefaultHdrsCheck() { return defaultHdrsCheck != null ? defaultHdrsCheck : "strict"; } /** * Returns the default copts value, to which rules should append their * specific copts. */ public ImmutableList<String> getDefaultCopts() { return defaultCopts; } /** * Returns whether the default header checking mode has been set or it is the * default value. */ public boolean isDefaultHdrsCheckSet() { return defaultHdrsCheck != null; } public boolean isDefaultVisibilitySet() { return defaultVisibilitySet; } /** * Gets the parsed license object for the default license * declared by this package. */ public License getDefaultLicense() { return defaultLicense; } /** * Returns the parsed set of distributions declared as the default for this * package. */ public Set<License.DistributionType> getDefaultDistribs() { return defaultDistributionSet; } /** * Returns the default value to use for a rule's {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR} * attribute when not explicitly specified by the rule. */ public Set<Label> getDefaultCompatibleWith() { return defaultCompatibleWith; } /** * Returns the default value to use for a rule's {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR} * attribute when not explicitly specified by the rule. */ public Set<Label> getDefaultRestrictedTo() { return defaultRestrictedTo; } @Override public String toString() { return "Package(" + name + ")=" + (targets != null ? getTargets(Rule.class) : "initializing..."); } /** * Dumps the package for debugging. Do not depend on the exact format/contents of this debugging * output. */ public void dump(PrintStream out) { out.println(" Package " + getName() + " (" + getFilename() + ")"); // Rules: out.println(" Rules"); for (Rule rule : getTargets(Rule.class)) { out.println(" " + rule.getTargetKind() + " " + rule.getLabel()); for (Attribute attr : rule.getAttributes()) { for (Object possibleValue : AggregatingAttributeMapper.of(rule) .visitAttribute(attr.getName(), attr.getType())) { out.println(" " + attr.getName() + " = " + possibleValue); } } } // Files: out.println(" Files"); for (FileTarget file : getTargets(FileTarget.class)) { out.print(" " + file.getTargetKind() + " " + file.getLabel()); if (file instanceof OutputFile) { out.println(" (generated by " + ((OutputFile) file).getGeneratingRule().getLabel() + ")"); } else { out.println(); } } } public static Builder newExternalPackageBuilder(Builder.Helper helper, Path workspacePath, String runfilesPrefix) { Builder b = new Builder(helper.createFreshPackage( Label.EXTERNAL_PACKAGE_IDENTIFIER, runfilesPrefix)); b.setFilename(workspacePath); b.setMakeEnv(new MakeEnvironment.Builder()); return b; } /** * A builder for {@link Package} objects. Only intended to be used by {@link PackageFactory} and * {@link com.google.devtools.build.lib.skyframe.PackageFunction}. */ public static class Builder { public static interface Helper { /** * Returns a fresh {@link Package} instance that a {@link Builder} will internally mutate * during package loading. Called by {@link PackageFactory}. */ Package createFreshPackage(PackageIdentifier packageId, String runfilesPrefix); /** * Called after {@link com.google.devtools.build.lib.skyframe.PackageFunction} is completely * done loading the given {@link Package}. */ void onLoadingComplete(Package pkg); } /** {@link Helper} that simply calls the {@link Package} constructor. */ public static class DefaultHelper implements Helper { public static final DefaultHelper INSTANCE = new DefaultHelper(); private DefaultHelper() { } @Override public Package createFreshPackage(PackageIdentifier packageId, String runfilesPrefix) { return new Package(packageId, runfilesPrefix); } @Override public void onLoadingComplete(Package pkg) { } } /** * The output instance for this builder. Needs to be instantiated and * available with name info throughout initialization. All other settings * are applied during {@link #build}. See {@link Package#Package} * and {@link Package#finishInit} for details. */ protected Package pkg; private Path filename = null; private Label buildFileLabel = null; private InputFile buildFile = null; private MakeEnvironment.Builder makeEnv = null; private RuleVisibility defaultVisibility = ConstantRuleVisibility.PRIVATE; private boolean defaultVisibilitySet; private List<String> defaultCopts = null; private List<String> features = new ArrayList<>(); private List<Event> events = Lists.newArrayList(); private boolean containsErrors = false; private License defaultLicense = License.NO_LICENSE; private Set<License.DistributionType> defaultDistributionSet = License.DEFAULT_DISTRIB; protected Map<String, Target> targets = new HashMap<>(); protected Map<Label, EnvironmentGroup> environmentGroups = new HashMap<>(); protected Map<Label, Path> subincludes = null; protected ImmutableList<Label> skylarkFileDependencies = ImmutableList.of(); protected ExternalPackageBuilder externalPackageData = new ExternalPackageBuilder(); /** * True iff the "package" function has already been called in this package. */ private boolean packageFunctionUsed; /** * The collection of the prefixes of every output file. Maps every prefix * to an output file whose prefix it is. * * <p>This is needed to make the output file prefix conflict check be * reasonably fast. However, since it can potentially take a lot of memory and * is useless after the package has been loaded, it isn't passed to the * package itself. */ private Map<String, OutputFile> outputFilePrefixes = new HashMap<>(); private boolean alreadyBuilt = false; private EventHandler builderEventHandler = new EventHandler() { @Override public void handle(Event event) { addEvent(event); } }; protected Builder(Package pkg) { this.pkg = pkg; if (pkg.getName().startsWith("javatests/")) { setDefaultTestonly(true); } } public Builder(Helper helper, PackageIdentifier id, String runfilesPrefix) { this(helper.createFreshPackage(id, runfilesPrefix)); } protected PackageIdentifier getPackageIdentifier() { return pkg.getPackageIdentifier(); } /** Determine if we are in the WORKSPACE file or not */ public boolean isWorkspace() { return pkg.getPackageIdentifier().equals(Label.EXTERNAL_PACKAGE_IDENTIFIER); } /** * Sets the name of this package's BUILD file. */ Builder setFilename(Path filename) { this.filename = filename; try { buildFileLabel = createLabel(filename.getBaseName()); addInputFile(buildFileLabel, Location.fromFile(filename)); } catch (LabelSyntaxException e) { // This can't actually happen. throw new AssertionError("Package BUILD file has an illegal name: " + filename); } return this; } public Label getBuildFileLabel() { return buildFileLabel; } Path getFilename() { return filename; } public List<Event> getEvents() { return events; } /** * Sets this package's Make environment. */ Builder setMakeEnv(MakeEnvironment.Builder makeEnv) { this.makeEnv = makeEnv; return this; } MakeEnvironment.Builder getMakeEnvironment() { return makeEnv; } /** * Sets the default visibility for this package. Called at most once per * package from PackageFactory. */ Builder setDefaultVisibility(RuleVisibility visibility) { this.defaultVisibility = visibility; this.defaultVisibilitySet = true; return this; } /** * Sets whether the default visibility is set in the BUILD file. */ Builder setDefaultVisibilitySet(boolean defaultVisibilitySet) { this.defaultVisibilitySet = defaultVisibilitySet; return this; } /** Sets the default value of 'testonly'. Rule-level 'testonly' will override this. */ Builder setDefaultTestonly(boolean defaultTestonly) { pkg.setDefaultTestOnly(defaultTestonly); return this; } /** * Sets the default value of 'deprecation'. Rule-level 'deprecation' will append to this. */ Builder setDefaultDeprecation(String defaultDeprecation) { pkg.setDefaultDeprecation(defaultDeprecation); return this; } /** * Uses the workspace name from {@code //external} to set this package's workspace name. */ @VisibleForTesting public Builder setWorkspaceName(String workspaceName) { pkg.workspaceName = workspaceName; return this; } /** * Returns whether the "package" function has been called yet */ public boolean isPackageFunctionUsed() { return packageFunctionUsed; } public void setPackageFunctionUsed() { packageFunctionUsed = true; } /** * Sets the default header checking mode. */ public Builder setDefaultHdrsCheck(String hdrsCheck) { // Note that this setting is propagated directly to the package because // other code needs the ability to read this info directly from the // under-construction package. See {@link Package#setDefaultHdrsCheck}. pkg.setDefaultHdrsCheck(hdrsCheck); return this; } /** Sets the default value of copts. Rule-level copts will append to this. */ public Builder setDefaultCopts(List<String> defaultCopts) { this.defaultCopts = defaultCopts; return this; } public Builder addFeatures(Iterable<String> features) { Iterables.addAll(this.features, features); return this; } /** * Declares that errors were encountering while loading this package. */ public Builder setContainsErrors() { containsErrors = true; return this; } public boolean containsErrors() { return containsErrors; } public Builder addEvents(Iterable<Event> events) { for (Event event : events) { addEvent(event); } return this; } public Builder addEvent(Event event) { this.events.add(event); return this; } Builder setSkylarkFileDependencies(ImmutableList<Label> skylarkFileDependencies) { this.skylarkFileDependencies = skylarkFileDependencies; return this; } /** * Sets the default license for this package. */ void setDefaultLicense(License license) { this.defaultLicense = license; } License getDefaultLicense() { return defaultLicense; } /** * Initializes the default set of distributions for targets in this package. * * <p> TODO(bazel-team): (2011) consider moving the license & distribs info into Metadata--maybe * even in the Build language. */ void setDefaultDistribs(Set<DistributionType> dists) { this.defaultDistributionSet = dists; } Set<DistributionType> getDefaultDistribs() { return defaultDistributionSet; } /** * Sets the default value to use for a rule's {@link RuleClass#COMPATIBLE_ENVIRONMENT_ATTR} * attribute when not explicitly specified by the rule. Records a package error if * any labels are duplicated. */ void setDefaultCompatibleWith(List<Label> environments, String attrName, Location location) { if (!checkForDuplicateLabels(environments, "package " + pkg.getName(), attrName, location, builderEventHandler)) { setContainsErrors(); } pkg.setDefaultCompatibleWith(ImmutableSet.copyOf(environments)); } /** * Sets the default value to use for a rule's {@link RuleClass#RESTRICTED_ENVIRONMENT_ATTR} * attribute when not explicitly specified by the rule. Records a package error if * any labels are duplicated. */ void setDefaultRestrictedTo(List<Label> environments, String attrName, Location location) { if (!checkForDuplicateLabels(environments, "package " + pkg.getName(), attrName, location, builderEventHandler)) { setContainsErrors(); } pkg.setDefaultRestrictedTo(ImmutableSet.copyOf(environments)); } /** * Creates a new {@link Rule} {@code r} where {@code r.getPackage()} is the {@link Package} * associated with this {@link Builder}. * * <p>The created {@link Rule} will have no attribute values, no output files, and therefore * will be in an invalid state. */ Rule createRule( Label label, RuleClass ruleClass, Location location, AttributeContainer attributeContainer) { return new Rule( pkg, label, ruleClass, location, attributeContainer); } /** * Same as {@link #createRule(Label, RuleClass, Location, AttributeContainer)}, except * allows specifying an {@link ImplicitOutputsFunction} override. Only use if you know what * you're doing. */ Rule createRule( Label label, RuleClass ruleClass, Location location, AttributeContainer attributeContainer, ImplicitOutputsFunction implicitOutputsFunction) { return new Rule( pkg, label, ruleClass, location, attributeContainer, implicitOutputsFunction); } /** * Called by the parser when a "mocksubinclude" is encountered, to record the * mappings from labels to absolute paths upon which that the validity of * this package depends. */ void addSubinclude(Label label, Path resolvedPath) { if (subincludes == null) { // This is a TreeMap because the order needs to be deterministic. subincludes = Maps.newTreeMap(); } Path oldResolvedPath = subincludes.put(label, resolvedPath); if (oldResolvedPath != null && !oldResolvedPath.equals(resolvedPath)){ // The same label should have been resolved to the same path throw new IllegalStateException("Ambiguous subinclude path"); } } public Set<Label> getSubincludeLabels() { return subincludes == null ? Sets.<Label>newHashSet() : subincludes.keySet(); } public Map<Label, Path> getSubincludes() { return subincludes == null ? Maps.<Label, Path>newHashMap() : subincludes; } public Collection<Target> getTargets() { return Package.getTargets(targets); } @Nullable public Target getTarget(String name) { return targets.get(name); } /** * Returns an (immutable, unordered) view of all the targets belonging to * this package which are instances of the specified class. */ <T extends Target> Iterable<T> getTargets(Class<T> targetClass) { return Package.getTargets(targets, targetClass); } /** * An input file name conflicts with an existing package member. */ static class GeneratedLabelConflict extends NameConflictException { private GeneratedLabelConflict(String message) { super(message); } } /** * Creates an input file target in this package with the specified name. * * @param targetName name of the input file. This must be a valid target * name as defined by {@link * com.google.devtools.build.lib.cmdline.LabelValidator#validateTargetName}. * @return the newly-created InputFile, or the old one if it already existed. * @throws GeneratedLabelConflict if the name was already taken by a Rule or * an OutputFile target. * @throws IllegalArgumentException if the name is not a valid label */ InputFile createInputFile(String targetName, Location location) throws GeneratedLabelConflict { Target existing = targets.get(targetName); if (existing == null) { try { return addInputFile(createLabel(targetName), location); } catch (LabelSyntaxException e) { throw new IllegalArgumentException("FileTarget in package " + pkg.getName() + " has illegal name: " + targetName); } } else if (existing instanceof InputFile) { return (InputFile) existing; // idempotent } else { throw new GeneratedLabelConflict("generated label '//" + pkg.getName() + ":" + targetName + "' conflicts with existing " + existing.getTargetKind()); } } /** * Sets the visibility and license for an input file. The input file must already exist as * a member of this package. * @throws IllegalArgumentException if the input file doesn't exist in this * package's target map. */ void setVisibilityAndLicense(InputFile inputFile, RuleVisibility visibility, License license) { String filename = inputFile.getName(); Target cacheInstance = targets.get(filename); if (!(cacheInstance instanceof InputFile)) { throw new IllegalArgumentException("Can't set visibility for nonexistent FileTarget " + filename + " in package " + pkg.getName() + "."); } if (!((InputFile) cacheInstance).isVisibilitySpecified() || cacheInstance.getVisibility() != visibility || cacheInstance.getLicense() != license) { targets.put(filename, new InputFile( pkg, cacheInstance.getLabel(), cacheInstance.getLocation(), visibility, license)); } } /** * Creates a label for a target inside this package. * * @throws LabelSyntaxException if the {@code targetName} is invalid */ Label createLabel(String targetName) throws LabelSyntaxException { return Label.create(pkg.getPackageIdentifier(), targetName); } /** * Adds a package group to the package. */ void addPackageGroup(String name, Collection<String> packages, Collection<Label> includes, EventHandler eventHandler, Location location) throws NameConflictException, LabelSyntaxException { PackageGroup group = new PackageGroup(createLabel(name), pkg, packages, includes, eventHandler, location); Target existing = targets.get(group.getName()); if (existing != null) { throw nameConflict(group, existing); } targets.put(group.getName(), group); if (group.containsErrors()) { setContainsErrors(); } } /** * Checks if any labels in the given list appear multiple times and reports an appropriate * error message if so. Returns true if no duplicates were found, false otherwise. * * <p> TODO(bazel-team): apply this to all build functions (maybe automatically?), possibly * integrate with RuleClass.checkForDuplicateLabels. */ private static boolean checkForDuplicateLabels(Collection<Label> labels, String owner, String attrName, Location location, EventHandler eventHandler) { Set<Label> dupes = CollectionUtils.duplicatedElementsOf(labels); for (Label dupe : dupes) { eventHandler.handle(Event.error(location, String.format( "label '%s' is duplicated in the '%s' list of '%s'", dupe, attrName, owner))); } return dupes.isEmpty(); } /** * Adds an environment group to the package. */ void addEnvironmentGroup(String name, List<Label> environments, List<Label> defaults, EventHandler eventHandler, Location location) throws NameConflictException, LabelSyntaxException { if (!checkForDuplicateLabels(environments, name, "environments", location, eventHandler) || !checkForDuplicateLabels(defaults, name, "defaults", location, eventHandler)) { setContainsErrors(); return; } EnvironmentGroup group = new EnvironmentGroup(createLabel(name), pkg, environments, defaults, location); Target existing = targets.get(group.getName()); if (existing != null) { throw nameConflict(group, existing); } targets.put(group.getName(), group); Collection<Event> membershipErrors = group.validateMembership(); if (!membershipErrors.isEmpty()) { for (Event error : membershipErrors) { eventHandler.handle(error); } setContainsErrors(); return; } // For each declared environment, make sure it doesn't also belong to some other group. for (Label environment : group.getEnvironments()) { EnvironmentGroup otherGroup = environmentGroups.get(environment); if (otherGroup != null) { eventHandler.handle(Event.error(location, "environment " + environment + " belongs to" + " both " + group.getLabel() + " and " + otherGroup.getLabel())); setContainsErrors(); } else { environmentGroups.put(environment, group); } } } /** * Same as {@link #addRule}, except with no name conflict checks. * * <p>Don't call this function unless you know what you're doing. */ void addRuleUnchecked(Rule rule) { Preconditions.checkArgument(rule.getPackage() == pkg); // Now, modify the package: for (OutputFile outputFile : rule.getOutputFiles()) { targets.put(outputFile.getName(), outputFile); PathFragment outputFileFragment = PathFragment.create(outputFile.getName()); for (int i = 1; i < outputFileFragment.segmentCount(); i++) { String prefix = outputFileFragment.subFragment(0, i).toString(); if (!outputFilePrefixes.containsKey(prefix)) { outputFilePrefixes.put(prefix, outputFile); } } } targets.put(rule.getName(), rule); if (rule.containsErrors()) { this.setContainsErrors(); } } void addRule(Rule rule) throws NameConflictException, InterruptedException { checkForConflicts(rule); addRuleUnchecked(rule); } private Builder beforeBuild(boolean discoverAssumedInputFiles) throws InterruptedException { Preconditions.checkNotNull(pkg); Preconditions.checkNotNull(filename); Preconditions.checkNotNull(buildFileLabel); Preconditions.checkNotNull(makeEnv); // Freeze subincludes. subincludes = (subincludes == null) ? Collections.<Label, Path>emptyMap() : Collections.unmodifiableMap(subincludes); // We create the original BUILD InputFile when the package filename is set; however, the // visibility may be overridden with an exports_files directive, so we need to obtain the // current instance here. buildFile = (InputFile) Preconditions.checkNotNull(targets.get(buildFileLabel.getName())); List<Rule> rules = Lists.newArrayList(getTargets(Rule.class)); if (discoverAssumedInputFiles) { // All labels mentioned in a rule that refer to an unknown target in the // current package are assumed to be InputFiles, so let's create them: for (final Rule rule : rules) { AggregatingAttributeMapper.of(rule).visitLabels(new AcceptsLabelAttribute() { @Override public void acceptLabelAttribute(Label label, Attribute attribute) { createInputFileMaybe(label, rule.getAttributeLocation(attribute.getName())); } }); } } // "test_suite" rules have the idiosyncratic semantics of implicitly // depending on all tests in the package, iff tests=[] and suites=[]. // Note, we implement this here when the Package is fully constructed, // since clearly this information isn't available at Rule construction // time, as forward references are permitted. List<Label> allTests = new ArrayList<>(); for (Rule rule : rules) { if (TargetUtils.isTestRule(rule) && !TargetUtils.hasManualTag(rule)) { allTests.add(rule.getLabel()); } } Collections.sort(allTests); for (Rule rule : rules) { AttributeMap attributes = NonconfigurableAttributeMapper.of(rule); if (rule.getRuleClass().equals("test_suite") && attributes.get("tests", BuildType.LABEL_LIST).isEmpty()) { rule.setAttributeValueByName("$implicit_tests", allTests); } } return this; } /** Intended for use by {@link com.google.devtools.build.lib.skyframe.PackageFunction} only. */ public Builder buildPartial() throws InterruptedException { if (alreadyBuilt) { return this; } return beforeBuild(/*discoverAssumedInputFiles=*/ true); } /** * Removes a target from the {@link Package} under construction. Intended to be used only by * {@link com.google.devtools.build.lib.skyframe.PackageFunction} to remove targets whose * labels cross subpackage boundaries. */ public void removeTarget(Target target) { if (target.getPackage() == pkg) { this.targets.remove(target.getName()); } } /** Intended for use by {@link com.google.devtools.build.lib.skyframe.PackageFunction} only. */ public Package finishBuild() { if (alreadyBuilt) { return pkg; } // Freeze targets and distributions. targets = ImmutableMap.copyOf(targets); defaultDistributionSet = Collections.unmodifiableSet(defaultDistributionSet); // Now all targets have been loaded, so we validate the group's member environments. for (EnvironmentGroup envGroup : ImmutableSet.copyOf(environmentGroups.values())) { Collection<Event> errors = envGroup.processMemberEnvironments(targets); if (!errors.isEmpty()) { addEvents(errors); setContainsErrors(); } } // Build the package. pkg.finishInit(this); alreadyBuilt = true; return pkg; } public ExternalPackageBuilder externalPackageData() { return externalPackageData; } public Package build() throws InterruptedException { return build(/*discoverAssumedInputFiles=*/ true); } /** * Build the package, optionally adding any labels in the package not already associated with * a target as an input file. */ public Package build(boolean discoverAssumedInputFiles) throws InterruptedException { if (alreadyBuilt) { return pkg; } beforeBuild(discoverAssumedInputFiles); return finishBuild(); } /** * If "label" refers to a non-existent target in the current package, create * an InputFile target. */ void createInputFileMaybe(Label label, Location location) { if (label != null && label.getPackageIdentifier().equals(pkg.getPackageIdentifier())) { if (!targets.containsKey(label.getName())) { addInputFile(label, location); } } } private InputFile addInputFile(Label label, Location location) { InputFile inputFile = new InputFile(pkg, label, location); Target prev = targets.put(label.getName(), inputFile); Preconditions.checkState(prev == null); return inputFile; } /** * Precondition check for addRule. We must maintain these invariants of the package: * * <ul> * <li>Each name refers to at most one target. * <li>No rule with errors is inserted into the package. * <li>The generating rule of every output file in the package must itself be in the package. * </ul> */ private void checkForConflicts(Rule rule) throws NameConflictException, InterruptedException { String name = rule.getName(); Target existing = targets.get(name); if (existing != null) { throw nameConflict(rule, existing); } Map<String, OutputFile> outputFiles = new HashMap<>(); for (OutputFile outputFile : rule.getOutputFiles()) { String outputFileName = outputFile.getName(); if (outputFiles.put(outputFileName, outputFile) != null) { // dups within a single rule: throw duplicateOutputFile(outputFile, outputFile); } existing = targets.get(outputFileName); if (existing != null) { throw duplicateOutputFile(outputFile, existing); } // Check if this output file is the prefix of an already existing one if (outputFilePrefixes.containsKey(outputFileName)) { throw conflictingOutputFile(outputFile, outputFilePrefixes.get(outputFileName)); } // Check if a prefix of this output file matches an already existing one PathFragment outputFileFragment = PathFragment.create(outputFileName); for (int i = 1; i < outputFileFragment.segmentCount(); i++) { String prefix = outputFileFragment.subFragment(0, i).toString(); if (outputFiles.containsKey(prefix)) { throw conflictingOutputFile(outputFile, outputFiles.get(prefix)); } if (targets.containsKey(prefix) && targets.get(prefix) instanceof OutputFile) { throw conflictingOutputFile(outputFile, (OutputFile) targets.get(prefix)); } if (!outputFilePrefixes.containsKey(prefix)) { outputFilePrefixes.put(prefix, outputFile); } } } checkForInputOutputConflicts(rule, outputFiles.keySet()); } /** * A utility method that checks for conflicts between input file names and output file names for * a rule from a build file. * * @param rule the rule whose inputs and outputs are to be checked for conflicts. * @param outputFiles a set containing the names of output files to be generated by the rule. * @throws NameConflictException if a conflict is found. */ private void checkForInputOutputConflicts(Rule rule, Set<String> outputFiles) throws NameConflictException, InterruptedException { PathFragment packageFragment = rule.getLabel().getPackageFragment(); for (Label inputLabel : rule.getLabels()) { if (packageFragment.equals(inputLabel.getPackageFragment()) && outputFiles.contains(inputLabel.getName())) { throw inputOutputNameConflict(rule, inputLabel.getName()); } } } /** An output file conflicts with another output file or the BUILD file. */ private NameConflictException duplicateOutputFile(OutputFile duplicate, Target existing) { return new NameConflictException(duplicate.getTargetKind() + " '" + duplicate.getName() + "' in rule '" + duplicate.getGeneratingRule().getName() + "' " + conflictsWith(existing)); } /** The package contains two targets with the same name. */ private NameConflictException nameConflict(Target duplicate, Target existing) { return new NameConflictException(duplicate.getTargetKind() + " '" + duplicate.getName() + "' in package '" + duplicate.getLabel().getPackageName() + "' " + conflictsWith(existing)); } /** A a rule has a input/output name conflict. */ private NameConflictException inputOutputNameConflict(Rule rule, String conflictingName) { return new NameConflictException("rule '" + rule.getName() + "' has file '" + conflictingName + "' as both an input and an output"); } private static NameConflictException conflictingOutputFile( OutputFile added, OutputFile existing) { if (added.getGeneratingRule() == existing.getGeneratingRule()) { return new NameConflictException(String.format( "rule '%s' has conflicting output files '%s' and '%s'", added.getGeneratingRule() .getName(), added.getName(), existing.getName())); } else { return new NameConflictException(String.format( "output file '%s' of rule '%s' conflicts with output file '%s' of rule '%s'", added .getName(), added.getGeneratingRule().getName(), existing.getName(), existing .getGeneratingRule().getName())); } } /** * Utility function for generating exception messages. */ private static String conflictsWith(Target target) { String message = "conflicts with existing "; if (target instanceof OutputFile) { message += "generated file from rule '" + ((OutputFile) target).getGeneratingRule().getName() + "'"; } else { message += target.getTargetKind(); } return message + ", defined at " + target.getLocation(); } } }