// 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.analysis;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
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.ListMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.actions.Action;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionOwner;
import com.google.devtools.build.lib.actions.ActionRegistry;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactOwner;
import com.google.devtools.build.lib.actions.Root;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider.PrerequisiteValidator;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext;
import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
import com.google.devtools.build.lib.analysis.config.BuildOptions;
import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
import com.google.devtools.build.lib.analysis.config.FragmentCollection;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.ImmutableSortedKeyListMultimap;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
import com.google.devtools.build.lib.packages.Attribute.SplitTransition;
import com.google.devtools.build.lib.packages.AttributeMap;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.ConfigurationFragmentPolicy;
import com.google.devtools.build.lib.packages.FileTarget;
import com.google.devtools.build.lib.packages.FilesetEntry;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.InputFile;
import com.google.devtools.build.lib.packages.OutputFile;
import com.google.devtools.build.lib.packages.PackageSpecification;
import com.google.devtools.build.lib.packages.RawAttributeMapper;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.packages.RuleErrorConsumer;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.rules.AliasProvider;
import com.google.devtools.build.lib.rules.fileset.FilesetProvider;
import com.google.devtools.build.lib.shell.ShellUtils;
import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.util.FileTypeSet;
import com.google.devtools.build.lib.util.OrderedSetMultimap;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.util.StringUtil;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* The totality of data available during the analysis of a rule.
*
*
These objects should not outlast the analysis phase. Do not pass them to {@link Action}
* objects or other persistent objects. There are internal tests to ensure that RuleContext objects
* are not persisted that check the name of this class, so update those tests if you change this
* class's name.
*
* @see com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory
*/
public final class RuleContext extends TargetContext
implements ActionConstructionContext, ActionRegistry, RuleErrorConsumer {
/**
* The configured version of FilesetEntry.
*/
@Immutable
public static final class ConfiguredFilesetEntry {
private final FilesetEntry entry;
private final TransitiveInfoCollection src;
private final ImmutableList files;
ConfiguredFilesetEntry(FilesetEntry entry, TransitiveInfoCollection src) {
this.entry = entry;
this.src = src;
this.files = null;
}
ConfiguredFilesetEntry(FilesetEntry entry, ImmutableList files) {
this.entry = entry;
this.src = null;
this.files = files;
}
public FilesetEntry getEntry() {
return entry;
}
public TransitiveInfoCollection getSrc() {
return src;
}
/**
* Targets from FilesetEntry.files, or null if the user omitted it.
*/
@Nullable
public ImmutableList getFiles() {
return files;
}
}
private static final String HOST_CONFIGURATION_PROGRESS_TAG = "for host";
private final Rule rule;
private final ListMultimap targetMap;
private final ListMultimap filesetEntryMap;
private final ImmutableMap configConditions;
private final AttributeMap attributes;
private final ImmutableSet features;
private final String ruleClassNameForLogging;
private final ImmutableMap aspectAttributes;
private final BuildConfiguration hostConfiguration;
private final ConfigurationFragmentPolicy configurationFragmentPolicy;
private final Class extends BuildConfiguration.Fragment> universalFragment;
private final ErrorReporter reporter;
private final ImmutableBiMap>
skylarkProviderRegistry;
private ActionOwner actionOwner;
/* lazily computed cache for Make variables, computed from the above. See get... method */
private transient ConfigurationMakeVariableContext configurationMakeVariableContext = null;
private RuleContext(
Builder builder,
AttributeMap attributes,
ListMultimap targetMap,
ListMultimap filesetEntryMap,
ImmutableMap configConditions,
Class extends BuildConfiguration.Fragment> universalFragment,
String ruleClassNameForLogging,
ImmutableMap aspectAttributes) {
super(builder.env, builder.rule, builder.configuration, builder.prerequisiteMap.get(null),
builder.visibility);
this.rule = builder.rule;
this.configurationFragmentPolicy = builder.configurationFragmentPolicy;
this.universalFragment = universalFragment;
this.targetMap = targetMap;
this.filesetEntryMap = filesetEntryMap;
this.configConditions = configConditions;
this.attributes = attributes;
this.features = getEnabledFeatures();
this.ruleClassNameForLogging = ruleClassNameForLogging;
this.aspectAttributes = aspectAttributes;
this.skylarkProviderRegistry = builder.skylarkProviderRegistry;
this.hostConfiguration = builder.hostConfiguration;
reporter = builder.reporter;
}
private ImmutableSet getEnabledFeatures() {
Set globallyEnabled = new HashSet<>();
Set globallyDisabled = new HashSet<>();
parseFeatures(getConfiguration().getDefaultFeatures(), globallyEnabled, globallyDisabled);
for (ImmutableMap.Entry, Fragment> entry :
getConfiguration().getAllFragments().entrySet()) {
if (isLegalFragment(entry.getKey())) {
globallyEnabled.addAll(entry.getValue().configurationEnabledFeatures(this));
}
}
Set packageEnabled = new HashSet<>();
Set packageDisabled = new HashSet<>();
parseFeatures(getRule().getPackage().getFeatures(), packageEnabled, packageDisabled);
Set packageFeatures =
Sets.difference(Sets.union(globallyEnabled, packageEnabled), packageDisabled);
Set ruleFeatures = packageFeatures;
if (attributes().has("features", Type.STRING_LIST)) {
Set ruleEnabled = new HashSet<>();
Set ruleDisabled = new HashSet<>();
parseFeatures(attributes().get("features", Type.STRING_LIST), ruleEnabled, ruleDisabled);
ruleFeatures = Sets.difference(Sets.union(packageFeatures, ruleEnabled), ruleDisabled);
}
return ImmutableSortedSet.copyOf(Sets.difference(ruleFeatures, globallyDisabled));
}
private void parseFeatures(Iterable features, Set enabled, Set disabled) {
for (String feature : features) {
if (feature.startsWith("-")) {
disabled.add(feature.substring(1));
} else if (feature.equals("no_layering_check")) {
// TODO(bazel-team): Remove once we do not have BUILD files left that contain
// 'no_layering_check'.
disabled.add(feature.substring(3));
} else {
enabled.add(feature);
}
}
}
@Override
public Rule getRule() {
return rule;
}
/**
* Returns a rule class name suitable for log messages, including an aspect name if applicable.
*/
public String getRuleClassNameForLogging() {
return ruleClassNameForLogging;
}
/**
* Returns the workspace name for the rule.
*/
public String getWorkspaceName() {
return rule.getPackage().getWorkspaceName();
}
/**
* The configuration conditions that trigger this rule's configurable attributes.
*/
ImmutableMap getConfigConditions() {
return configConditions;
}
/**
* Returns the host configuration for this rule.
*/
public BuildConfiguration getHostConfiguration() {
return hostConfiguration;
}
/**
* Attributes from aspects.
*/
public ImmutableMap getAspectAttributes() {
return aspectAttributes;
}
/**
* Accessor for the Rule's attribute values.
*/
public AttributeMap attributes() {
return attributes;
}
/**
* Returns a map that indicates which providers should be exported to skylark under the key
* (map key). These provider types will also be exportable by skylark rules under (map key).
*/
public ImmutableBiMap>
getSkylarkProviderRegistry() {
return skylarkProviderRegistry;
}
/**
* Returns whether this instance is known to have errors at this point during analysis. Do not
* call this method after the initializationHook has returned.
*/
public boolean hasErrors() {
return getAnalysisEnvironment().hasErrors();
}
/**
* No-op if {@link #hasErrors} is false, throws {@link RuleErrorException} if it is true.
* This provides a convenience to early-exit of configured target creation if there are errors.
*/
public void assertNoErrors() throws RuleErrorException {
if (hasErrors()) {
throw new RuleErrorException();
}
}
/**
* Returns an immutable map from attribute name to list of configured targets for that attribute.
*/
public ListMultimap getConfiguredTargetMap() {
return targetMap;
}
/**
* Returns an immutable map from attribute name to list of fileset entries.
*/
public ListMultimap getFilesetEntryMap() {
return filesetEntryMap;
}
@Override
public ActionOwner getActionOwner() {
if (actionOwner == null) {
actionOwner = createActionOwner(rule, getConfiguration());
}
return actionOwner;
}
/**
* Returns a configuration fragment for this this target.
*/
@Nullable
public T getFragment(Class fragment, ConfigurationTransition config) {
return getFragment(fragment, fragment.getSimpleName(), "", config);
}
@Nullable
protected T getFragment(Class fragment, String name,
String additionalErrorMessage, ConfigurationTransition config) {
// TODO(bazel-team): The fragments can also be accessed directly through BuildConfiguration.
// Can we lock that down somehow?
Preconditions.checkArgument(isLegalFragment(fragment, config),
"%s has to declare '%s' as a required fragment "
+ "in %s configuration in order to access it.%s",
getRuleClassNameForLogging(), name, FragmentCollection.getConfigurationName(config),
additionalErrorMessage);
return getConfiguration(config).getFragment(fragment);
}
@Nullable
public T getFragment(Class fragment) {
// NONE means target configuration.
return getFragment(fragment, ConfigurationTransition.NONE);
}
@Nullable
public Fragment getSkylarkFragment(String name, ConfigurationTransition config) {
Class extends Fragment> fragmentClass =
getConfiguration(config).getSkylarkFragmentByName(name);
if (fragmentClass == null) {
return null;
}
return getFragment(fragmentClass, name,
String.format(
" Please update the '%1$sfragments' argument of the rule definition "
+ "(for example: %1$sfragments = [\"%2$s\"])",
(config == ConfigurationTransition.HOST) ? "host_" : "", name),
config);
}
public ImmutableCollection getSkylarkFragmentNames(ConfigurationTransition config) {
return getConfiguration(config).getSkylarkFragmentNames();
}
public boolean isLegalFragment(
Class fragment, ConfigurationTransition config) {
return fragment == universalFragment
|| configurationFragmentPolicy.isLegalConfigurationFragment(fragment, config);
}
public boolean isLegalFragment(Class fragment) {
// NONE means target configuration.
return isLegalFragment(fragment, ConfigurationTransition.NONE);
}
protected BuildConfiguration getConfiguration(ConfigurationTransition config) {
return config.equals(ConfigurationTransition.HOST) ? hostConfiguration : getConfiguration();
}
@Override
public ArtifactOwner getOwner() {
return getAnalysisEnvironment().getOwner();
}
public ImmutableList getBuildInfo(BuildInfoKey key) throws InterruptedException {
return getAnalysisEnvironment().getBuildInfo(this, key, getConfiguration());
}
@VisibleForTesting
public static ActionOwner createActionOwner(Rule rule, BuildConfiguration configuration) {
return new ActionOwner(
rule.getLabel(),
rule.getLocation(),
configuration.getMnemonic(),
rule.getTargetKind(),
configuration.checksum(),
configuration.isHostConfiguration() ? HOST_CONFIGURATION_PROGRESS_TAG : null);
}
@Override
public void registerAction(ActionAnalysisMetadata... action) {
getAnalysisEnvironment().registerAction(action);
}
/**
* Convenience function for subclasses to report non-attribute-specific
* errors in the current rule.
*/
@Override
public void ruleError(String message) {
reporter.ruleError(message);
}
/**
* Convenience function to report non-attribute-specific errors in the current rule and then
* throw a {@link RuleErrorException}, immediately exiting the build invocation. Alternatively,
* invoke {@link #ruleError} instead to collect additional error information before ending the
* invocation.
*/
public void throwWithRuleError(String message) throws RuleErrorException {
reporter.ruleError(message);
throw new RuleErrorException();
}
/**
* Convenience function for subclasses to report non-attribute-specific
* warnings in the current rule.
*/
@Override
public void ruleWarning(String message) {
reporter.ruleWarning(message);
}
/**
* Convenience function for subclasses to report attribute-specific errors in
* the current rule.
*
* If the name of the attribute starts with $
* it is replaced with a string (an implicit dependency)
.
*/
@Override
public void attributeError(String attrName, String message) {
reporter.attributeError(attrName, message);
}
/**
* Convenience function to report attribute-specific errors in the current rule, and then throw a
* {@link RuleErrorException}, immediately exiting the build invocation. Alternatively, invoke
* {@link #attributeError} instead to collect additional error information before ending the
* invocation.
*
*
If the name of the attribute starts with $
* it is replaced with a string (an implicit dependency)
.
*/
public RuleErrorException throwWithAttributeError(String attrName, String message)
throws RuleErrorException {
reporter.attributeError(attrName, message);
throw new RuleErrorException();
}
/**
* Like attributeError, but does not mark the configured target as errored.
*
*
If the name of the attribute starts with $
* it is replaced with a string (an implicit dependency)
.
*/
@Override
public void attributeWarning(String attrName, String message) {
reporter.attributeWarning(attrName, message);
}
/**
* Returns an artifact beneath the root of either the "bin" or "genfiles"
* tree, whose path is based on the name of this target and the current
* configuration. The choice of which tree to use is based on the rule with
* which this target (which must be an OutputFile or a Rule) is associated.
*/
public Artifact createOutputArtifact() {
return internalCreateOutputArtifact(getTarget());
}
/**
* Returns the output artifact of an {@link OutputFile} of this target.
*
* @see #createOutputArtifact()
*/
public Artifact createOutputArtifact(OutputFile out) {
return internalCreateOutputArtifact(out);
}
/**
* Implementation for {@link #createOutputArtifact()} and
* {@link #createOutputArtifact(OutputFile)}. This is private so that
* {@link #createOutputArtifact(OutputFile)} can have a more specific
* signature.
*/
private Artifact internalCreateOutputArtifact(Target target) {
Preconditions.checkState(
target.getLabel().getPackageIdentifier().equals(getLabel().getPackageIdentifier()),
"Creating output artifact for target '%s' in different package than the rule '%s' "
+ "being analyzed", target.getLabel(), getLabel());
Root root = getBinOrGenfilesDirectory();
return getPackageRelativeArtifact(target.getName(), root);
}
/**
* Returns the root of either the "bin" or "genfiles"
* tree, based on this target and the current configuration.
* The choice of which tree to use is based on the rule with
* which this target (which must be an OutputFile or a Rule) is associated.
*/
public Root getBinOrGenfilesDirectory() {
return rule.hasBinaryOutput() ? getBinDirectory() : getGenfilesDirectory();
}
/**
* Returns the bin directory for this build configuration.
*/
@SkylarkCallable(name = "bin_dir", structField = true,
doc = "The root corresponding to bin directory.")
public Root getBinDirectory() {
return getConfiguration().getBinDirectory(rule.getRepository());
}
/**
* Returns the genfiles directory for this build configuration.
*/
@SkylarkCallable(name = "genfiles_dir", structField = true,
doc = "The root corresponding to genfiles directory.")
public Root getGenfilesDirectory() {
return getConfiguration().getGenfilesDirectory(rule.getRepository());
}
/**
* Creates an artifact in a directory that is unique to the package that contains the rule,
* thus guaranteeing that it never clashes with artifacts created by rules in other packages.
*/
public Artifact getPackageRelativeArtifact(String relative, Root root) {
return getPackageRelativeArtifact(new PathFragment(relative), root);
}
/**
* Creates an artifact in a directory that is unique to the package that contains the rule, thus
* guaranteeing that it never clashes with artifacts created by rules in other packages.
*/
public Artifact getBinArtifact(String relative) {
return getBinArtifact(new PathFragment(relative));
}
public Artifact getBinArtifact(PathFragment relative) {
return getPackageRelativeArtifact(
relative, getConfiguration().getBinDirectory(rule.getRepository()));
}
/**
* Creates an artifact in a directory that is unique to the package that contains the rule, thus
* guaranteeing that it never clashes with artifacts created by rules in other packages.
*/
public Artifact getGenfilesArtifact(String relative) {
return getGenfilesArtifact(new PathFragment(relative));
}
public Artifact getGenfilesArtifact(PathFragment relative) {
return getPackageRelativeArtifact(
relative, getConfiguration().getGenfilesDirectory(rule.getRepository()));
}
/**
* Returns an artifact that can be an output of shared actions. Only use when there is no other
* option.
*
*
This artifact can be created anywhere in the output tree, which, in addition to making
* sharing possible, opens up the possibility of action conflicts and makes it impossible to
* infer the label of the rule creating the artifact from the path of the artifact.
*/
public Artifact getShareableArtifact(PathFragment rootRelativePath, Root root) {
return getAnalysisEnvironment().getDerivedArtifact(rootRelativePath, root);
}
/**
* Creates an artifact in a directory that is unique to the package that contains the rule,
* thus guaranteeing that it never clashes with artifacts created by rules in other packages.
*/
public Artifact getPackageRelativeArtifact(PathFragment relative, Root root) {
return getDerivedArtifact(getPackageDirectory().getRelative(relative), root);
}
/**
* Returns the root-relative path fragment under which output artifacts of this rule should go.
*
*
Note that:
*
* This doesn't guarantee that there are no clashes with rules in the same package.
* If possible, {@link #getPackageRelativeArtifact(PathFragment, Root)} should be used
* instead of this method.
*
*
* Ideally, user-visible artifacts should all have corresponding output file targets, all others
* should go into a rule-specific directory.
* {@link #getUniqueDirectoryArtifact(String, PathFragment, Root)}) ensures that this is the case.
*/
public PathFragment getPackageDirectory() {
return getLabel().getPackageIdentifier().getSourceRoot();
}
/**
* Creates an artifact under a given root with the given root-relative path.
*
* Verifies that it is in the root-relative directory corresponding to the package of the rule,
* thus ensuring that it doesn't clash with other artifacts generated by other rules using this
* method.
*/
@Override
public Artifact getDerivedArtifact(PathFragment rootRelativePath, Root root) {
Preconditions.checkState(rootRelativePath.startsWith(getPackageDirectory()),
"Output artifact '%s' not under package directory '%s' for target '%s'",
rootRelativePath, getPackageDirectory(), getLabel());
return getAnalysisEnvironment().getDerivedArtifact(rootRelativePath, root);
}
/**
* Creates a TreeArtifact under a given root with the given root-relative path.
*
*
Verifies that it is in the root-relative directory corresponding to the package of the rule,
* thus ensuring that it doesn't clash with other artifacts generated by other rules using this
* method.
*/
public Artifact getTreeArtifact(PathFragment rootRelativePath, Root root) {
Preconditions.checkState(rootRelativePath.startsWith(getPackageDirectory()),
"Output artifact '%s' not under package directory '%s' for target '%s'",
rootRelativePath, getPackageDirectory(), getLabel());
return getAnalysisEnvironment().getTreeArtifact(rootRelativePath, root);
}
/**
* Creates an artifact in a directory that is unique to the rule, thus guaranteeing that it never
* clashes with artifacts created by other rules.
*/
public Artifact getUniqueDirectoryArtifact(
String uniqueDirectory, String relative, Root root) {
return getUniqueDirectoryArtifact(uniqueDirectory, new PathFragment(relative), root);
}
/**
* Creates an artifact in a directory that is unique to the rule, thus guaranteeing that it never
* clashes with artifacts created by other rules.
*/
public Artifact getUniqueDirectoryArtifact(
String uniqueDirectory, PathFragment relative, Root root) {
return getDerivedArtifact(getUniqueDirectory(uniqueDirectory).getRelative(relative), root);
}
/**
* Returns the Attribute associated with this name, if it's a valid attribute for this rule,
* or is associated with an attached aspect. Otherwise returns null.
*/
@Nullable
public Attribute getAttribute(String attributeName) {
Attribute result = getRule().getAttributeDefinition(attributeName);
if (result != null) {
return result;
}
return aspectAttributes.get(attributeName);
}
/**
* Returns the dependencies through a {@code LABEL_DICT_UNARY} attribute as a map from
* a string to a {@link TransitiveInfoCollection}.
*/
public Map getPrerequisiteMap(String attributeName) {
Attribute attributeDefinition = getAttribute(attributeName);
Preconditions.checkState(attributeDefinition.getType() == BuildType.LABEL_DICT_UNARY);
ImmutableMap.Builder result = ImmutableMap.builder();
Map dict = attributes().get(attributeName, BuildType.LABEL_DICT_UNARY);
Map labelToDep = new HashMap<>();
for (ConfiguredTarget dep : targetMap.get(attributeName)) {
labelToDep.put(dep.getLabel(), dep);
}
for (Map.Entry entry : dict.entrySet()) {
result.put(entry.getKey(), Preconditions.checkNotNull(labelToDep.get(entry.getValue())));
}
return result.build();
}
/**
* Returns the list of transitive info collections that feed into this target through the
* specified attribute. Note that you need to specify the correct mode for the attribute,
* otherwise an assertion will be raised.
*/
public List extends TransitiveInfoCollection> getPrerequisites(String attributeName,
Mode mode) {
Attribute attributeDefinition = getAttribute(attributeName);
if ((mode == Mode.TARGET) && (attributeDefinition.hasSplitConfigurationTransition())) {
// TODO(bazel-team): If you request a split-configured attribute in the target configuration,
// we return only the list of configured targets for the first architecture; this is for
// backwards compatibility with existing code in cases where the call to getPrerequisites is
// deeply nested and we can't easily inject the behavior we want. However, we should fix all
// such call sites.
checkAttribute(attributeName, Mode.SPLIT);
Map, ? extends List extends TransitiveInfoCollection>> map =
getSplitPrerequisites(attributeName);
return map.isEmpty()
? ImmutableList.of()
: map.entrySet().iterator().next().getValue();
}
checkAttribute(attributeName, mode);
return targetMap.get(attributeName);
}
/**
* Returns the a prerequisites keyed by the CPU of their configurations.
* If the split transition is not active (e.g. split() returned an empty
* list), the key is an empty Optional.
*/
public Map, ? extends List extends TransitiveInfoCollection>>
getSplitPrerequisites(String attributeName) {
checkAttribute(attributeName, Mode.SPLIT);
Attribute attributeDefinition = getAttribute(attributeName);
@SuppressWarnings("unchecked") // Attribute.java doesn't have the BuildOptions symbol.
SplitTransition transition =
(SplitTransition) attributeDefinition.getSplitTransition(rule);
List deps = targetMap.get(attributeName);
List splitOptions = transition.split(getConfiguration().getOptions());
if (splitOptions.isEmpty()) {
// The split transition is not active. Defer the decision on which CPU to use.
return ImmutableMap.of(Optional.absent(), deps);
}
Set cpus = new HashSet<>();
for (BuildOptions options : splitOptions) {
// This method should only be called when the split config is enabled on the command line, in
// which case this cpu can't be null.
cpus.add(options.get(BuildConfiguration.Options.class).cpu);
}
// Use an ImmutableListMultimap.Builder here to preserve ordering.
ImmutableListMultimap.Builder, TransitiveInfoCollection> result =
ImmutableListMultimap.builder();
for (TransitiveInfoCollection t : deps) {
if (t.getConfiguration() != null) {
result.put(Optional.of(t.getConfiguration().getCpu()), t);
} else {
// Source files don't have a configuration, so we add them to all architecture entries.
for (String cpu : cpus) {
result.put(Optional.of(cpu), t);
}
}
}
return Multimaps.asMap(result.build());
}
/**
* Returns the specified provider of the prerequisite referenced by the attribute in the
* argument. Note that you need to specify the correct mode for the attribute, otherwise an
* assertion will be raised. If the attribute is empty of it does not support the specified
* provider, returns null.
*/
public C getPrerequisite(
String attributeName, Mode mode, Class provider) {
TransitiveInfoCollection prerequisite = getPrerequisite(attributeName, mode);
return prerequisite == null ? null : prerequisite.getProvider(provider);
}
/**
* Returns the transitive info collection that feeds into this target through the specified
* attribute. Note that you need to specify the correct mode for the attribute, otherwise an
* assertion will be raised. Returns null if the attribute is empty.
*/
public TransitiveInfoCollection getPrerequisite(String attributeName, Mode mode) {
checkAttribute(attributeName, mode);
List extends TransitiveInfoCollection> elements = targetMap.get(attributeName);
if (elements.size() > 1) {
throw new IllegalStateException(getRuleClassNameForLogging() + " attribute " + attributeName
+ " produces more than one prerequisite");
}
return elements.isEmpty() ? null : elements.get(0);
}
/**
* For a given attribute, returns all {@link TransitiveInfoProvider}s provided by targets
* of that attribute. Each {@link TransitiveInfoProvider} is keyed by the
* {@link BuildConfiguration} under which the provider was created.
*/
public ImmutableListMultimap
getPrerequisitesByConfiguration(String attributeName, Mode mode, final Class classType) {
AnalysisUtils.checkProvider(classType);
List extends TransitiveInfoCollection> transitiveInfoCollections =
getPrerequisites(attributeName, mode);
ImmutableListMultimap.Builder result =
ImmutableListMultimap.builder();
for (TransitiveInfoCollection prerequisite : transitiveInfoCollections) {
C prerequisiteProvider = prerequisite.getProvider(classType);
if (prerequisiteProvider != null) {
result.put(prerequisite.getConfiguration(), prerequisiteProvider);
}
}
return result.build();
}
/**
* For a given attribute, returns all {@link TransitiveInfoCollection}s provided by targets
* of that attribute. Each {@link TransitiveInfoCollection} is keyed by the
* {@link BuildConfiguration} under which the collection was created.
*/
public ImmutableListMultimap
getPrerequisitesByConfiguration(String attributeName, Mode mode) {
List extends TransitiveInfoCollection> transitiveInfoCollections =
getPrerequisites(attributeName, mode);
ImmutableListMultimap.Builder result =
ImmutableListMultimap.builder();
for (TransitiveInfoCollection prerequisite : transitiveInfoCollections) {
result.put(prerequisite.getConfiguration(), prerequisite);
}
return result.build();
}
/**
* Returns all the providers of the specified type that are listed under the specified attribute
* of this target in the BUILD file.
*/
public Iterable getPrerequisites(String attributeName,
Mode mode, final Class classType) {
AnalysisUtils.checkProvider(classType);
return AnalysisUtils.getProviders(getPrerequisites(attributeName, mode), classType);
}
/**
* Returns all the providers of the specified type that are listed under the specified attribute
* of this target in the BUILD file, and that contain the specified provider.
*/
public Iterable extends TransitiveInfoCollection>
getPrerequisitesIf(String attributeName, Mode mode, final Class classType) {
AnalysisUtils.checkProvider(classType);
return AnalysisUtils.filterByProvider(getPrerequisites(attributeName, mode), classType);
}
/**
* Returns the prerequisite referred to by the specified attribute. Also checks whether
* the attribute is marked as executable and that the target referred to can actually be
* executed.
*
* The {@code mode} argument must match the configuration transition specified in the
* definition of the attribute.
*
* @param attributeName the name of the attribute
* @param mode the configuration transition of the attribute
*
* @return the {@link FilesToRunProvider} interface of the prerequisite.
*/
@Nullable
public FilesToRunProvider getExecutablePrerequisite(String attributeName, Mode mode) {
Attribute ruleDefinition = getAttribute(attributeName);
if (ruleDefinition == null) {
throw new IllegalStateException(getRuleClassNameForLogging() + " attribute " + attributeName
+ " is not defined");
}
if (!ruleDefinition.isExecutable()) {
throw new IllegalStateException(getRuleClassNameForLogging() + " attribute " + attributeName
+ " is not configured to be executable");
}
TransitiveInfoCollection prerequisite = getPrerequisite(attributeName, mode);
if (prerequisite == null) {
return null;
}
FilesToRunProvider result = prerequisite.getProvider(FilesToRunProvider.class);
if (result == null || result.getExecutable() == null) {
attributeError(
attributeName, prerequisite.getLabel() + " does not refer to a valid executable target");
}
return result;
}
/** Indicates whether a string list attribute should be tokenized. */
public enum Tokenize {
YES,
NO
}
/**
* Gets an attribute of type STRING_LIST expanding Make variables, $(location) tags into the
* dependency location (see {@link LocationExpander} for details) and tokenizes the result.
*
* @param attributeName the name of the attribute to process
* @return a list of strings containing the expanded and tokenized values for the attribute
*/
public ImmutableList getTokenizedStringListAttr(String attributeName) {
return getExpandedStringListAttr(attributeName, Tokenize.YES);
}
/**
* Gets an attribute of type STRING_LIST expanding Make variables and $(location) tags,
* and optionally tokenizes the result.
*
* @param attributeName the name of the attribute to process
* @return a list of strings containing the processed values for the attribute
*/
public ImmutableList getExpandedStringListAttr(String attributeName, Tokenize tokenize) {
if (!getRule().isAttrDefined(attributeName, Type.STRING_LIST)) {
// TODO(bazel-team): This should be an error.
return ImmutableList.of();
}
List original = attributes().get(attributeName, Type.STRING_LIST);
if (original.isEmpty()) {
return ImmutableList.of();
}
List tokens = new ArrayList<>();
LocationExpander locationExpander =
new LocationExpander(this, LocationExpander.Options.ALLOW_DATA);
for (String token : original) {
expandValue(tokens, attributeName, token, locationExpander, tokenize);
}
return ImmutableList.copyOf(tokens);
}
/**
* Expands make variables in value and tokenizes the result into tokens.
*
* This methods should be called only during initialization.
*/
public void tokenizeAndExpandMakeVars(List tokens, String attributeName, String value) {
tokenizeAndExpandMakeVars(tokens, attributeName, value, null);
}
/**
* Expands make variables and $(location) tags in value and tokenizes the result into tokens.
*
* This methods should be called only during initialization.
*/
public void tokenizeAndExpandMakeVars(
List tokens,
String attributeName,
String value,
@Nullable LocationExpander locationExpander) {
expandValue(tokens, attributeName, value, locationExpander, Tokenize.YES);
}
/**
* Expands make variables and $(location) tags in value, and optionally tokenizes the result.
*
* This methods should be called only during initialization.
*/
public void expandValue(
List tokens,
String attributeName,
String value,
@Nullable LocationExpander locationExpander,
Tokenize tokenize) {
if (locationExpander != null) {
value = locationExpander.expandAttribute(attributeName, value);
}
value = expandMakeVariables(attributeName, value);
if (tokenize == Tokenize.YES) {
try {
ShellUtils.tokenize(tokens, value);
} catch (ShellUtils.TokenizationException e) {
attributeError(attributeName, e.getMessage());
}
} else {
tokens.add(value);
}
}
/**
* Return a context that maps Make variable names (string) to values (string).
*
* @return a ConfigurationMakeVariableContext.
**/
public ConfigurationMakeVariableContext getConfigurationMakeVariableContext() {
if (configurationMakeVariableContext == null) {
configurationMakeVariableContext = new ConfigurationMakeVariableContext(
getRule().getPackage(), getConfiguration());
}
return configurationMakeVariableContext;
}
/**
* Returns the string "expression" after expanding all embedded references to
* "Make" variables. If any errors are encountered, they are reported, and
* "expression" is returned unchanged.
*
* @param attributeName the name of the attribute from which "expression" comes;
* used for error reporting.
* @param expression the string to expand.
* @return the expansion of "expression".
*/
public String expandMakeVariables(String attributeName, String expression) {
return expandMakeVariables(attributeName, expression, getConfigurationMakeVariableContext());
}
/**
* Returns the string "expression" after expanding all embedded references to
* "Make" variables. If any errors are encountered, they are reported, and
* "expression" is returned unchanged.
*
* @param attributeName the name of the attribute from which "expression" comes;
* used for error reporting.
* @param expression the string to expand.
* @param context the ConfigurationMakeVariableContext which can have a customized
* lookupMakeVariable(String) method.
* @return the expansion of "expression".
*/
public String expandMakeVariables(String attributeName, String expression,
ConfigurationMakeVariableContext context) {
try {
return MakeVariableExpander.expand(expression, context);
} catch (MakeVariableExpander.ExpansionException e) {
attributeError(attributeName, e.getMessage());
return expression;
}
}
/**
* Gets the value of the STRING_LIST attribute expanding all make variables.
*/
public List expandedMakeVariablesList(String attrName) {
List variables = new ArrayList<>();
for (String variable : attributes().get(attrName, Type.STRING_LIST)) {
variables.add(expandMakeVariables(attrName, variable));
}
return variables;
}
/**
* If the string consists of a single variable, returns the expansion of
* that variable. Otherwise, returns null. Syntax errors are reported.
*
* @param attrName the name of the attribute from which "expression" comes;
* used for error reporting.
* @param expression the string to expand.
* @return the expansion of "expression", or null.
*/
public String expandSingleMakeVariable(String attrName, String expression) {
try {
return MakeVariableExpander.expandSingleVariable(expression,
new ConfigurationMakeVariableContext(getRule().getPackage(), getConfiguration()));
} catch (MakeVariableExpander.ExpansionException e) {
attributeError(attrName, e.getMessage());
return expression;
}
}
private void checkAttribute(String attributeName, Mode mode) {
Attribute attributeDefinition = getAttribute(attributeName);
if (attributeDefinition == null) {
throw new IllegalStateException(getRule().getLocation() + ": " + getRuleClassNameForLogging()
+ " attribute " + attributeName + " is not defined");
}
if (!(attributeDefinition.getType() == BuildType.LABEL
|| attributeDefinition.getType() == BuildType.LABEL_LIST)) {
throw new IllegalStateException(getRuleClassNameForLogging() + " attribute " + attributeName
+ " is not a label type attribute");
}
if (mode == Mode.HOST) {
if (attributeDefinition.getConfigurationTransition() != ConfigurationTransition.HOST) {
throw new IllegalStateException(getRule().getLocation() + ": "
+ getRuleClassNameForLogging() + " attribute " + attributeName
+ " is not configured for the host configuration");
}
} else if (mode == Mode.TARGET) {
if (attributeDefinition.getConfigurationTransition() != ConfigurationTransition.NONE) {
throw new IllegalStateException(getRule().getLocation() + ": "
+ getRuleClassNameForLogging() + " attribute " + attributeName
+ " is not configured for the target configuration");
}
} else if (mode == Mode.DATA) {
if (attributeDefinition.getConfigurationTransition() != ConfigurationTransition.DATA) {
throw new IllegalStateException(getRule().getLocation() + ": "
+ getRuleClassNameForLogging() + " attribute " + attributeName
+ " is not configured for the data configuration");
}
} else if (mode == Mode.SPLIT) {
if (!(attributeDefinition.hasSplitConfigurationTransition())) {
throw new IllegalStateException(getRule().getLocation() + ": "
+ getRuleClassNameForLogging() + " attribute " + attributeName
+ " is not configured for a split transition");
}
}
}
/**
* Returns the Mode for which the attribute is configured.
* This is intended for Skylark, where the Mode is implicitly chosen.
*/
public Mode getAttributeMode(String attributeName) {
Attribute attributeDefinition = getAttribute(attributeName);
if (attributeDefinition == null) {
throw new IllegalStateException(getRule().getLocation() + ": " + getRuleClassNameForLogging()
+ " attribute " + attributeName + " is not defined");
}
if (!(attributeDefinition.getType() == BuildType.LABEL
|| attributeDefinition.getType() == BuildType.LABEL_LIST)) {
throw new IllegalStateException(getRuleClassNameForLogging() + " attribute " + attributeName
+ " is not a label type attribute");
}
if (attributeDefinition.getConfigurationTransition() == ConfigurationTransition.HOST) {
return Mode.HOST;
} else if (attributeDefinition.getConfigurationTransition() == ConfigurationTransition.NONE) {
return Mode.TARGET;
} else if (attributeDefinition.getConfigurationTransition() == ConfigurationTransition.DATA) {
return Mode.DATA;
} else if (attributeDefinition.hasSplitConfigurationTransition()) {
return Mode.SPLIT;
}
throw new IllegalStateException(getRule().getLocation() + ": "
+ getRuleClassNameForLogging() + " attribute " + attributeName + " is not configured");
}
/**
* For the specified attribute "attributeName" (which must be of type
* list(label)), resolve all the labels into ConfiguredTargets (for the
* configuration appropriate to the attribute) and return their build
* artifacts as a {@link PrerequisiteArtifacts} instance.
*
* @param attributeName the name of the attribute to traverse
*/
public PrerequisiteArtifacts getPrerequisiteArtifacts(String attributeName, Mode mode) {
return PrerequisiteArtifacts.get(this, attributeName, mode);
}
/**
* For the specified attribute "attributeName" (which must be of type label),
* resolves the ConfiguredTarget and returns its single build artifact.
*
* If the attribute is optional, has no default and was not specified, then
* null will be returned. Note also that null is returned (and an attribute
* error is raised) if there wasn't exactly one build artifact for the target.
*/
public Artifact getPrerequisiteArtifact(String attributeName, Mode mode) {
TransitiveInfoCollection target = getPrerequisite(attributeName, mode);
return transitiveInfoCollectionToArtifact(attributeName, target);
}
/**
* Equivalent to getPrerequisiteArtifact(), but also asserts that
* host-configuration is appropriate for the specified attribute.
*/
public Artifact getHostPrerequisiteArtifact(String attributeName) {
TransitiveInfoCollection target = getPrerequisite(attributeName, Mode.HOST);
return transitiveInfoCollectionToArtifact(attributeName, target);
}
private Artifact transitiveInfoCollectionToArtifact(
String attributeName, TransitiveInfoCollection target) {
if (target != null) {
Iterable artifacts = target.getProvider(FileProvider.class).getFilesToBuild();
if (Iterables.size(artifacts) == 1) {
return Iterables.getOnlyElement(artifacts);
} else {
attributeError(attributeName, target.getLabel() + " expected a single artifact");
}
}
return null;
}
/**
* Returns the sole file in the "srcs" attribute. Reports an error and
* (possibly) returns null if "srcs" does not identify a single file of the
* expected type.
*/
public Artifact getSingleSource(String fileTypeName) {
List srcs = PrerequisiteArtifacts.get(this, "srcs", Mode.TARGET).list();
switch (srcs.size()) {
case 0 : // error already issued by getSrc()
return null;
case 1 : // ok
return Iterables.getOnlyElement(srcs);
default :
attributeError("srcs", "only a single " + fileTypeName + " is allowed here");
return srcs.get(0);
}
}
public Artifact getSingleSource() {
return getSingleSource(getRuleClassNameForLogging() + " source file");
}
/**
* Returns a path fragment qualified by the rule name and unique fragment to
* disambiguate artifacts produced from the source file appearing in
* multiple rules.
*
* For example "pkg/dir/name" -> "pkg/<fragment>/rule/dir/name.
*/
public final PathFragment getUniqueDirectory(String fragment) {
return AnalysisUtils.getUniqueDirectory(getLabel(), new PathFragment(fragment));
}
/**
* Check that all targets that were specified as sources are from the same
* package as this rule. Output a warning or an error for every target that is
* imported from a different package.
*/
public void checkSrcsSamePackage(boolean onlyWarn) {
PathFragment packageName = getLabel().getPackageFragment();
for (Artifact srcItem : PrerequisiteArtifacts.get(this, "srcs", Mode.TARGET).list()) {
if (!srcItem.isSourceArtifact()) {
// In theory, we should not do this check. However, in practice, we
// have a couple of rules that do not obey the "srcs must contain
// files and only files" rule. Thus, we are stuck with this hack here :(
continue;
}
Label associatedLabel = srcItem.getOwner();
PathFragment itemPackageName = associatedLabel.getPackageFragment();
if (!itemPackageName.equals(packageName)) {
String message = "please do not import '" + associatedLabel + "' directly. "
+ "You should either move the file to this package or depend on "
+ "an appropriate rule there";
if (onlyWarn) {
attributeWarning("srcs", message);
} else {
attributeError("srcs", message);
}
}
}
}
/**
* Returns the label to which the {@code NODEP_LABEL} attribute
* {@code attrName} refers, checking that it is a valid label, and that it is
* referring to a local target. Reports a warning otherwise.
*/
public Label getLocalNodepLabelAttribute(String attrName) {
Label label = attributes().get(attrName, BuildType.NODEP_LABEL);
if (label == null) {
return null;
}
if (!getTarget().getLabel().getPackageFragment().equals(label.getPackageFragment())) {
attributeWarning(attrName, "does not reference a local rule");
}
return label;
}
/**
* Returns the implicit output artifact for a given template function. If multiple or no artifacts
* can be found as a result of the template, an exception is thrown.
*/
public Artifact getImplicitOutputArtifact(ImplicitOutputsFunction function)
throws InterruptedException {
Iterable result;
try {
result = function.getImplicitOutputs(RawAttributeMapper.of(rule));
} catch (EvalException e) {
// It's ok as long as we don't use this method from Skylark.
throw new IllegalStateException(e);
}
return getImplicitOutputArtifact(Iterables.getOnlyElement(result));
}
/**
* Only use from Skylark. Returns the implicit output artifact for a given output path.
*/
public Artifact getImplicitOutputArtifact(String path) {
return getPackageRelativeArtifact(path, getBinOrGenfilesDirectory());
}
/**
* Convenience method to return a host configured target for the "compiler"
* attribute. Allows caller to decide whether a warning should be printed if
* the "compiler" attribute is not set to the default value.
*
* @param warnIfNotDefault if true, print a warning if the value for the
* "compiler" attribute is set to something other than the default
* @return a ConfiguredTarget using the host configuration for the "compiler"
* attribute
*/
public final FilesToRunProvider getCompiler(boolean warnIfNotDefault) {
Label label = attributes().get("compiler", BuildType.LABEL);
if (warnIfNotDefault && !label.equals(getRule().getAttrDefaultValue("compiler"))) {
attributeWarning("compiler", "setting the compiler is strongly discouraged");
}
return getExecutablePrerequisite("compiler", Mode.HOST);
}
/**
* Returns the (unmodifiable, ordered) list of artifacts which are the outputs
* of this target.
*
* Each element in this list is associated with a single output, either
* declared implicitly (via setImplicitOutputsFunction()) or explicitly
* (listed in the 'outs' attribute of our rule).
*/
public final ImmutableList getOutputArtifacts() {
ImmutableList.Builder artifacts = ImmutableList.builder();
for (OutputFile out : getRule().getOutputFiles()) {
artifacts.add(createOutputArtifact(out));
}
return artifacts.build();
}
/**
* Like {@link #getOutputArtifacts()} but for a singular output item.
* Reports an error if the "out" attribute is not a singleton.
*
* @return null if the output list is empty, the artifact for the first item
* of the output list otherwise
*/
public Artifact getOutputArtifact() {
List outs = getOutputArtifacts();
if (outs.size() != 1) {
attributeError("out", "exactly one output file required");
if (outs.isEmpty()) {
return null;
}
}
return outs.get(0);
}
/**
* Returns an artifact with a given file extension. All other path components
* are the same as in {@code pathFragment}.
*/
public final Artifact getRelatedArtifact(PathFragment pathFragment, String extension) {
PathFragment file = FileSystemUtils.replaceExtension(pathFragment, extension);
return getDerivedArtifact(file, getConfiguration().getBinDirectory(rule.getRepository()));
}
/**
* Returns true if the target for this context is a test target.
*/
public boolean isTestTarget() {
return TargetUtils.isTestRule(getTarget());
}
/**
* Returns true if runfiles support should create the runfiles tree, or
* false if it should just create the manifest.
*/
public boolean shouldCreateRunfilesSymlinks() {
// TODO(bazel-team): Ideally we wouldn't need such logic, and we'd
// always use the BuildConfiguration#buildRunfiles() to determine
// whether to build the runfiles. The problem is that certain build
// steps actually consume their runfiles. These include:
// a. par files consumes the runfiles directory
// We should modify autopar to take a list of files instead.
// of the runfiles directory.
// b. host tools could potentially use data files, but currently don't
// (they're run from the execution root, not a runfiles tree).
// Currently hostConfiguration.buildRunfiles() returns true.
if (isTestTarget()) {
// Tests are only executed during testing (duh),
// and their runfiles are generated lazily on local
// execution (see LocalTestStrategy). Therefore, it
// is safe not to build their runfiles.
return getConfiguration().buildRunfiles();
} else {
return true;
}
}
/**
* @return true if {@code rule} is visible from {@code prerequisite}.
*
* This only computes the logic as implemented by the visibility system. The final decision
* whether a dependency is allowed is made by
* {@link ConfiguredRuleClassProvider.PrerequisiteValidator}.
*/
public static boolean isVisible(Rule rule, TransitiveInfoCollection prerequisite) {
// Check visibility attribute
for (PackageSpecification specification :
prerequisite.getProvider(VisibilityProvider.class).getVisibility()) {
if (specification.containsPackage(rule.getLabel().getPackageIdentifier())) {
return true;
}
}
return false;
}
/**
* @return the set of features applicable for the current rule's package.
*/
public ImmutableSet getFeatures() {
return features;
}
@Override
public String toString() {
return "RuleContext(" + getLabel() + ", " + getConfiguration() + ")";
}
/**
* Builder class for a RuleContext.
*/
public static final class Builder implements RuleErrorConsumer {
private final AnalysisEnvironment env;
private final Rule rule;
private final ConfigurationFragmentPolicy configurationFragmentPolicy;
private Class extends BuildConfiguration.Fragment> universalFragment;
private final BuildConfiguration configuration;
private final BuildConfiguration hostConfiguration;
private final PrerequisiteValidator prerequisiteValidator;
@Nullable private final String aspectName;
private final ErrorReporter reporter;
private OrderedSetMultimap prerequisiteMap;
private ImmutableMap configConditions;
private NestedSet visibility;
private ImmutableMap aspectAttributes;
private ImmutableBiMap> skylarkProviderRegistry;
Builder(
AnalysisEnvironment env,
Rule rule,
@Nullable String aspectName,
BuildConfiguration configuration,
BuildConfiguration hostConfiguration,
PrerequisiteValidator prerequisiteValidator,
ConfigurationFragmentPolicy configurationFragmentPolicy) {
this.env = Preconditions.checkNotNull(env);
this.rule = Preconditions.checkNotNull(rule);
this.aspectName = aspectName;
this.configurationFragmentPolicy = Preconditions.checkNotNull(configurationFragmentPolicy);
this.configuration = Preconditions.checkNotNull(configuration);
this.hostConfiguration = Preconditions.checkNotNull(hostConfiguration);
this.prerequisiteValidator = prerequisiteValidator;
reporter = new ErrorReporter(env, rule, getRuleClassNameForLogging());
}
RuleContext build() {
Preconditions.checkNotNull(prerequisiteMap);
Preconditions.checkNotNull(configConditions);
Preconditions.checkNotNull(visibility);
AttributeMap attributes = ConfiguredAttributeMapper.of(rule, configConditions);
validateAttributes(attributes);
ListMultimap targetMap = createTargetMap();
ListMultimap filesetEntryMap =
createFilesetEntryMap(rule, configConditions);
return new RuleContext(
this,
attributes,
targetMap,
filesetEntryMap,
configConditions,
universalFragment,
getRuleClassNameForLogging(),
aspectAttributes != null ? aspectAttributes : ImmutableMap.of());
}
private void validateAttributes(AttributeMap attributes) {
rule.getRuleClassObject().checkAttributesNonEmpty(rule, reporter, attributes);
}
Builder setVisibility(NestedSet visibility) {
this.visibility = visibility;
return this;
}
/**
* Sets the prerequisites and checks their visibility. It also generates appropriate error or
* warning messages and sets the error flag as appropriate.
*/
Builder setPrerequisites(OrderedSetMultimap prerequisiteMap) {
this.prerequisiteMap = Preconditions.checkNotNull(prerequisiteMap);
return this;
}
/**
* Adds attributes which are defined by an Aspect (and not by RuleClass).
*/
Builder setAspectAttributes(Map aspectAttributes) {
this.aspectAttributes = ImmutableMap.copyOf(aspectAttributes);
return this;
}
/**
* Sets the configuration conditions needed to determine which paths to follow for this
* rule's configurable attributes.
*/
Builder setConfigConditions(ImmutableMap configConditions) {
this.configConditions = Preconditions.checkNotNull(configConditions);
return this;
}
/**
* Sets the fragment that can be legally accessed even when not explicitly declared.
*/
Builder setUniversalFragment(Class extends BuildConfiguration.Fragment> fragment) {
// TODO(bazel-team): Add this directly to ConfigurationFragmentPolicy, so we
// don't need separate logic specifically for checking this fragment. The challenge is
// that we need RuleClassProvider to figure out what this fragment is, and not every
// call state that creates ConfigurationFragmentPolicy has access to that.
this.universalFragment = fragment;
return this;
}
/**
* Sets a map that indicates which providers should be exported to skylark under the key
* (map key). These provider types will also be exportable by skylark rules under (map key).
*/
Builder setSkylarkProvidersRegistry(
ImmutableBiMap> skylarkProviderRegistry) {
this.skylarkProviderRegistry = skylarkProviderRegistry;
return this;
}
private boolean validateFilesetEntry(FilesetEntry filesetEntry, ConfiguredTarget src) {
if (src.getProvider(FilesetProvider.class) != null) {
return true;
}
if (filesetEntry.isSourceFileset()) {
return true;
}
Target srcTarget = src.getTarget();
if (!(srcTarget instanceof FileTarget)) {
attributeError("entries", String.format(
"Invalid 'srcdir' target '%s'. Must be another Fileset or package",
srcTarget.getLabel()));
return false;
}
if (srcTarget instanceof OutputFile) {
attributeWarning("entries", String.format("'srcdir' target '%s' is not an input file. "
+ "This forces the Fileset to be executed unconditionally",
srcTarget.getLabel()));
}
return true;
}
/**
* Determines and returns a map from attribute name to list of configured fileset entries, based
* on a PrerequisiteMap instance.
*/
private ListMultimap createFilesetEntryMap(
final Rule rule, ImmutableMap configConditions) {
final ImmutableSortedKeyListMultimap.Builder mapBuilder =
ImmutableSortedKeyListMultimap.builder();
for (Attribute attr : rule.getAttributes()) {
if (attr.getType() != BuildType.FILESET_ENTRY_LIST) {
continue;
}
String attributeName = attr.getName();
Map ctMap = new HashMap<>();
for (ConfiguredTarget prerequisite : prerequisiteMap.get(attr)) {
ctMap.put(AliasProvider.getDependencyLabel(prerequisite), prerequisite);
}
List entries = ConfiguredAttributeMapper.of(rule, configConditions)
.get(attributeName, BuildType.FILESET_ENTRY_LIST);
for (FilesetEntry entry : entries) {
if (entry.getFiles() == null) {
Label label = entry.getSrcLabel();
ConfiguredTarget src = ctMap.get(label);
if (!validateFilesetEntry(entry, src)) {
continue;
}
mapBuilder.put(attributeName, new ConfiguredFilesetEntry(entry, src));
} else {
ImmutableList.Builder files = ImmutableList.builder();
for (Label file : entry.getFiles()) {
files.add(ctMap.get(file));
}
mapBuilder.put(attributeName, new ConfiguredFilesetEntry(entry, files.build()));
}
}
}
return mapBuilder.build();
}
/**
* Determines and returns a map from attribute name to list of configured targets.
*/
private ImmutableSortedKeyListMultimap createTargetMap() {
ImmutableSortedKeyListMultimap.Builder mapBuilder =
ImmutableSortedKeyListMultimap.builder();
for (Map.Entry> entry :
prerequisiteMap.asMap().entrySet()) {
Attribute attribute = entry.getKey();
if (attribute == null) {
continue;
}
if (attribute.isSilentRuleClassFilter()) {
Predicate filter = attribute.getAllowedRuleClassesPredicate();
for (ConfiguredTarget configuredTarget : entry.getValue()) {
Target prerequisiteTarget = configuredTarget.getTarget();
if ((prerequisiteTarget instanceof Rule)
&& filter.apply(((Rule) prerequisiteTarget).getRuleClassObject())) {
validateDirectPrerequisite(attribute, configuredTarget);
mapBuilder.put(attribute.getName(), configuredTarget);
}
}
} else {
for (ConfiguredTarget configuredTarget : entry.getValue()) {
validateDirectPrerequisite(attribute, configuredTarget);
mapBuilder.put(attribute.getName(), configuredTarget);
}
}
}
return mapBuilder.build();
}
public void reportError(Location location, String message) {
reporter.reportError(location, message);
}
@Override
public void ruleError(String message) {
reporter.ruleError(message);
}
@Override
public void attributeError(String attrName, String message) {
reporter.attributeError(attrName, message);
}
public void reportWarning(Location location, String message) {
reporter.reportWarning(location, message);
}
@Override
public void ruleWarning(String message) {
reporter.ruleWarning(message);
}
@Override
public void attributeWarning(String attrName, String message) {
reporter.attributeWarning(attrName, message);
}
private String badPrerequisiteMessage(String targetKind, ConfiguredTarget prerequisite,
String reason, boolean isWarning) {
String msgPrefix = targetKind != null ? targetKind + " " : "";
String msgReason = reason != null ? " (" + reason + ")" : "";
if (isWarning) {
return String.format(
"%s'%s'%s is unexpected here%s; continuing anyway",
msgPrefix, prerequisite.getLabel(), AliasProvider.printVisibilityChain(prerequisite),
msgReason);
}
return String.format(
"%s'%s'%s is misplaced here%s", msgPrefix, prerequisite.getLabel(),
AliasProvider.printVisibilityChain(prerequisite), msgReason);
}
private void reportBadPrerequisite(Attribute attribute, String targetKind,
ConfiguredTarget prerequisite, String reason, boolean isWarning) {
String message = badPrerequisiteMessage(targetKind, prerequisite, reason, isWarning);
if (isWarning) {
attributeWarning(attribute.getName(), message);
} else {
attributeError(attribute.getName(), message);
}
}
private void validateDirectPrerequisiteType(ConfiguredTarget prerequisite,
Attribute attribute) {
Target prerequisiteTarget = prerequisite.getTarget();
Label prerequisiteLabel = prerequisiteTarget.getLabel();
if (prerequisiteTarget instanceof Rule) {
Rule prerequisiteRule = (Rule) prerequisiteTarget;
String reason = attribute.getValidityPredicate().checkValid(rule, prerequisiteRule);
if (reason != null) {
reportBadPrerequisite(attribute, prerequisiteTarget.getTargetKind(),
prerequisite, reason, false);
}
}
if (prerequisiteTarget instanceof Rule) {
validateRuleDependency(prerequisite, attribute);
} else if (prerequisiteTarget instanceof FileTarget) {
if (attribute.isStrictLabelCheckingEnabled()) {
if (!attribute.getAllowedFileTypesPredicate()
.apply(((FileTarget) prerequisiteTarget).getFilename())) {
if (prerequisiteTarget instanceof InputFile
&& !((InputFile) prerequisiteTarget).getPath().exists()) {
// Misplaced labels, no corresponding target exists
if (attribute.getAllowedFileTypesPredicate().isNone()
&& !((InputFile) prerequisiteTarget).getFilename().contains(".")) {
// There are no allowed files in the attribute but it's not a valid rule,
// and the filename doesn't contain a dot --> probably a misspelled rule
attributeError(attribute.getName(),
"rule '" + prerequisiteLabel + "' does not exist");
} else {
attributeError(attribute.getName(),
"target '" + prerequisiteLabel + "' does not exist");
}
} else {
// The file exists but has a bad extension
reportBadPrerequisite(attribute, "file", prerequisite,
"expected " + attribute.getAllowedFileTypesPredicate(), false);
}
}
}
}
}
public Rule getRule() {
return rule;
}
/**
* Returns a rule class name suitable for log messages, including an aspect name if applicable.
*/
public String getRuleClassNameForLogging() {
return aspectName != null
? aspectName + " aspect on " + rule.getRuleClass()
: rule.getRuleClass();
}
public BuildConfiguration getConfiguration() {
return configuration;
}
/**
* @return true if {@code rule} is visible from {@code prerequisite}.
*
* This only computes the logic as implemented by the visibility system. The final decision
* whether a dependency is allowed is made by
* {@link ConfiguredRuleClassProvider.PrerequisiteValidator}, who is supposed to call this
* method to determine whether a dependency is allowed as per visibility rules.
*/
public boolean isVisible(TransitiveInfoCollection prerequisite) {
return RuleContext.isVisible(rule, prerequisite);
}
private void validateDirectPrerequisiteFileTypes(ConfiguredTarget prerequisite,
Attribute attribute) {
if (attribute.isSkipAnalysisTimeFileTypeCheck()) {
return;
}
FileTypeSet allowedFileTypes = attribute.getAllowedFileTypesPredicate();
if (allowedFileTypes == null) {
// It's not a label or label_list attribute.
return;
}
if (allowedFileTypes == FileTypeSet.ANY_FILE && !attribute.isNonEmpty()
&& !attribute.isSingleArtifact()) {
return;
}
// If we allow any file we still need to check if there are actually files generated
// Note that this check only runs for ANY_FILE predicates if the attribute is NON_EMPTY
// or SINGLE_ARTIFACT
// If we performed this check when allowedFileTypes == NO_FILE this would
// always throw an error in those cases
if (allowedFileTypes != FileTypeSet.NO_FILE) {
Iterable artifacts = prerequisite.getProvider(FileProvider.class)
.getFilesToBuild();
if (attribute.isSingleArtifact() && Iterables.size(artifacts) != 1) {
attributeError(attribute.getName(),
"'" + prerequisite.getLabel() + "' must produce a single file");
return;
}
for (Artifact sourceArtifact : artifacts) {
if (allowedFileTypes.apply(sourceArtifact.getFilename())) {
return;
}
}
attributeError(attribute.getName(), "'" + prerequisite.getLabel()
+ "' does not produce any " + getRuleClassNameForLogging() + " " + attribute.getName()
+ " files (expected " + allowedFileTypes + ")");
}
}
private String getMissingMandatoryProviders(ConfiguredTarget prerequisite, Attribute attribute){
List> mandatoryProvidersList = attribute.getMandatoryProvidersList();
if (mandatoryProvidersList.isEmpty()) {
return null;
}
List> missingProvidersList = new ArrayList<>();
for (ImmutableSet providers : mandatoryProvidersList) {
List missing = new ArrayList<>();
for (String provider : providers) {
if (prerequisite.get(provider) == null) {
missing.add(provider);
}
}
if (missing.isEmpty()) {
return null;
} else {
missingProvidersList.add(missing);
}
}
StringBuilder missingProviders = new StringBuilder();
Joiner joinProvider = Joiner.on("', '");
for (List providers : missingProvidersList) {
if (missingProviders.length() > 0) {
missingProviders.append(" or ");
}
missingProviders.append((providers.size() > 1) ? "[" : "")
.append("'");
joinProvider.appendTo(missingProviders, providers);
missingProviders.append("'")
.append((providers.size() > 1) ? "]" : "");
}
return missingProviders.toString();
}
private String getMissingMandatoryNativeProviders(
ConfiguredTarget prerequisite, Attribute attribute) {
List>> mandatoryProvidersList =
attribute.getMandatoryNativeProvidersList();
if (mandatoryProvidersList.isEmpty()) {
return null;
}
List> missingProvidersList = new ArrayList<>();
for (ImmutableList> providers
: mandatoryProvidersList) {
List missing = new ArrayList<>();
for (Class extends TransitiveInfoProvider> provider : providers) {
if (prerequisite.getProvider(provider) == null) {
missing.add(provider.getSimpleName());
}
}
if (missing.isEmpty()) {
return null;
} else {
missingProvidersList.add(missing);
}
}
StringBuilder missingProviders = new StringBuilder();
Joiner joinProvider = Joiner.on(", ");
for (List providers : missingProvidersList) {
if (missingProviders.length() > 0) {
missingProviders.append(" or ");
}
missingProviders.append((providers.size() > 1) ? "[" : "");
joinProvider.appendTo(missingProviders, providers);
missingProviders.append((providers.size() > 1) ? "]" : "");
}
return missingProviders.toString();
}
/**
* Because some rules still have to use allowedRuleClasses to do rule dependency validation.
* A dependency is valid if it is from a rule in allowedRuledClasses, OR if all of the providers
* in mandatoryNativeProviders AND mandatoryProvidersList are provided by the target.
*/
private void validateRuleDependency(ConfiguredTarget prerequisite, Attribute attribute) {
Target prerequisiteTarget = prerequisite.getTarget();
RuleClass ruleClass = ((Rule) prerequisiteTarget).getRuleClassObject();
HashSet unfulfilledRequirements = new HashSet<>();
if (attribute.getAllowedRuleClassesPredicate() != Predicates.alwaysTrue()) {
if (attribute.getAllowedRuleClassesPredicate().apply(ruleClass)) {
return;
}
unfulfilledRequirements.add(badPrerequisiteMessage(prerequisiteTarget.getTargetKind(),
prerequisite, "expected " + attribute.getAllowedRuleClassesPredicate(), false));
}
if (attribute.getAllowedRuleClassesWarningPredicate()
!= Predicates.alwaysTrue()) {
Predicate warningPredicate = attribute.getAllowedRuleClassesWarningPredicate();
if (warningPredicate.apply(ruleClass)) {
reportBadPrerequisite(attribute, prerequisiteTarget.getTargetKind(), prerequisite,
"expected " + attribute.getAllowedRuleClassesPredicate(), true);
return;
}
}
// This condition is required; getMissingMandatory[Native]Providers() would be vacuously true
// if no providers were mandatory (thus, none are missing), which would cause an early return
// below without emitting the error message about the not-allowed rule class if that
// requirement was unfulfilled.
if (!attribute.getMandatoryNativeProvidersList().isEmpty()
|| !attribute.getMandatoryProvidersList().isEmpty()) {
boolean hadAllMandatoryProviders = true;
String missingNativeProviders = getMissingMandatoryNativeProviders(prerequisite, attribute);
if (missingNativeProviders != null) {
unfulfilledRequirements.add(
"'" + prerequisite.getLabel() + "' does not have mandatory providers: "
+ missingNativeProviders);
hadAllMandatoryProviders = false;
}
String missingProviders = getMissingMandatoryProviders(prerequisite, attribute);
if (missingProviders != null) {
unfulfilledRequirements.add(
"'" + prerequisite.getLabel() + "' does not have mandatory provider "
+ missingProviders);
hadAllMandatoryProviders = false;
}
if (hadAllMandatoryProviders) {
return;
}
}
if (!unfulfilledRequirements.isEmpty()) {
attributeError(
attribute.getName(), StringUtil.joinEnglishList(unfulfilledRequirements, "and"));
}
}
private void validateDirectPrerequisite(Attribute attribute, ConfiguredTarget prerequisite) {
validateDirectPrerequisiteType(prerequisite, attribute);
validateDirectPrerequisiteFileTypes(prerequisite, attribute);
if (attribute.performPrereqValidatorCheck()) {
prerequisiteValidator.validate(this, prerequisite, attribute);
}
}
}
/**
* Helper class for reporting errors and warnings.
*/
public static final class ErrorReporter implements RuleErrorConsumer {
private final AnalysisEnvironment env;
private final Rule rule;
private final String ruleClassNameForLogging;
ErrorReporter(AnalysisEnvironment env, Rule rule, String ruleClassNameForLogging) {
this.env = env;
this.rule = rule;
this.ruleClassNameForLogging = ruleClassNameForLogging;
}
public void reportError(Location location, String message) {
env.getEventHandler().handle(Event.error(location, message));
}
@Override
public void ruleError(String message) {
reportError(rule.getLocation(), prefixRuleMessage(message));
}
@Override
public void attributeError(String attrName, String message) {
reportError(rule.getAttributeLocation(attrName), completeAttributeMessage(attrName, message));
}
public void reportWarning(Location location, String message) {
env.getEventHandler().handle(Event.warn(location, message));
}
@Override
public void ruleWarning(String message) {
env.getEventHandler().handle(Event.warn(rule.getLocation(), prefixRuleMessage(message)));
}
@Override
public void attributeWarning(String attrName, String message) {
reportWarning(
rule.getAttributeLocation(attrName), completeAttributeMessage(attrName, message));
}
private String prefixRuleMessage(String message) {
return String.format(
"in %s rule %s: %s", getRuleClassNameForLogging(), rule.getLabel(), message);
}
private String maskInternalAttributeNames(String name) {
return Attribute.isImplicit(name) ? "(an implicit dependency)" : name;
}
/**
* Prefixes the given message with details about the rule and appends details about the macro
* that created this rule, if applicable.
*/
private String completeAttributeMessage(String attrName, String message) {
// Appends a note to the given message if the offending rule was created by a macro.
String macroMessageAppendix =
rule.wasCreatedByMacro()
? String.format(
". Since this rule was created by the macro '%s', the error might have been "
+ "caused by the macro implementation in %s",
getGeneratorFunction(), rule.getAttributeLocationWithoutMacro(attrName))
: "";
return String.format("in %s attribute of %s rule %s: %s%s",
maskInternalAttributeNames(attrName), getRuleClassNameForLogging(), rule.getLabel(),
message, macroMessageAppendix);
}
/**
* Returns a rule class name suitable for log messages, including an aspect name if applicable.
*/
private String getRuleClassNameForLogging() {
return ruleClassNameForLogging;
}
private String getGeneratorFunction() {
return (String) rule.getAttributeContainer().getAttr("generator_function");
}
}
}