// 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.rules; import com.google.common.base.Function; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.Root; import com.google.devtools.build.lib.analysis.AnalysisUtils; import com.google.devtools.build.lib.analysis.ConfigurationMakeVariableContext; import com.google.devtools.build.lib.analysis.FilesToRunProvider; import com.google.devtools.build.lib.analysis.LabelExpander; import com.google.devtools.build.lib.analysis.LabelExpander.NotUniqueExpansionException; import com.google.devtools.build.lib.analysis.MakeVariableExpander.ExpansionException; import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.analysis.config.FragmentCollection; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSet; 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.BuildType; import com.google.devtools.build.lib.packages.ImplicitOutputsFunction; import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunction; import com.google.devtools.build.lib.packages.OutputFile; import com.google.devtools.build.lib.packages.Package; import com.google.devtools.build.lib.packages.RawAttributeMapper; import com.google.devtools.build.lib.shell.ShellUtils; import com.google.devtools.build.lib.shell.ShellUtils.TokenizationException; import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable; import com.google.devtools.build.lib.skylarkinterface.SkylarkModule; import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject; import com.google.devtools.build.lib.syntax.EvalException; import com.google.devtools.build.lib.syntax.FuncallExpression.FuncallException; import com.google.devtools.build.lib.syntax.Runtime; import com.google.devtools.build.lib.syntax.SkylarkDict; import com.google.devtools.build.lib.syntax.SkylarkList; import com.google.devtools.build.lib.syntax.SkylarkList.MutableList; import com.google.devtools.build.lib.syntax.SkylarkType; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.Preconditions; 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.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * A Skylark API for the ruleContext. */ @SkylarkModule(name = "ctx", doc = "The context of the rule containing helper functions and " + "information about attributes, depending targets and outputs. " + "You get a ctx object as an argument to the implementation function when " + "you create a rule.") public final class SkylarkRuleContext { public static final String PROVIDER_CLASS_PREFIX = "com.google.devtools.build.lib."; private static final String DOC_NEW_FILE_TAIL = "Does not actually create a file on the file " + "system, just declares that some action will do so. You must create an action that " + "generates the file. If the file should be visible to other rules, declare a rule output " + "instead when possible. Doing so enables Blaze to associate a label with the file that " + "rules can refer to (allowing finer dependency control) instead of referencing the whole " + "rule."; static final LoadingCache> classCache = CacheBuilder.newBuilder() .initialCapacity(10) .maximumSize(100) .build(new CacheLoader>() { @Override public Class load(String key) throws Exception { String classPath = SkylarkRuleContext.PROVIDER_CLASS_PREFIX + key; return Class.forName(classPath); } }); public static final String EXECUTABLE_DOC = "A struct containing executable files defined in label type " + "attributes marked as executable=True. The struct fields correspond " + "to the attribute names. Each struct value is always a files or " + "None. If an optional attribute is not specified in the rule " + "then the corresponding struct value is None. If a label type is not " + "marked as executable=True, no corresponding struct field is generated."; public static final String FILES_DOC = "A struct containing files defined in label or label list " + "type attributes. The struct fields correspond to the attribute names. The struct " + "values are list of files. If an optional attribute is " + "not specified in the rule, an empty list is generated. " + "It is a shortcut for:" + "
[f for t in ctx.attr.<ATTR> for f in t.files]
"; public static final String FILE_DOC = "A struct containing files defined in label type " + "attributes marked as single_file=True. The struct fields correspond " + "to the attribute names. The struct value is always a file or " + "None. If an optional attribute is not specified in the rule " + "then the corresponding struct value is None. If a label type is not " + "marked as single_file=True, no corresponding struct field is generated. " + "It is a shortcut for:" + "
list(ctx.attr.<ATTR>.files)[0]
"; public static final String ATTR_DOC = "A struct to access the values of the attributes. The values are provided by " + "the user (if not, a default value is used)."; public static final String OUTPUTS_DOC = "A struct containing all the output files." + " The struct is generated the following way:
" + ""; public static final Function ATTRIBUTE_VALUE_EXTRACTOR_FOR_ASPECT = new Function() { @Nullable @Override public Object apply(Attribute attribute) { return attribute.getDefaultValue(null); } }; private final RuleContext ruleContext; private final FragmentCollection fragments; private final FragmentCollection hostFragments; private final SkylarkDict makeVariables; private final SkylarkRuleAttributesCollection attributesCollection; private final SkylarkRuleAttributesCollection ruleAttributesCollection; // TODO(bazel-team): we only need this because of the css_binary rule. private final ImmutableMap artifactsLabelMap; private final SkylarkClassObject outputsObject; /** * Determines whether this context is for rule implementation or for aspect implementation. */ public enum Kind { RULE, ASPECT } /** * Creates a new SkylarkRuleContext using ruleContext. * @throws InterruptedException */ public SkylarkRuleContext(RuleContext ruleContext, Kind kind) throws EvalException, InterruptedException { this.ruleContext = Preconditions.checkNotNull(ruleContext); fragments = new FragmentCollection(ruleContext, ConfigurationTransition.NONE); hostFragments = new FragmentCollection(ruleContext, ConfigurationTransition.HOST); if (kind == Kind.RULE) { Collection attributes = ruleContext.getRule().getAttributes(); HashMap outputsBuilder = new HashMap<>(); if (ruleContext.getRule().getRuleClassObject().outputsDefaultExecutable()) { addOutput(outputsBuilder, "executable", ruleContext.createOutputArtifact()); } ImplicitOutputsFunction implicitOutputsFunction = ruleContext.getRule().getRuleClassObject().getImplicitOutputsFunction(); if (implicitOutputsFunction instanceof SkylarkImplicitOutputsFunction) { SkylarkImplicitOutputsFunction func = (SkylarkImplicitOutputsFunction) ruleContext.getRule().getRuleClassObject().getImplicitOutputsFunction(); for (Map.Entry entry : func.calculateOutputs(RawAttributeMapper.of(ruleContext.getRule())).entrySet()) { addOutput( outputsBuilder, entry.getKey(), ruleContext.getImplicitOutputArtifact(entry.getValue())); } } Builder artifactLabelMapBuilder = ImmutableMap.builder(); for (Attribute a : attributes) { String attrName = a.getName(); Type type = a.getType(); if (type != BuildType.OUTPUT && type != BuildType.OUTPUT_LIST) { continue; } ImmutableList.Builder artifactsBuilder = ImmutableList.builder(); for (OutputFile outputFile : ruleContext.getRule().getOutputFileMap().get(attrName)) { Artifact artifact = ruleContext.createOutputArtifact(outputFile); artifactsBuilder.add(artifact); artifactLabelMapBuilder.put(artifact, outputFile.getLabel()); } ImmutableList artifacts = artifactsBuilder.build(); if (type == BuildType.OUTPUT) { if (artifacts.size() == 1) { addOutput(outputsBuilder, attrName, Iterables.getOnlyElement(artifacts)); } else { addOutput(outputsBuilder, attrName, Runtime.NONE); } } else if (type == BuildType.OUTPUT_LIST) { addOutput(outputsBuilder, attrName, new MutableList(artifacts)); } else { throw new IllegalArgumentException( "Type of " + attrName + "(" + type + ") is not output type "); } } this.artifactsLabelMap = artifactLabelMapBuilder.build(); this.outputsObject = new SkylarkClassObject( outputsBuilder, "No attribute '%s' in outputs. Make sure you declared a rule output with this name."); this.attributesCollection = buildAttributesCollection( attributes, ruleContext, attributeValueExtractorForRule(ruleContext)); this.ruleAttributesCollection = null; } else { // ASPECT this.artifactsLabelMap = ImmutableMap.of(); this.outputsObject = null; this.attributesCollection = buildAttributesCollection( ruleContext.getAspectAttributes().values(), ruleContext, ATTRIBUTE_VALUE_EXTRACTOR_FOR_ASPECT); this.ruleAttributesCollection = buildAttributesCollection( ruleContext.getRule().getAttributes(), ruleContext, attributeValueExtractorForRule(ruleContext)); } makeVariables = ruleContext.getConfigurationMakeVariableContext().collectMakeVariables(); } private Function attributeValueExtractorForRule( final RuleContext ruleContext) { return new Function() { @Nullable @Override public Object apply(Attribute attribute) { return ruleContext.attributes().get(attribute.getName(), attribute.getType()); } }; } private static SkylarkRuleAttributesCollection buildAttributesCollection( Collection attributes, RuleContext ruleContext, Function attributeValueExtractor) { Builder attrBuilder = new Builder<>(); Builder executableBuilder = new Builder<>(); Builder executableRunfilesbuilder = new Builder<>(); Builder fileBuilder = new Builder<>(); Builder filesBuilder = new Builder<>(); HashSet seenExecutables = new HashSet<>(); for (Attribute a : attributes) { Type type = a.getType(); Object val = attributeValueExtractor.apply(a); if (type != BuildType.LABEL && type != BuildType.LABEL_LIST) { attrBuilder.put(a.getPublicName(), val == null ? Runtime.NONE // Attribute values should be type safe : SkylarkType.convertToSkylark(val, null)); continue; } String skyname = a.getPublicName(); if (a.isExecutable()) { // In Skylark only label (not label list) type attributes can have the Executable flag. FilesToRunProvider provider = ruleContext.getExecutablePrerequisite(a.getName(), Mode.DONT_CHECK); if (provider != null && provider.getExecutable() != null) { Artifact executable = provider.getExecutable(); executableBuilder.put(skyname, executable); if (!seenExecutables.contains(executable)) { // todo(dslomov,laurentlb): In general, this is incorrect. // We associate the first encountered FilesToRunProvider with // the executable (this provider is later used to build the spawn). // However ideally we should associate a provider with the attribute name, // and pass the correct FilesToRunProvider to the spawn depending on // what attribute is used to access the executable. executableRunfilesbuilder.put(executable, provider); seenExecutables.add(executable); } } else { executableBuilder.put(skyname, Runtime.NONE); } } if (a.isSingleArtifact()) { // In Skylark only label (not label list) type attributes can have the SingleArtifact flag. Artifact artifact = ruleContext.getPrerequisiteArtifact(a.getName(), Mode.DONT_CHECK); if (artifact != null) { fileBuilder.put(skyname, artifact); } else { fileBuilder.put(skyname, Runtime.NONE); } } filesBuilder.put( skyname, ruleContext.getPrerequisiteArtifacts(a.getName(), Mode.DONT_CHECK).list()); List allPrereq = ruleContext.getPrerequisites(a.getName(), Mode.DONT_CHECK); if (type == BuildType.LABEL) { Object prereq = ruleContext.getPrerequisite(a.getName(), Mode.DONT_CHECK); if (prereq == null) { prereq = Runtime.NONE; } attrBuilder.put(skyname, prereq); } else { // Type.LABEL_LIST attrBuilder.put(skyname, new MutableList(allPrereq)); } } return new SkylarkRuleAttributesCollection( ruleContext.getRule().getRuleClass(), attrBuilder.build(), executableBuilder.build(), fileBuilder.build(), filesBuilder.build(), executableRunfilesbuilder.build()); } @SkylarkModule( name = "rule_attributes", doc = "Information about attributes of a rule an aspect is applied to." ) private static class SkylarkRuleAttributesCollection { private final SkylarkClassObject attrObject; private final SkylarkClassObject executableObject; private final SkylarkClassObject fileObject; private final SkylarkClassObject filesObject; private final ImmutableMap executableRunfilesMap; private final String ruleClassName; private SkylarkRuleAttributesCollection( String ruleClassName, ImmutableMap attrs, ImmutableMap executables, ImmutableMap singleFiles, ImmutableMap files, ImmutableMap executableRunfilesMap) { this.ruleClassName = ruleClassName; attrObject = new SkylarkClassObject( attrs, "No attribute '%s' in attr. Make sure you declared a rule attribute with this name."); executableObject = new SkylarkClassObject( executables, "No attribute '%s' in executable. Make sure there is a label type attribute marked " + "as 'executable' with this name"); fileObject = new SkylarkClassObject( singleFiles, "No attribute '%s' in file. Make sure there is a label type attribute marked " + "as 'single_file' with this name"); filesObject = new SkylarkClassObject( files, "No attribute '%s' in files. Make sure there is a label or label_list type attribute " + "with this name"); this.executableRunfilesMap = executableRunfilesMap; } @SkylarkCallable(name = "attr", structField = true, doc = ATTR_DOC) public SkylarkClassObject getAttr() { return attrObject; } @SkylarkCallable(name = "executable", structField = true, doc = EXECUTABLE_DOC) public SkylarkClassObject getExecutable() { return executableObject; } @SkylarkCallable(name = "file", structField = true, doc = FILE_DOC) public SkylarkClassObject getFile() { return fileObject; } @SkylarkCallable(name = "files", structField = true, doc = FILES_DOC) public SkylarkClassObject getFiles() { return filesObject; } @SkylarkCallable(name = "kind", structField = true, doc = "The kind of a rule, such as 'cc_library'") public String getRuleClassName() { return ruleClassName; } public ImmutableMap getExecutableRunfilesMap() { return executableRunfilesMap; } } private void addOutput(HashMap outputsBuilder, String key, Object value) throws EvalException { if (outputsBuilder.containsKey(key)) { throw new EvalException(null, "Multiple outputs with the same key: " + key); } outputsBuilder.put(key, value); } /** * Returns the original ruleContext. */ public RuleContext getRuleContext() { return ruleContext; } @SkylarkCallable(name = "attr", structField = true, doc = ATTR_DOC) public SkylarkClassObject getAttr() { return attributesCollection.getAttr(); } /** *

