blob: a5c7d74cfcff24bb02f52d9a4d3011b3b4ed5961 [file] [log] [blame]
// 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.starlark;
import static com.google.devtools.build.lib.analysis.config.transitions.ConfigurationTransition.PATCH_TRANSITION_KEY;
import static com.google.devtools.build.lib.packages.RuleClass.Builder.STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
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.Iterables;
import com.google.common.collect.Ordering;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.analysis.ActionsProvider;
import com.google.devtools.build.lib.analysis.AliasProvider;
import com.google.devtools.build.lib.analysis.BashCommandConstructor;
import com.google.devtools.build.lib.analysis.CommandHelper;
import com.google.devtools.build.lib.analysis.ConfigurationMakeVariableContext;
import com.google.devtools.build.lib.analysis.DefaultInfo;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.LocationExpander;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.Runfiles.SymlinkEntry;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.ShToolchain;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
import com.google.devtools.build.lib.analysis.config.CoreOptions;
import com.google.devtools.build.lib.analysis.config.FragmentCollection;
import com.google.devtools.build.lib.analysis.platform.ConstraintValueInfo;
import com.google.devtools.build.lib.analysis.stringtemplate.ExpansionException;
import com.google.devtools.build.lib.analysis.test.InstrumentedFilesCollector;
import com.google.devtools.build.lib.analysis.test.InstrumentedFilesInfo;
import com.google.devtools.build.lib.cmdline.BazelModuleContext;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import com.google.devtools.build.lib.collect.nestedset.Depset.TypeException;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.Aspect;
import com.google.devtools.build.lib.packages.AspectDescriptor;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.Attribute.ComputedDefault;
import com.google.devtools.build.lib.packages.BuildSetting;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.ImplicitOutputsFunction;
import com.google.devtools.build.lib.packages.OutputFile;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.Provider;
import com.google.devtools.build.lib.packages.Rule;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.packages.StructImpl;
import com.google.devtools.build.lib.packages.StructProvider;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.packages.Type.LabelClass;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.shell.ShellUtils;
import com.google.devtools.build.lib.shell.ShellUtils.TokenizationException;
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkRuleContextApi;
import com.google.devtools.build.lib.starlarkbuildapi.platform.ToolchainContextApi;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import net.starlark.java.eval.Dict;
import net.starlark.java.eval.Dict.ImmutableKeyTrackingDict;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Module;
import net.starlark.java.eval.Printer;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.StarlarkValue;
import net.starlark.java.eval.Structure;
import net.starlark.java.eval.Tuple;
/**
* A Starlark API for the ruleContext.
*
* <p>"This object becomes featureless once the rule implementation function that it was created for
* has completed. To achieve this, the {@link #nullify()} should be called once the evaluation of
* the function is completed. The method both frees memory by deleting all significant fields of the
* object and makes it impossible to accidentally use this object where it's not supposed to be used
* (such attempts will result in {@link EvalException}s).
*/
public final class StarlarkRuleContext implements StarlarkRuleContextApi<ConstraintValueInfo> {
private static final ImmutableSet<PackageIdentifier> PRIVATE_STARLARKIFICATION_ALLOWLIST =
ImmutableSet.of(
PackageIdentifier.createUnchecked("_builtins", ""),
PackageIdentifier.createInMainRepo("test"), // for tests
PackageIdentifier.createInMainRepo("third_party/bazel_rules/rules_android"),
PackageIdentifier.createUnchecked("build_bazel_rules_android", ""),
PackageIdentifier.createInMainRepo("tools/build_defs/android"));
public static final String EXECUTABLE_OUTPUT_NAME = "executable";
// This field is a copy of the info from ruleContext, stored separately so it can be accessed
// after this object has been nullified.
private final String ruleLabelCanonicalName;
private final boolean isForAspect;
private final StarlarkActionFactory actionFactory;
// The fields below are intended to be final except that they can be cleared by calling
// `nullify()` when the object becomes featureless (analogous to freezing).
private RuleContext ruleContext;
private FragmentCollection fragments;
@Nullable private AspectDescriptor aspectDescriptor;
/**
* This variable is used to expose the state of {@link
* RuleContext#configurationMakeVariableContext} to the user via {@code ctx.var}.
*
* <p>Computing this field causes a side-effect of initializing the Make var context with an empty
* list of additional MakeVariableSuppliers. Historically, this was fine for Starlark-defined
* rules, but became a problem when we started giving StarlarkRuleContexts to native rules (to
* sandwich them with {@code @_builtins}, for Starlarkification). The native rules would then
* compete with this default initialization for control over the Make var context.
*
* <p>To work around this, we now compute and cache the Dict of all Make vars lazily at the first
* call to {@code ctx.var}. If a native rule provides custom MakeVariableSuppliers (via {@link
* RuleContext#initConfigurationMakeVariableContext}) and also passes {@code ctx} to a
* Starlark-defined function that accesses {@code ctx.var}, then the call to {@code
* initConfigurationMakeVariableContext} must come first or else that call will throw a
* precondition exception.
*
* <p>Note that StarlarkRuleContext can (for pathological user-written rules) survive the analysis
* phase and be accessed concurrently. Nonetheless, it is still safe to initialize {@code ctx.var}
* lazily without synchronization, because {@code ctx.var} is inaccessible once {@code nullify()}
* has been called.
*/
private Dict<String, String> cachedMakeVariables = null;
private StarlarkAttributesCollection attributesCollection;
private StarlarkAttributesCollection ruleAttributesCollection;
private StructImpl splitAttributes;
private Outputs outputsObject;
/**
* Counter for calls to {@code ctx.resolve_command} with a command longer than {@link
* CommandHelper#maxCommandLength}.
*
* <p>Such calls require generating a script. This counter ensures that each call results in
* unique script name to avoid action conflicts.
*/
private int resolveCommandScriptCounter = 0;
/**
* Creates a new StarlarkRuleContext wrapping ruleContext.
*
* <p>{@code aspectDescriptor} is the aspect for which the context is created, or <code>
* null</code> if it is for a rule.
*/
public StarlarkRuleContext(RuleContext ruleContext, @Nullable AspectDescriptor aspectDescriptor)
throws RuleErrorException {
// Init ruleContext first, we need it to obtain the StarlarkSemantics used by
// StarlarkActionFactory (and possibly others).
this.ruleContext = Preconditions.checkNotNull(ruleContext);
this.actionFactory = new StarlarkActionFactory(this);
this.ruleLabelCanonicalName = ruleContext.getLabel().getCanonicalForm();
this.fragments = new FragmentCollection(ruleContext);
this.aspectDescriptor = aspectDescriptor;
this.isForAspect = aspectDescriptor != null;
Rule rule = ruleContext.getRule();
if (aspectDescriptor == null) {
Collection<Attribute> attributes =
rule.getAttributes().stream()
.filter(attribute -> !attribute.getName().equals("aspect_hints"))
.collect(Collectors.toList());
// Populate ctx.outputs.
Outputs outputs = new Outputs(this);
// These getters do some computational work to return a view, so ensure we only do it once.
ImmutableListMultimap<String, OutputFile> explicitOutMap = rule.getExplicitOutputFileMap();
ImmutableMap<String, OutputFile> implicitOutMap = rule.getStarlarkImplicitOutputFileMap();
// Add the explicit outputs -- values of attributes of type OUTPUT or OUTPUT_LIST.
// We must iterate over the attribute definitions, and not just the entries in the
// explicitOutMap, because the latter omits empty output attributes, which must still
// generate None or [] fields in the struct.
for (Attribute a : attributes) {
// Skip non-output attrs.
String attrName = a.getName();
Type<?> type = a.getType();
if (type.getLabelClass() != LabelClass.OUTPUT) {
continue;
}
// Grab all associated outputs.
ImmutableList.Builder<Artifact> artifactsBuilder = ImmutableList.builder();
for (OutputFile outputFile : explicitOutMap.get(attrName)) {
artifactsBuilder.add(ruleContext.createOutputArtifact(outputFile));
}
StarlarkList<Artifact> artifacts = StarlarkList.immutableCopyOf(artifactsBuilder.build());
// For singular output attributes, unwrap sole element or else use None for arity mismatch.
if (type == BuildType.OUTPUT) {
if (artifacts.size() == 1) {
outputs.addOutput(attrName, Iterables.getOnlyElement(artifacts));
} else {
outputs.addOutput(attrName, Starlark.NONE);
}
} else if (type == BuildType.OUTPUT_LIST) {
outputs.addOutput(attrName, artifacts);
} else {
throw new AssertionError(
String.format("Attribute %s has unexpected output type %s", attrName, type));
}
}
// Add the implicit outputs. In the case where the rule has a native-defined implicit outputs
// function, nothing is added. Note that Rule ensures that Starlark-defined implicit output
// keys don't conflict with output attribute names.
// TODO(bazel-team): Also see about requiring the key to be a valid Starlark identifier.
for (Map.Entry<String, OutputFile> e : implicitOutMap.entrySet()) {
outputs.addOutput(e.getKey(), ruleContext.createOutputArtifact(e.getValue()));
}
this.outputsObject = outputs;
// Populate ctx.attr.
StarlarkAttributesCollection.Builder builder = StarlarkAttributesCollection.builder(this);
for (Attribute attribute : attributes) {
Object value = ruleContext.attributes().get(attribute.getName(), attribute.getType());
builder.addAttribute(attribute, value);
}
this.attributesCollection = builder.build();
this.splitAttributes = buildSplitAttributeInfo(attributes, ruleContext);
this.ruleAttributesCollection = null;
} else { // ASPECT
this.outputsObject = null;
ImmutableCollection<Attribute> attributes =
ruleContext.getMainAspect().getDefinition().getAttributes().values();
StarlarkAttributesCollection.Builder aspectBuilder =
StarlarkAttributesCollection.builder(this);
for (Attribute attribute : attributes) {
Object defaultValue = attribute.getDefaultValue(rule);
if (defaultValue instanceof ComputedDefault) {
defaultValue = ((ComputedDefault) defaultValue).getDefault(ruleContext.attributes());
}
aspectBuilder.addAttribute(attribute, defaultValue);
}
this.attributesCollection = aspectBuilder.build();
this.splitAttributes = null;
StarlarkAttributesCollection.Builder ruleBuilder = StarlarkAttributesCollection.builder(this);
for (Attribute attribute : rule.getAttributes()) {
// The aspect_hints attribute is experimental. When not enabled through the
// --enable_aspect_hints flag, we don't add it to the list of attributes that the aspect
// has access to.
if (attribute.getName().equals("aspect_hints")
&& !ruleContext
.getConfiguration()
.getOptions()
.get(CoreOptions.class)
.enableAspectHints) {
continue;
}
Object value = ruleContext.attributes().get(attribute.getName(), attribute.getType());
ruleBuilder.addAttribute(attribute, value);
}
for (Aspect aspect : ruleContext.getAspects()) {
if (aspect.equals(ruleContext.getMainAspect())) {
// Aspect's own attributes are in <code>attributesCollection</code>.
continue;
}
for (Attribute attribute : aspect.getDefinition().getAttributes().values()) {
Object defaultValue = attribute.getDefaultValue(rule);
if (defaultValue instanceof ComputedDefault) {
defaultValue = ((ComputedDefault) defaultValue).getDefault(ruleContext.attributes());
}
ruleBuilder.addAttribute(attribute, defaultValue);
}
}
this.ruleAttributesCollection = ruleBuilder.build();
}
}
/**
* Represents `ctx.outputs`.
*
* <p>The value of its {@code ctx.outputs.executable} field is computed on-demand.
*
* <p>Note: There is only one {@code Outputs} object per rule context, so default (object
* identity) equals and hashCode suffice.
*/
// TODO(adonovan): add StarlarkBuiltin(name="ctx.outputs") annotation.
private static class Outputs implements Structure, StarlarkValue {
private final Map<String, Object> outputs;
private final StarlarkRuleContext context;
private boolean executableCreated = false;
Outputs(StarlarkRuleContext context) {
this.outputs = new LinkedHashMap<>();
this.context = context;
}
private void addOutput(String key, Object value) throws RuleErrorException {
Preconditions.checkState(!context.isImmutable());
// TODO(bazel-team): We should reject outputs whose key is not an identifier. Today this is
// allowed, and the resulting ctx.outputs value can be retrieved using getattr().
if (outputs.containsKey(key)
|| (context.isExecutable() && EXECUTABLE_OUTPUT_NAME.equals(key))) {
context.getRuleContext().throwWithRuleError("Multiple outputs with the same key: " + key);
}
outputs.put(key, value);
}
@Override
public boolean isImmutable() {
return context.isImmutable();
}
@Override
public ImmutableCollection<String> getFieldNames() {
// TODO(b/175954936): There's an NPE here when accessing dir(ctx.outputs) after rule
// analysis has completed. Since we can't throw EvalException here, this may require that we
// preemptively copy the fields into this object, or at least keep a "nullified" bit so we
// know to produce an empty result here.
ImmutableList.Builder<String> result = ImmutableList.builder();
if (context.isExecutable() && executableCreated) {
result.add(EXECUTABLE_OUTPUT_NAME);
}
result.addAll(outputs.keySet());
return result.build();
}
@Nullable
@Override
public Object getValue(String name) throws EvalException {
checkMutable();
if (context.isExecutable() && EXECUTABLE_OUTPUT_NAME.equals(name)) {
executableCreated = true;
// createOutputArtifact() will cache the created artifact.
return context.getRuleContext().createOutputArtifact();
}
return outputs.get(name);
}
@Nullable
@Override
public String getErrorMessageForUnknownField(String name) {
return String.format(
"No attribute '%s' in outputs. Make sure you declared a rule output with this name.",
name);
}
@Override
public void repr(Printer printer) {
if (isImmutable()) {
printer.append("ctx.outputs(for ");
printer.append(context.ruleLabelCanonicalName);
printer.append(")");
return;
}
boolean first = true;
printer.append("ctx.outputs(");
// Sort by field name to ensure deterministic output.
try {
for (String field : Ordering.natural().sortedCopy(getFieldNames())) {
if (!first) {
printer.append(", ");
}
first = false;
printer.append(field);
printer.append(" = ");
printer.repr(getValue(field));
}
printer.append(")");
} catch (EvalException e) {
throw new AssertionError("mutable ctx.outputs should not throw", e);
}
}
private void checkMutable() throws EvalException {
if (isImmutable()) {
throw Starlark.errorf(
"cannot access outputs of rule '%s' outside of its own rule implementation function",
context.ruleLabelCanonicalName);
}
}
}
public boolean isExecutable() {
return ruleContext.getRule().getRuleClassObject().isExecutableStarlark();
}
public boolean isDefaultExecutableCreated() {
return this.outputsObject.executableCreated;
}
/**
* Nullifies fields of the object when it's not supposed to be used anymore to free unused memory
* and to make sure this object is not accessed when it's not supposed to (after the corresponding
* rule implementation function has exited).
*/
public void nullify() {
ruleContext = null;
fragments = null;
aspectDescriptor = null;
cachedMakeVariables = null;
attributesCollection = null;
ruleAttributesCollection = null;
splitAttributes = null;
outputsObject = null;
}
/** Throws an EvalException mentioning {@code attrName} if we've already been nullified. */
public void checkMutable(String attrName) throws EvalException {
if (isImmutable()) {
throw Starlark.errorf(
"cannot access field or method '%s' of rule context for '%s' outside of its own rule "
+ "implementation function",
attrName, ruleLabelCanonicalName);
}
}
@Nullable
public AspectDescriptor getAspectDescriptor() {
return aspectDescriptor;
}
public String getRuleLabelCanonicalName() {
return ruleLabelCanonicalName;
}
private static StructImpl buildSplitAttributeInfo(
Collection<Attribute> attributes, RuleContext ruleContext) {
ImmutableMap.Builder<String, Object> splitAttrInfos = ImmutableMap.builder();
for (Attribute attr : attributes) {
if (!attr.getTransitionFactory().isSplit()) {
continue;
}
Map<Optional<String>, ? extends List<? extends TransitiveInfoCollection>> splitPrereqs =
ruleContext.getSplitPrerequisites(attr.getName());
Map<Object, Object> splitPrereqsMap = new LinkedHashMap<>();
for (Map.Entry<Optional<String>, ? extends List<? extends TransitiveInfoCollection>>
splitPrereq : splitPrereqs.entrySet()) {
// Skip a split with an empty dependency list.
// TODO(jungjw): Figure out exactly which cases trigger this and see if this can be made
// more error-proof.
if (splitPrereq.getValue().isEmpty()) {
continue;
}
Object value;
if (attr.getType() == BuildType.LABEL) {
Preconditions.checkState(splitPrereq.getValue().size() == 1);
value = splitPrereq.getValue().get(0);
} else {
// BuildType.LABEL_LIST
value = StarlarkList.immutableCopyOf(splitPrereq.getValue());
}
if (splitPrereq.getKey().isPresent()
&& !splitPrereq.getKey().get().equals(PATCH_TRANSITION_KEY)) {
splitPrereqsMap.put(splitPrereq.getKey().get(), value);
} else {
// If the split transition is not in effect, then the key will be missing since there's
// nothing to key on because the dependencies aren't split and getSplitPrerequisites()
// behaves like getPrerequisites(). This also means there should be only one entry in
// the map. Use None in Starlark to represent this.
Preconditions.checkState(splitPrereqs.size() == 1);
splitPrereqsMap.put(Starlark.NONE, value);
}
}
splitAttrInfos.put(attr.getPublicName(), Dict.immutableCopyOf(splitPrereqsMap));
}
return StructProvider.STRUCT.create(
splitAttrInfos.buildOrThrow(),
"No attribute '%s' in split_attr."
+ "This attribute is not defined with a split configuration.");
}
@Override
public boolean isImmutable() {
return ruleContext == null;
}
@Override
public void repr(Printer printer) {
if (isForAspect) {
printer.append("<aspect context for " + ruleLabelCanonicalName + ">");
} else {
printer.append("<rule context for " + ruleLabelCanonicalName + ">");
}
}
/** Returns the wrapped ruleContext. */
public RuleContext getRuleContext() {
return ruleContext;
}
@Override
public Provider getDefaultProvider() {
return DefaultInfo.PROVIDER;
}
@Override
public StarlarkActionFactory actions() {
return actionFactory;
}
@Override
public StarlarkValue createdActions() throws EvalException {
checkMutable("created_actions");
if (ruleContext.getRule().getRuleClassObject().isStarlarkTestable()) {
return ActionsProvider.create(ruleContext.getAnalysisEnvironment().getRegisteredActions());
} else {
return Starlark.NONE;
}
}
@Override
public StructImpl getAttr() throws EvalException {
checkMutable("attr");
return attributesCollection.getAttr();
}
@Override
public StructImpl getSplitAttr() throws EvalException {
checkMutable("split_attr");
if (splitAttributes == null) {
throw new EvalException("'split_attr' is available only in rule implementations");
}
return splitAttributes;
}
/** See {@link RuleContext#getExecutablePrerequisite(String)}. */
@Override
public StructImpl getExecutable() throws EvalException {
checkMutable("executable");
return attributesCollection.getExecutable();
}
/** See {@link RuleContext#getPrerequisiteArtifact(String)}. */
@Override
public StructImpl getFile() throws EvalException {
checkMutable("file");
return attributesCollection.getFile();
}
/** See {@link RuleContext#getPrerequisiteArtifacts(String)}. */
@Override
public StructImpl getFiles() throws EvalException {
checkMutable("files");
return attributesCollection.getFiles();
}
@Override
public String getWorkspaceName() throws EvalException {
checkMutable("workspace_name");
return ruleContext.getWorkspaceName();
}
@Override
public Label getLabel() throws EvalException {
checkMutable("label");
return ruleContext.getLabel();
}
@Override
public FragmentCollection getFragments() throws EvalException {
checkMutable("fragments");
return fragments;
}
@Override
public BuildConfigurationValue getConfiguration() throws EvalException {
checkMutable("configuration");
return ruleContext.getConfiguration();
}
@Override
@Nullable
public Object getBuildSettingValue() throws EvalException {
if (ruleContext.getRule().getRuleClassObject().getBuildSetting() == null) {
throw Starlark.errorf(
"attempting to access 'build_setting_value' of non-build setting %s",
ruleLabelCanonicalName);
}
ImmutableMap<Label, Object> starlarkFlagSettings =
ruleContext.getConfiguration().getOptions().getStarlarkOptions();
BuildSetting buildSetting = ruleContext.getRule().getRuleClassObject().getBuildSetting();
if (starlarkFlagSettings.containsKey(ruleContext.getLabel())) {
return starlarkFlagSettings.get(ruleContext.getLabel());
} else {
Object defaultValue =
ruleContext
.attributes()
.get(STARLARK_BUILD_SETTING_DEFAULT_ATTR_NAME, buildSetting.getType());
return buildSetting.allowsMultiple() ? ImmutableList.of(defaultValue) : defaultValue;
}
}
@Override
public boolean instrumentCoverage(Object targetUnchecked) throws EvalException {
checkMutable("coverage_instrumented");
BuildConfigurationValue config = ruleContext.getConfiguration();
if (!config.isCodeCoverageEnabled()) {
return false;
}
if (targetUnchecked == Starlark.NONE) {
return InstrumentedFilesCollector.shouldIncludeLocalSources(
ruleContext.getConfiguration(), ruleContext.getLabel(), ruleContext.isTestTarget());
}
TransitiveInfoCollection target = (TransitiveInfoCollection) targetUnchecked;
return (target.get(InstrumentedFilesInfo.STARLARK_CONSTRUCTOR) != null)
&& InstrumentedFilesCollector.shouldIncludeLocalSources(config, target);
}
@Override
public ImmutableList<String> getFeatures() throws EvalException {
checkMutable("features");
return ImmutableList.copyOf(ruleContext.getFeatures());
}
@Override
public ImmutableList<String> getDisabledFeatures() throws EvalException {
checkMutable("disabled_features");
return ImmutableList.copyOf(ruleContext.getDisabledFeatures());
}
@Override
public ArtifactRoot getBinDirectory() throws EvalException {
checkMutable("bin_dir");
return getConfiguration().getBinDirectory(ruleContext.getRule().getRepository());
}
@Override
public ArtifactRoot getGenfilesDirectory() throws EvalException {
checkMutable("genfiles_dir");
return getConfiguration().getGenfilesDirectory(ruleContext.getRule().getRepository());
}
@Override
public Structure outputs() throws EvalException {
checkMutable("outputs");
if (outputsObject == null) {
throw new EvalException("'outputs' is not defined");
}
return outputsObject;
}
@Override
public StarlarkAttributesCollection rule() throws EvalException {
checkMutable("rule");
if (!isForAspect) {
throw new EvalException("'rule' is only available in aspect implementations");
}
return ruleAttributesCollection;
}
@Override
public ImmutableList<String> aspectIds() throws EvalException {
checkMutable("aspect_ids");
if (!isForAspect) {
throw new EvalException("'aspect_ids' is only available in aspect implementations");
}
ImmutableList.Builder<String> result = ImmutableList.builder();
for (AspectDescriptor descriptor : ruleContext.getAspectDescriptors()) {
result.add(descriptor.getDescription());
}
return result.build();
}
@Override
public Dict<String, String> var() throws EvalException {
checkMutable("var");
if (cachedMakeVariables == null) {
Dict.Builder<String, String> vars;
try {
vars = ruleContext.getConfigurationMakeVariableContext().collectMakeVariables();
} catch (ExpansionException e) {
throw new EvalException(e.getMessage());
}
// When tracking required fragments, use a key-tracking dict to support lookedUpVariables().
cachedMakeVariables =
ruleContext.shouldIncludeRequiredConfigFragmentsProvider()
? vars.buildImmutableWithKeyTracking()
: vars.buildImmutable();
}
return cachedMakeVariables;
}
/** Returns the set of variables accessed through {@code ctx.var}. */
public ImmutableSet<String> lookedUpVariables() {
Preconditions.checkState(ruleContext.shouldIncludeRequiredConfigFragmentsProvider(), this);
return cachedMakeVariables == null
? ImmutableSet.of()
: ((ImmutableKeyTrackingDict<String, String>) cachedMakeVariables).getAccessedKeys();
}
@Override
public ToolchainContextApi toolchains() throws EvalException {
checkMutable("toolchains");
return StarlarkToolchainContext.create(ruleContext.getToolchainContext());
}
@Override
public boolean targetPlatformHasConstraint(ConstraintValueInfo constraintValue) {
return ruleContext.targetPlatformHasConstraint(constraintValue);
}
@Override
public StarlarkExecGroupCollection execGroups() {
// Create a thin wrapper around the toolchain collection, to expose the Starlark API.
return StarlarkExecGroupCollection.create(ruleContext.getToolchainContexts());
}
@Override
public String toString() {
return ruleLabelCanonicalName;
}
@Override
public Sequence<String> tokenize(String optionString) throws EvalException {
checkMutable("tokenize");
List<String> options = new ArrayList<>();
try {
ShellUtils.tokenize(options, optionString);
} catch (TokenizationException e) {
throw Starlark.errorf("%s while tokenizing '%s'", e.getMessage(), optionString);
}
return StarlarkList.immutableCopyOf(options);
}
boolean isForAspect() {
return isForAspect;
}
@Override
public Artifact newFile(Object var1, Object var2, Object fileSuffix) throws EvalException {
checkMutable("new_file");
checkDeprecated("ctx.actions.declare_file", "ctx.new_file", getStarlarkSemantics());
// Determine which of new_file's four signatures is being used. Yes, this is terrible.
// It's one major reason that this method is deprecated.
if (fileSuffix != Starlark.UNBOUND) {
// new_file(file_root, sibling_file, suffix)
ArtifactRoot root =
assertTypeForNewFile(
var1, ArtifactRoot.class, "expected first param to be of type 'root'");
Artifact siblingFile =
assertTypeForNewFile(var2, Artifact.class, "expected second param to be of type 'File'");
PathFragment original =
siblingFile.getOutputDirRelativePath(getConfiguration().isSiblingRepositoryLayout());
PathFragment fragment = original.replaceName(original.getBaseName() + fileSuffix);
return ruleContext.getDerivedArtifact(fragment, root);
} else if (var2 == Starlark.UNBOUND) {
// new_file(filename)
String filename =
assertTypeForNewFile(var1, String.class, "expected first param to be of type 'string'");
return actionFactory.declareFile(filename, Starlark.NONE);
} else {
String filename =
assertTypeForNewFile(var2, String.class, "expected second param to be of type 'string'");
if (var1 instanceof ArtifactRoot) {
// new_file(root, filename)
ArtifactRoot root = (ArtifactRoot) var1;
return ruleContext.getPackageRelativeArtifact(filename, root);
} else {
// new_file(sibling_file, filename)
Artifact siblingFile =
assertTypeForNewFile(
var1, Artifact.class, "expected first param to be of type 'File' or 'root'");
return actionFactory.declareFile(filename, siblingFile);
}
}
}
private static <T> T assertTypeForNewFile(Object obj, Class<T> type, String errorMessage)
throws EvalException {
if (type.isInstance(obj)) {
return type.cast(obj);
} else {
throw new EvalException(errorMessage);
}
}
@Override
public boolean checkPlaceholders(String template, Sequence<?> allowedPlaceholders) // <String>
throws EvalException {
checkMutable("check_placeholders");
List<String> actualPlaceHolders = new LinkedList<>();
Set<String> allowedPlaceholderSet =
ImmutableSet.copyOf(
Sequence.cast(allowedPlaceholders, String.class, "allowed_placeholders"));
ImplicitOutputsFunction.createPlaceholderSubstitutionFormatString(template, actualPlaceHolders);
for (String placeholder : actualPlaceHolders) {
if (!allowedPlaceholderSet.contains(placeholder)) {
return false;
}
}
return true;
}
@Override
public String expandMakeVariables(
String attributeName, String command, Dict<?, ?> additionalSubstitutions) // <String, String>
throws EvalException {
checkMutable("expand_make_variables");
final Map<String, String> additionalSubstitutionsMap =
Dict.cast(additionalSubstitutions, String.class, String.class, "additional_substitutions");
return expandMakeVariables(attributeName, command, additionalSubstitutionsMap);
}
private String expandMakeVariables(
String attributeName, String command, Map<String, String> additionalSubstitutionsMap) {
ConfigurationMakeVariableContext makeVariableContext =
new ConfigurationMakeVariableContext(
ruleContext,
ruleContext.getRule().getPackage(),
ruleContext.getConfiguration(),
ImmutableList.of()) {
@Override
public String lookupVariable(String variableName) throws ExpansionException {
if (additionalSubstitutionsMap.containsKey(variableName)) {
return additionalSubstitutionsMap.get(variableName);
} else {
return super.lookupVariable(variableName);
}
}
};
return ruleContext.getExpander(makeVariableContext).expand(attributeName, command);
}
FilesToRunProvider getExecutableRunfiles(Artifact executable) {
return attributesCollection.getExecutableRunfilesMap().get(executable);
}
@Override
public Artifact getStableWorkspaceStatus() throws InterruptedException, EvalException {
checkMutable("info_file");
return ruleContext.getAnalysisEnvironment().getStableWorkspaceStatusArtifact();
}
@Override
public Artifact getVolatileWorkspaceStatus() throws InterruptedException, EvalException {
checkMutable("version_file");
return ruleContext.getAnalysisEnvironment().getVolatileWorkspaceStatusArtifact();
}
@Override
public String getBuildFileRelativePath() throws EvalException {
checkMutable("build_file_path");
Package pkg = ruleContext.getRule().getPackage();
return pkg.getSourceRoot().get().relativize(pkg.getBuildFile().getPath()).getPathString();
}
@Override
public String expandLocation(
String input, Sequence<?> targets, boolean shortPaths, StarlarkThread thread)
throws EvalException {
checkMutable("expand_location");
try {
ImmutableMap<Label, ImmutableCollection<Artifact>> labelMap =
makeLabelMap(Sequence.cast(targets, TransitiveInfoCollection.class, "targets"));
LocationExpander expander;
if (!shortPaths) {
expander = LocationExpander.withExecPaths(ruleContext, labelMap);
} else {
checkPrivateAccess(thread);
expander = LocationExpander.withRunfilesPaths(ruleContext, labelMap);
}
return expander.expand(input);
} catch (IllegalStateException ise) {
throw new EvalException(ise);
}
}
static void checkPrivateAccess(StarlarkThread thread) throws EvalException {
Label label =
((BazelModuleContext) Module.ofInnermostEnclosingStarlarkFunction(thread).getClientData())
.label();
if (PRIVATE_STARLARKIFICATION_ALLOWLIST.stream()
.noneMatch(
allowedPrefix ->
label.getRepository().equals(allowedPrefix.getRepository())
&& label.getPackageFragment().startsWith(allowedPrefix.getPackageFragment()))) {
throw Starlark.errorf("Rule in '%s' cannot use private API", label.getPackageName());
}
}
@Override
public Runfiles runfiles(
Sequence<?> files,
Object transitiveFiles,
Boolean collectData,
Boolean collectDefault,
Object symlinks,
Object rootSymlinks)
throws EvalException, TypeException {
checkMutable("runfiles");
Runfiles.Builder builder =
new Runfiles.Builder(
ruleContext.getWorkspaceName(), getConfiguration().legacyExternalRunfiles());
boolean checkConflicts = false;
if (Starlark.truth(collectData)) {
builder.addRunfiles(ruleContext, RunfilesProvider.DATA_RUNFILES);
}
if (Starlark.truth(collectDefault)) {
builder.addRunfiles(ruleContext, RunfilesProvider.DEFAULT_RUNFILES);
}
if (!files.isEmpty()) {
Sequence<Artifact> artifacts = Sequence.cast(files, Artifact.class, "files");
try {
builder.addArtifacts(artifacts);
} catch (IllegalArgumentException e) {
throw Starlark.errorf("could not add all 'files': %s", e.getMessage());
}
}
if (transitiveFiles != Starlark.NONE) {
NestedSet<Artifact> transitiveArtifacts =
Depset.cast(transitiveFiles, Artifact.class, "transitive_files");
// Runfiles uses compile order. Check that the given transitive_files depset is compatible.
if (!Order.COMPILE_ORDER.isCompatible(transitiveArtifacts.getOrder())) {
throw Starlark.errorf(
"order '%s' is invalid for transitive_files",
transitiveArtifacts.getOrder().getStarlarkName());
}
builder.addTransitiveArtifacts(transitiveArtifacts);
}
if (isDepset(symlinks)) {
builder.addSymlinks(((Depset) symlinks).getSet(SymlinkEntry.class));
} else if (isNonEmptyDict(symlinks)) {
// If Starlark code directly manipulates symlinks, activate more stringent validity checking.
checkConflicts = true;
for (Map.Entry<String, Artifact> entry :
Dict.cast(symlinks, String.class, Artifact.class, "symlinks").entrySet()) {
builder.addSymlink(PathFragment.create(entry.getKey()), entry.getValue());
}
}
if (isDepset(rootSymlinks)) {
builder.addRootSymlinks(((Depset) rootSymlinks).getSet(SymlinkEntry.class));
} else if (isNonEmptyDict(rootSymlinks)) {
checkConflicts = true;
for (Map.Entry<String, Artifact> entry :
Dict.cast(rootSymlinks, String.class, Artifact.class, "root_symlinks").entrySet()) {
builder.addRootSymlink(PathFragment.create(entry.getKey()), entry.getValue());
}
}
Runfiles runfiles = builder.build();
if (checkConflicts) {
runfiles.setConflictPolicy(Runfiles.ConflictPolicy.ERROR);
}
return runfiles;
}
private static boolean isNonEmptyDict(Object o) {
return o instanceof Dict && !((Dict<?, ?>) o).isEmpty();
}
private static boolean isDepset(Object o) {
return o instanceof Depset;
}
@Override
public Tuple resolveCommand(
String command,
Object attributeUnchecked,
Boolean expandLocations,
Object makeVariablesUnchecked,
Sequence<?> tools,
Dict<?, ?> labelDictUnchecked,
Dict<?, ?> executionRequirementsUnchecked,
StarlarkThread thread)
throws EvalException {
checkMutable("resolve_command");
Map<Label, Iterable<Artifact>> labelDict = checkLabelDict(labelDictUnchecked);
// The best way to fix this probably is to convert CommandHelper to Starlark.
CommandHelper helper =
CommandHelper.builder(ruleContext)
.addToolDependencies(Sequence.cast(tools, TransitiveInfoCollection.class, "tools"))
.addLabelMap(labelDict)
.build();
String attribute = Type.STRING.convertOptional(attributeUnchecked, "attribute");
if (expandLocations) {
command =
helper.resolveCommandAndExpandLabels(command, attribute, /*allowDataInLabel=*/ false);
}
if (!Starlark.isNullOrNone(makeVariablesUnchecked)) {
Map<String, String> makeVariables =
Type.STRING_DICT.convert(makeVariablesUnchecked, "make_variables");
command = expandMakeVariables(attribute, command, makeVariables);
}
// TODO(lberki): This flattens a NestedSet.
// However, we can't turn this into a Depset because it's an incompatible change to Starlark.
List<Artifact> inputs = new ArrayList<>(helper.getResolvedTools().toList());
ImmutableMap<String, String> executionRequirements =
ImmutableMap.copyOf(
Dict.noneableCast(
executionRequirementsUnchecked,
String.class,
String.class,
"execution_requirements"));
// TODO(b/234923262): Take exec_group into consideration instead of using the default
// exec_group.
PathFragment shExecutable =
ShToolchain.getPathForPlatform(
ruleContext.getConfiguration(), ruleContext.getExecutionPlatform());
BashCommandConstructor constructor =
CommandHelper.buildBashCommandConstructor(
executionRequirements,
shExecutable,
String.format(".resolve_command_%d.script.sh", resolveCommandScriptCounter++));
List<String> argv = helper.buildCommandLine(command, inputs, constructor);
return Tuple.triple(
StarlarkList.copyOf(thread.mutability(), inputs),
StarlarkList.copyOf(thread.mutability(), argv),
helper.getToolsRunfilesSuppliers());
}
@Override
public Tuple resolveTools(Sequence<?> tools) throws EvalException {
checkMutable("resolve_tools");
CommandHelper helper =
CommandHelper.builder(ruleContext)
.addToolDependencies(Sequence.cast(tools, TransitiveInfoCollection.class, "tools"))
.build();
return Tuple.pair(
Depset.of(Artifact.TYPE, helper.getResolvedTools()), helper.getToolsRunfilesSuppliers());
}
public StarlarkSemantics getStarlarkSemantics() {
return ruleContext.getAnalysisEnvironment().getStarlarkSemantics();
}
/**
* Ensures the given {@link Map} has keys that have {@link Label} type and values that have either
* {@link Iterable} or {@link Depset} type, and raises {@link EvalException} otherwise. Returns a
* corresponding map where any sets are replaced by iterables.
*/
// TODO(bazel-team): find a better way to typecheck this argument.
private static Map<Label, Iterable<Artifact>> checkLabelDict(Map<?, ?> labelDict)
throws EvalException {
Map<Label, Iterable<Artifact>> convertedMap = new HashMap<>();
for (Map.Entry<?, ?> entry : labelDict.entrySet()) {
Object key = entry.getKey();
if (!(key instanceof Label)) {
throw Starlark.errorf("invalid key %s in 'label_dict'", Starlark.repr(key));
}
ImmutableList.Builder<Artifact> files = ImmutableList.builder();
Object val = entry.getValue();
Iterable<?> valIter;
if (val instanceof Iterable) {
valIter = (Iterable<?>) val;
} else {
throw Starlark.errorf(
"invalid value %s in 'label_dict': expected iterable, but got '%s'",
Starlark.repr(val), Starlark.type(val));
}
for (Object file : valIter) {
if (!(file instanceof Artifact)) {
throw Starlark.errorf("invalid value %s in 'label_dict'", Starlark.repr(val));
}
files.add((Artifact) file);
}
convertedMap.put((Label) key, files.build());
}
return convertedMap;
}
private static void checkDeprecated(String newApi, String oldApi, StarlarkSemantics semantics)
throws EvalException {
if (semantics.getBool(BuildLanguageOptions.INCOMPATIBLE_NEW_ACTIONS_API)) {
throw Starlark.errorf(
"Use %s instead of %s. \n"
+ "Use --incompatible_new_actions_api=false to temporarily disable this check.",
newApi, oldApi);
}
}
/**
* Builds a map: Label -> List of files from the given labels
*
* @param knownLabels List of known labels
* @return Immutable map with immutable collections as values
*/
public static ImmutableMap<Label, ImmutableCollection<Artifact>> makeLabelMap(
Iterable<TransitiveInfoCollection> knownLabels) {
ImmutableMap.Builder<Label, ImmutableCollection<Artifact>> builder = ImmutableMap.builder();
for (TransitiveInfoCollection current : knownLabels) {
builder.put(
AliasProvider.getDependencyLabel(current),
current.getProvider(FileProvider.class).getFilesToBuild().toList());
}
return builder.buildOrThrow();
}
}