// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.rules;
import com.google.common.base.Preconditions;
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.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.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition;
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.RawAttributeMapper;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.shell.ShellUtils;
import com.google.devtools.build.lib.shell.ShellUtils.TokenizationException;
import com.google.devtools.build.lib.syntax.ClassObject.SkylarkClassObject;
import com.google.devtools.build.lib.syntax.Environment;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.FuncallExpression.FuncallException;
import com.google.devtools.build.lib.syntax.Label;
import com.google.devtools.build.lib.syntax.SkylarkCallable;
import com.google.devtools.build.lib.syntax.SkylarkList;
import com.google.devtools.build.lib.syntax.SkylarkModule;
import com.google.devtools.build.lib.syntax.SkylarkType;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.HashMap;
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);
}
});
private final RuleContext ruleContext;
// TODO(bazel-team): support configurable attributes.
private final SkylarkClassObject attrObject;
private final SkylarkClassObject outputsObject;
private final SkylarkClassObject executableObject;
private final SkylarkClassObject fileObject;
private final SkylarkClassObject filesObject;
// TODO(bazel-team): we only need this because of the css_binary rule.
private final ImmutableMap artifactLabelMap;
private final ImmutableMap executableRunfilesMap;
private final ImmutableMap makeVariables;
/**
* In native code, private values start with $.
* In Skylark, private values start with _, because of the grammar.
*/
private String attributeToSkylark(String oldName) {
if (!oldName.isEmpty() && (oldName.charAt(0) == '$' || oldName.charAt(0) == ':')) {
return "_" + oldName.substring(1);
}
return oldName;
}
/**
* Creates a new SkylarkRuleContext using ruleContext.
*/
public SkylarkRuleContext(RuleContext ruleContext) throws EvalException {
this.ruleContext = Preconditions.checkNotNull(ruleContext);
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()));
}
}
ImmutableMap.Builder artifactLabelMapBuilder =
ImmutableMap.builder();
for (Attribute a : ruleContext.getRule().getAttributes()) {
String attrName = a.getName();
Type> type = a.getType();
if (type != Type.OUTPUT && type != Type.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 == Type.OUTPUT) {
if (artifacts.size() == 1) {
addOutput(outputsBuilder, attrName, Iterables.getOnlyElement(artifacts));
} else {
addOutput(outputsBuilder, attrName, Environment.NONE);
}
} else if (type == Type.OUTPUT_LIST) {
addOutput(outputsBuilder, attrName,
SkylarkList.list(artifacts, Artifact.class));
} else {
throw new IllegalArgumentException(
"Type of " + attrName + "(" + type + ") is not output type ");
}
}
artifactLabelMap = artifactLabelMapBuilder.build();
outputsObject = new SkylarkClassObject(outputsBuilder, "No such output '%s'");
ImmutableMap.Builder attrBuilder = new ImmutableMap.Builder<>();
ImmutableMap.Builder executableBuilder = new ImmutableMap.Builder<>();
ImmutableMap.Builder executableRunfilesbuilder =
new ImmutableMap.Builder<>();
ImmutableMap.Builder fileBuilder = new ImmutableMap.Builder<>();
ImmutableMap.Builder filesBuilder = new ImmutableMap.Builder<>();
for (Attribute a : ruleContext.getRule().getAttributes()) {
Type> type = a.getType();
Object val = ruleContext.attributes().get(a.getName(), type);
if (type != Type.LABEL && type != Type.LABEL_LIST) {
attrBuilder.put(attributeToSkylark(a.getName()), val == null ? Environment.NONE
// Attribute values should be type safe
: SkylarkType.convertToSkylark(val, null));
continue;
}
String skyname = attributeToSkylark(a.getName());
Mode mode = getMode(a.getName());
if (a.isExecutable()) {
// In Skylark only label (not label list) type attributes can have the Executable flag.
FilesToRunProvider provider = ruleContext.getExecutablePrerequisite(a.getName(), mode);
if (provider != null && provider.getExecutable() != null) {
Artifact executable = provider.getExecutable();
executableBuilder.put(skyname, executable);
executableRunfilesbuilder.put(executable, provider);
} else {
executableBuilder.put(skyname, Environment.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);
if (artifact != null) {
fileBuilder.put(skyname, artifact);
} else {
fileBuilder.put(skyname, Environment.NONE);
}
}
filesBuilder.put(skyname, ruleContext.getPrerequisiteArtifacts(a.getName(), mode).list());
List> allPrereq = ruleContext.getPrerequisites(a.getName(), mode);
if (type == Type.LABEL) {
Object prereq = ruleContext.getPrerequisite(a.getName(), mode);
if (prereq == null) {
prereq = Environment.NONE;
}
attrBuilder.put(skyname, prereq);
} else {
// Type.LABEL_LIST
attrBuilder.put(skyname, SkylarkList.list(allPrereq, TransitiveInfoCollection.class));
}
}
attrObject = new SkylarkClassObject(attrBuilder.build(), "No such attribute '%s'");
executableObject = new SkylarkClassObject(executableBuilder.build(), "No such executable. "
+ "Make sure there is a '%s' label type attribute marked as 'executable'");
fileObject = new SkylarkClassObject(fileBuilder.build(),
"No such file. Make sure there is a '%s' label type attribute marked as 'single_file'");
filesObject = new SkylarkClassObject(filesBuilder.build(),
"No such files. Make sure there is a '%s' label or label_list type attribute");
executableRunfilesMap = executableRunfilesbuilder.build();
makeVariables = ruleContext.getConfigurationMakeVariableContext().collectMakeVariables();
}
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;
}
private Mode getMode(String attributeName) {
return ruleContext.getAttributeMode(attributeName);
}
@SkylarkCallable(name = "attr", structField = true,
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 SkylarkClassObject getAttr() {
return attrObject;
}
/**
*
See {@link RuleContext#getExecutablePrerequisite(String, Mode)}.
*/
@SkylarkCallable(name = "executable", structField = true,
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 SkylarkClassObject getExecutable() {
return executableObject;
}
/**
* See {@link RuleContext#getPrerequisiteArtifact(String, Mode)}.
*/
@SkylarkCallable(name = "file", structField = true,
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.")
public SkylarkClassObject getFile() {
return fileObject;
}
/**
* See {@link RuleContext#getPrerequisiteArtifacts(String, Mode)}.
*/
@SkylarkCallable(name = "files", structField = true,
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.")
public SkylarkClassObject getFiles() {
return filesObject;
}
@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 = "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 = "data_configuration", structField = true,
doc = "Returns the data configuration. See the "
+ "configuration type for more details.")
public BuildConfiguration getDataConfiguration() {
return ruleContext.getConfiguration().getConfiguration(ConfigurationTransition.DATA);
}
@SkylarkCallable(structField = true,
doc = "A struct containing all the output files."
+ " The struct is generated the following way: "
+ "
If the rule is marked as executable=True the struct has an "
+ "\"executable\" field with the rules default executable file value."
+ "
For every entry in the rule's outputs dict an attr is generated with "
+ "the same name and the corresponding file value."
+ "
For every output type attribute a struct attribute is generated with the "
+ "same name and the corresponding file value or None, "
+ "if no value is specified in the rule."
+ "
For every output list type attribute a struct attribute is generated with the "
+ "same name and corresponding list of files value "
+ "(an empty list if no value is specified in the rule).
")
public SkylarkClassObject outputs() {
return outputsObject;
}
@SkylarkCallable(structField = true,
doc = "Dictionary (String to String) of configuration variables")
public ImmutableMap 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 List 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 ImmutableList.copyOf(options);
}
@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,
List artifacts, Label labelResolver) throws FuncallException {
try {
Map