See {@link RuleContext#getExecutablePrerequisite(String, Mode)}. */ @SkylarkCallable(name = "executable", structField = true, doc = EXECUTABLE_DOC) public SkylarkClassObject getExecutable() { return attributesCollection.getExecutable(); } /** * See {@link RuleContext#getPrerequisiteArtifact(String, Mode)}. */ @SkylarkCallable(name = "file", structField = true, doc = FILE_DOC) public SkylarkClassObject getFile() { return attributesCollection.getFile(); } /** * See {@link RuleContext#getPrerequisiteArtifacts(String, Mode)}. */ @SkylarkCallable(name = "files", structField = true, doc = FILES_DOC) public SkylarkClassObject getFiles() { return attributesCollection.getFiles(); } @SkylarkCallable(name = "workspace_name", structField = true, doc = "Returns the workspace name as defined in the WORKSPACE file.") public String getWorkspaceName() { return ruleContext.getWorkspaceName(); } @SkylarkCallable(name = "label", structField = true, doc = "The label of this rule.") public Label getLabel() { return ruleContext.getLabel(); } @SkylarkCallable(name = "fragments", structField = true, doc = "Allows access to configuration fragments in target configuration.") public FragmentCollection getFragments() { return fragments; } @SkylarkCallable(name = "host_fragments", structField = true, doc = "Allows access to configuration fragments in host configuration.") public FragmentCollection getHostFragments() { return hostFragments; } @SkylarkCallable(name = "configuration", structField = true, doc = "Returns the default configuration. See the " + "configuration type for more details.") public BuildConfiguration getConfiguration() { return ruleContext.getConfiguration(); } @SkylarkCallable(name = "host_configuration", structField = true, doc = "Returns the host configuration. See the " + "configuration type for more details.") public BuildConfiguration getHostConfiguration() { return ruleContext.getHostConfiguration(); } @SkylarkCallable(name = "features", structField = true, doc = "Returns the set of features that are enabled for this rule." ) public ImmutableList getFeatures() { return ImmutableList.copyOf(ruleContext.getFeatures()); } @SkylarkCallable(structField = true, doc = OUTPUTS_DOC) public SkylarkClassObject outputs() throws EvalException { if (outputsObject == null) { throw new EvalException(Location.BUILTIN, "'outputs' is not defined"); } return outputsObject; } @SkylarkCallable(structField = true, doc = "Returns rule attributes descriptor for the rule that aspect is applied to." + " Only available in aspect implementation functions.") public SkylarkRuleAttributesCollection rule() throws EvalException { if (ruleAttributesCollection == null) { throw new EvalException( Location.BUILTIN, "'rule' is only available in aspect implementations"); } return ruleAttributesCollection; } @SkylarkCallable(structField = true, doc = "Dictionary (String to String) of configuration variables") public SkylarkDict var() { return makeVariables; } @Override public String toString() { return ruleContext.getLabel().toString(); } @SkylarkCallable(doc = "Splits a shell command to a list of tokens.", documented = false) public MutableList tokenize(String optionString) throws FuncallException { List options = new ArrayList<>(); try { ShellUtils.tokenize(options, optionString); } catch (TokenizationException e) { throw new FuncallException(e.getMessage() + " while tokenizing '" + optionString + "'"); } return new MutableList(options); // no env is provided, so it's effectively immutable } @SkylarkCallable( doc = "Expands all references to labels embedded within a string for all files using a mapping " + "from definition labels (i.e. the label in the output type attribute) to files. " + "Deprecated.", documented = false ) public String expand( @Nullable String expression, SkylarkList artifacts, Label labelResolver) throws EvalException, FuncallException { try { Map> labelMap = new HashMap<>(); for (Artifact artifact : artifacts.getContents(Artifact.class, "artifacts")) { labelMap.put(artifactsLabelMap.get(artifact), ImmutableList.of(artifact)); } return LabelExpander.expand(expression, labelMap, labelResolver); } catch (NotUniqueExpansionException e) { throw new FuncallException(e.getMessage() + " while expanding '" + expression + "'"); } } private boolean isForAspect() { return ruleAttributesCollection != null; } @SkylarkCallable( doc = "Creates a file object with the given filename, in the current package. " + DOC_NEW_FILE_TAIL ) public Artifact newFile(String filename) { return newFile(newFileRoot(), filename); } private Root newFileRoot() { return isForAspect() ? getConfiguration().getBinDirectory() : ruleContext.getBinOrGenfilesDirectory(); } // Kept for compatibility with old code. @SkylarkCallable(documented = false) public Artifact newFile(Root root, String filename) { return ruleContext.getPackageRelativeArtifact(filename, root); } @SkylarkCallable(doc = "Creates a new file object in the same directory as the original file. " + DOC_NEW_FILE_TAIL) public Artifact newFile(Artifact baseArtifact, String newBaseName) { PathFragment original = baseArtifact.getRootRelativePath(); PathFragment fragment = original.replaceName(newBaseName); return ruleContext.getDerivedArtifact(fragment, newFileRoot()); } // Kept for compatibility with old code. @SkylarkCallable(documented = false) public Artifact newFile(Root root, Artifact baseArtifact, String suffix) { PathFragment original = baseArtifact.getRootRelativePath(); PathFragment fragment = original.replaceName(original.getBaseName() + suffix); return ruleContext.getDerivedArtifact(fragment, root); } @SkylarkCallable(documented = false) public NestedSet middleMan(String attribute) { return AnalysisUtils.getMiddlemanFor(ruleContext, attribute); } @SkylarkCallable(documented = false) public boolean checkPlaceholders(String template, SkylarkList allowedPlaceholders) throws EvalException { List actualPlaceHolders = new LinkedList<>(); Set allowedPlaceholderSet = ImmutableSet.copyOf(allowedPlaceholders.getContents(String.class, "allowed_placeholders")); ImplicitOutputsFunction.createPlaceholderSubstitutionFormatString(template, actualPlaceHolders); for (String placeholder : actualPlaceHolders) { if (!allowedPlaceholderSet.contains(placeholder)) { return false; } } return true; } @SkylarkCallable(doc = "Returns a string after expanding all references to \"Make variables\". The variables " + "must have the following format: $(VAR_NAME). Also, $$VAR_NAME" + " expands to $VAR_NAME. Parameters:" + "
  • The name of the attribute (string). It's only used for error " + "reporting.
  • \n" + "
  • The expression to expand (string). It can contain references to " + "\"Make variables\".
  • \n" + "
  • A mapping of additional substitutions (dict of string : " + "string).
\n" + "Examples:" + "
\n"
      + "ctx.expand_make_variables(\"cmd\", \"$(MY_VAR)\", {\"MY_VAR\": \"Hi\"})  # == \"Hi\"\n"
      + "ctx.expand_make_variables(\"cmd\", \"$$PWD\", {})  # == \"$PWD\"\n"
      + "
" + "Additional variables may come from other places, such as configurations. Note that " + "this function is experimental.") public String expandMakeVariables(String attributeName, String command, final Map additionalSubstitutions) { return ruleContext.expandMakeVariables(attributeName, command, new ConfigurationMakeVariableContext(ruleContext.getRule().getPackage(), ruleContext.getConfiguration()) { @Override public String lookupMakeVariable(String name) throws ExpansionException { if (additionalSubstitutions.containsKey(name)) { return additionalSubstitutions.get(name); } else { return super.lookupMakeVariable(name); } } }); } FilesToRunProvider getExecutableRunfiles(Artifact executable) { return attributesCollection.getExecutableRunfilesMap().get(executable); } @SkylarkCallable(name = "info_file", structField = true, documented = false, doc = "Returns the file that is used to hold the non-volatile workspace status for the " + "current build request.") public Artifact getStableWorkspaceStatus() { return ruleContext.getAnalysisEnvironment().getStableWorkspaceStatusArtifact(); } @SkylarkCallable(name = "version_file", structField = true, documented = false, doc = "Returns the file that is used to hold the volatile workspace status for the " + "current build request.") public Artifact getVolatileWorkspaceStatus() { return ruleContext.getAnalysisEnvironment().getVolatileWorkspaceStatusArtifact(); } @SkylarkCallable(name = "build_file_path", structField = true, documented = true, doc = "Returns path to the BUILD file for this rule, relative to the source root" ) public String getBuildFileRelativePath() { Package pkg = ruleContext.getRule().getPackage(); Root root = Root.asSourceRoot(pkg.getSourceRoot()); return pkg.getBuildFile().getPath().relativeTo(root.getPath()).getPathString(); } }