blob: 9024f12bce73b406c6c48e4fb8ee58276003d755 [file] [log] [blame]
// Copyright 2023 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.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableCollection;
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.docgen.annot.DocCategory;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.analysis.BazelRuleAnalysisThreadContext;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.config.Fragment;
import com.google.devtools.build.lib.analysis.config.ToolchainTypeRequirement;
import com.google.devtools.build.lib.analysis.starlark.StarlarkActionFactory.StarlarkActionContext;
import com.google.devtools.build.lib.analysis.starlark.StarlarkAttrModule.Descriptor;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.packages.Attribute;
import com.google.devtools.build.lib.packages.AttributeValueSource;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.packages.StarlarkExportable;
import com.google.devtools.build.lib.starlarkbuildapi.FragmentCollectionApi;
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkActionFactoryApi;
import com.google.devtools.build.lib.starlarkbuildapi.StarlarkSubruleApi;
import com.google.devtools.build.lib.starlarkbuildapi.platform.ToolchainContextApi;
import com.google.devtools.build.lib.util.Pair;
import javax.annotation.Nullable;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.Dict;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Printer;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkCallable;
import net.starlark.java.eval.StarlarkFunction;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.Tuple;
/**
* Represents a subrule which can be invoked in a Starlark rule's implementation function.
*
* <p>The basic mechanism used is that a rule class declared a dependency on a set of subrules. The
* (implicit) attributes of the subrule are lifted to the rule class, and thus, behave as if they
* were directly declared on the rule class itself. The rule class also holds a reference to the set
* of subrules. The latter is only used for validating that a rule invoking a subrule declared that
* subrule as a dependency.
*/
public class StarlarkSubrule implements StarlarkExportable, StarlarkCallable, StarlarkSubruleApi {
// TODO(hvd) this class is a WIP, will be implemented over many commits
private final StarlarkFunction implementation;
private final ImmutableSet<ToolchainTypeRequirement> toolchains;
private final ImmutableSet<String> fragments;
private final ImmutableSet<StarlarkSubrule> subrules;
// following fields are set on export
@Nullable private String exportedName = null;
private ImmutableList<SubruleAttribute> attributes;
public StarlarkSubrule(
StarlarkFunction implementation,
ImmutableMap<String, Descriptor> attributes,
ImmutableSet<ToolchainTypeRequirement> toolchains,
ImmutableSet<String> fragments,
ImmutableSet<StarlarkSubrule> subrules) {
this.implementation = implementation;
this.attributes = SubruleAttribute.from(attributes);
this.toolchains = toolchains;
this.fragments = fragments;
this.subrules = subrules;
}
@Override
public String getName() {
if (isExported()) {
return exportedName;
} else {
return "unexported subrule";
}
}
@Override
public void repr(Printer printer) {
printer.append("<subrule ").append(getName()).append(">");
}
@Override
public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwargs)
throws EvalException, InterruptedException {
checkExported();
StarlarkRuleContext ruleContext =
BazelRuleAnalysisThreadContext.fromOrFail(thread, getName())
.getRuleContext()
.getStarlarkRuleContext();
SubruleContext callerSubruleContext = ruleContext.getLockedForSubrule();
if (callerSubruleContext != null) {
if (!callerSubruleContext.subrule.getDeclaredSubrules().contains(this)) {
throw Starlark.errorf(
"subrule %s must declare %s in 'subrules'",
callerSubruleContext.subrule.getName(), getName());
}
} else if (!ruleContext.getSubrules().contains(this)) {
throw getUndeclaredSubruleError(ruleContext);
}
ImmutableSet.Builder<FilesToRunProvider> runfilesFromDeps = ImmutableSet.builder();
ImmutableMap.Builder<String, Object> namedArgs = ImmutableMap.builder();
namedArgs.putAll(kwargs);
for (SubruleAttribute attr : attributes) {
// TODO: b/293304174 - maybe permit overriding?
if (kwargs.containsKey(attr.attrName)) {
throw Starlark.errorf(
"got invalid named argument: '%s' is an implicit dependency and cannot be overridden",
attr.attrName);
}
Attribute attribute =
ruleContext
.getRuleContext()
.getRule()
.getRuleClassObject()
.getAttributeByName(attr.ruleAttrName);
// We need to use the underlying RuleContext because the subrule attributes are hidden from
// the rule ctx.attr
Object value;
if (attribute.isExecutable()) {
FilesToRunProvider runfiles =
ruleContext.getRuleContext().getExecutablePrerequisite(attribute.getName());
runfilesFromDeps.add(runfiles);
value = runfiles;
} else if (attribute.getType() == BuildType.LABEL_LIST) {
value = ruleContext.getRuleContext().getPrerequisites(attribute.getName());
} else if (attribute.getType() == BuildType.LABEL) {
if (attribute.isSingleArtifact()) {
value = ruleContext.getRuleContext().getPrerequisiteArtifact(attribute.getName());
} else {
value = ruleContext.getRuleContext().getPrerequisite(attribute.getName());
}
} else {
// this should never happen, we've already validated the type while evaluating the subrule
throw new IllegalStateException("unexpected attribute type");
}
namedArgs.put(attr.attrName, value == null ? Starlark.NONE : value);
}
SubruleContext subruleContext =
new SubruleContext(this, ruleContext, toolchains, runfilesFromDeps.build());
ImmutableList<Object> positionals =
ImmutableList.builder().add(subruleContext).addAll(args).build();
try {
ruleContext.setLockedForSubrule(subruleContext);
return Starlark.call(
thread, implementation, positionals, Dict.immutableCopyOf(namedArgs.buildOrThrow()));
} finally {
subruleContext.nullify();
// callerSubruleContext may be null if this subrule was called from the rule itself, but in
// that case null is exactly what we want to set here
ruleContext.setLockedForSubrule(callerSubruleContext);
}
}
private ImmutableSet<StarlarkSubrule> getDeclaredSubrules() {
return subrules;
}
private EvalException getUndeclaredSubruleError(StarlarkRuleContext starlarkRuleContext) {
if (starlarkRuleContext.isForAspect()) {
return Starlark.errorf(
"aspect '%s' must declare '%s' in 'subrules'",
starlarkRuleContext.getRuleContext().getMainAspect().getAspectClass().getName(),
this.getName());
} else {
return Starlark.errorf(
"rule '%s' must declare '%s' in 'subrules'",
starlarkRuleContext.getRuleClassUnderEvaluation(), this.getName());
}
}
/**
* Returns the collection of attributes to be lifted to a rule that uses this {@code subrule}.
*
* @throws EvalException if this subrule is unexported
*/
private ImmutableList<Pair<String, Descriptor>> attributesForRule() throws EvalException {
checkExported();
ImmutableList.Builder<Pair<String, Descriptor>> builder = ImmutableList.builder();
for (SubruleAttribute attr : attributes) {
builder.add(Pair.of(attr.ruleAttrName, attr.descriptor));
}
return builder.build();
}
private void checkExported() throws EvalException {
if (!isExported()) {
throw Starlark.errorf("Invalid subrule hasn't been exported by a bzl file");
}
}
@Override
public boolean isExported() {
return this.exportedName != null;
}
@Override
public void export(EventHandler handler, Label extensionLabel, String exportedName) {
Preconditions.checkState(!isExported());
this.exportedName = exportedName;
this.attributes =
SubruleAttribute.transformOnExport(attributes, extensionLabel, exportedName, handler);
}
/**
* Returns all attributes to be lifted from the given subrules to a rule/aspect
*
* <p>Attributes are discovered transitively (if a subrule depends on another subrule) and those
* from common, transitive dependencies are de-duped.
*
* @throws EvalException if any of the given subrules are unexported
*/
static ImmutableList<Pair<String, Descriptor>> discoverAttributes(
ImmutableList<? extends StarlarkSubruleApi> subrules) throws EvalException {
ImmutableList.Builder<Pair<String, Descriptor>> attributes = ImmutableList.builder();
for (StarlarkSubrule subrule : getTransitiveSubrules(subrules)) {
attributes.addAll(subrule.attributesForRule());
}
return attributes.build();
}
/** Returns all toolchain types to be lifted from the given subrules to a rule/aspect */
static ImmutableSet<ToolchainTypeRequirement> discoverToolchains(
ImmutableList<? extends StarlarkSubruleApi> subrules) {
ImmutableSet.Builder<ToolchainTypeRequirement> toolchains = ImmutableSet.builder();
for (StarlarkSubrule subrule : getTransitiveSubrules(subrules)) {
toolchains.addAll(subrule.toolchains);
}
return toolchains.build();
}
private static ImmutableSet<StarlarkSubrule> getTransitiveSubrules(
ImmutableCollection<? extends StarlarkSubruleApi> subrules) {
ImmutableSet.Builder<StarlarkSubrule> uniqueSubrules = ImmutableSet.builder();
for (StarlarkSubruleApi subruleApi : subrules) {
if (subruleApi instanceof StarlarkSubrule) {
StarlarkSubrule subrule = (StarlarkSubrule) subruleApi;
uniqueSubrules.add(subrule).addAll(getTransitiveSubrules(subrule.getDeclaredSubrules()));
}
}
return uniqueSubrules.build();
}
/**
* The context object passed to the implementation function of a subrule.
*
* <p>This class exists to reduce the API surface visible to subrules and avoid leaking deprecated
* or legacy APIs. It wraps the underlying rule's {@link StarlarkRuleContext} and either simply
* delegates the operation to the latter, or has very similar behavior to it. Cases where behavior
* differs is documented on the respective methods.
*/
@StarlarkBuiltin(
name = "subrule_ctx",
category = DocCategory.BUILTIN,
doc = "A context object passed to the implementation function of a subrule.")
static class SubruleContext implements StarlarkActionContext {
// these fields are effectively final, set to null once this instance is no longer usable by
// Starlark
private StarlarkSubrule subrule;
private StarlarkRuleContext starlarkRuleContext;
private ImmutableSet<Label> requestedToolchains;
private ImmutableSet<FilesToRunProvider> runfilesFromDeps;
private StarlarkActionFactory actions;
private FragmentCollectionApi fragmentCollection;
private SubruleContext(
StarlarkSubrule subrule,
StarlarkRuleContext ruleContext,
ImmutableSet<ToolchainTypeRequirement> requestedToolchains,
ImmutableSet<FilesToRunProvider> runfilesFromDeps) {
this.subrule = subrule;
this.starlarkRuleContext = ruleContext;
this.requestedToolchains =
requestedToolchains.stream()
.map(ToolchainTypeRequirement::toolchainType)
.collect(toImmutableSet());
this.runfilesFromDeps = runfilesFromDeps;
this.actions = new StarlarkActionFactory(this);
this.fragmentCollection = new SubruleFragmentCollection(this);
}
@StarlarkMethod(
name = "label",
doc = "The label of the target currently being analyzed",
structField = true)
public Label getLabel() throws EvalException {
checkMutable("label");
// we use the underlying RuleContext to bypass the mutability check in
// StarlarkRuleContext.getLabel() since it's locked
return starlarkRuleContext.getRuleContext().getLabel();
}
// This is identical to the StarlarkActionFactory used by StarlarkRuleContext, and subrule
// specific behaviour is triggered by the methods inherited from StarlarkActionContext
@StarlarkMethod(
name = "actions",
doc = "Contains methods for declaring output files and the actions that produce them",
structField = true)
public StarlarkActionFactoryApi actions() throws EvalException {
checkMutable("actions");
return actions;
}
@StarlarkMethod(
name = "toolchains",
doc = "Contains methods for declaring output files and the actions that produce them",
structField = true)
public ToolchainContextApi toolchains() throws EvalException {
checkMutable("toolchains");
RuleContext ruleContext = starlarkRuleContext.getRuleContext();
if (ruleContext.getToolchainContext() == null) {
return StarlarkToolchainContext.TOOLCHAINS_NOT_VALID;
}
if (ruleContext.useAutoExecGroups()) {
return StarlarkToolchainContext.create(
/* targetDescription= */ ruleContext.getToolchainContext().targetDescription(),
/* resolveToolchainInfoFunc= */ ruleContext::getToolchainInfo,
/* resolvedToolchainTypeLabels= */ getAutomaticExecGroupLabels());
} else {
throw Starlark.errorf("subrules using toolchains must enable automatic exec-groups");
}
}
private ImmutableSet<Label> getAutomaticExecGroupLabels() {
return starlarkRuleContext.getAutomaticExecGroupLabels().stream()
.filter(label -> requestedToolchains.contains(label))
.collect(toImmutableSet());
}
@StarlarkMethod(
name = "fragments",
doc = "Allows access to configuration fragments in target configuration.",
structField = true)
public FragmentCollectionApi getFragmentCollection() throws EvalException {
checkMutable("fragments");
return fragmentCollection;
}
@Override
public ArtifactRoot newFileRoot() {
return starlarkRuleContext.getRuleContext().getBinDirectory();
}
@Override
public void checkMutable(String attrName) throws EvalException {
if (isImmutable()) {
throw Starlark.errorf(
"cannot access field or method '%s' of subrule context outside of its own"
+ " implementation function",
attrName);
}
}
@Override
public boolean isImmutable() {
return starlarkRuleContext == null || starlarkRuleContext.getLockedForSubrule() != this;
}
@Override
@Nullable
public FilesToRunProvider getExecutableRunfiles(Artifact executable, String what)
throws EvalException {
if (runfilesFromDeps.stream().anyMatch(dep -> executable.equals(dep.getExecutable()))) {
// TODO: b/293304174 - maybe return the matched FilesToRunProvider instead of failing?
throw Starlark.errorf("for '%s', expected FilesToRunProvider, got File", what);
} else {
// executable attributes of a subrule are passed to the implementation as FilesToRunProvider
// so this should never happen unless this comes from somewhere else, in which case, we
// can't resolve it anyway
return null;
}
}
@Override
public boolean areRunfilesFromDeps(FilesToRunProvider executable) {
return runfilesFromDeps.contains(executable);
}
@Override
public RuleContext getRuleContext() {
return starlarkRuleContext.getRuleContext();
}
@Override
public StarlarkSemantics getStarlarkSemantics() {
return starlarkRuleContext.getStarlarkSemantics();
}
@Override
public Object maybeOverrideExecGroup(Object execGroupUnchecked) throws EvalException {
if (execGroupUnchecked != Starlark.NONE) {
throw Starlark.errorf("'exec_group' may not be specified in subrules");
}
// TODO: b/293304174 - return the correct exec group
return execGroupUnchecked;
}
@Override
public Object maybeOverrideToolchain(Object toolchainUnchecked) throws EvalException {
if (toolchainUnchecked != Starlark.UNBOUND) {
throw Starlark.errorf("'toolchain' may not be specified in subrules");
}
return requestedToolchains.isEmpty()
? toolchainUnchecked
: Iterables.getOnlyElement(requestedToolchains);
}
// TODO: b/293304174 - maybe simplify all this by just relying on starlarkRuleContext
private void nullify() {
this.subrule = null;
this.starlarkRuleContext = null;
this.actions = null;
this.requestedToolchains = null;
this.runfilesFromDeps = null;
this.fragmentCollection = null;
}
}
private static class SubruleAttribute {
private final String attrName;
private final Descriptor descriptor;
/**
* This is the attribute name when lifted to a rule, see {@link #copyWithRuleAttributeName} and
* is set only after the subrule is exported
*/
@Nullable private final String ruleAttrName;
private SubruleAttribute(
String attrName, Descriptor descriptor, @Nullable String ruleAttrName) {
this.attrName = attrName;
this.descriptor = descriptor;
this.ruleAttrName = ruleAttrName;
}
private static ImmutableList<SubruleAttribute> from(
ImmutableMap<String, Descriptor> attributes) {
return attributes.entrySet().stream()
.map(e -> new SubruleAttribute(e.getKey(), e.getValue(), null))
.collect(toImmutableList());
}
private static ImmutableList<SubruleAttribute> transformOnExport(
ImmutableList<SubruleAttribute> attributes,
Label label,
String exportedName,
EventHandler handler) {
ImmutableList.Builder<SubruleAttribute> builder = ImmutableList.builder();
for (SubruleAttribute attribute : attributes) {
try {
builder.add(attribute.copyWithRuleAttributeName(label, exportedName));
} catch (EvalException e) {
handler.handle(Event.error(e.getMessage()));
}
}
return builder.build();
}
private SubruleAttribute copyWithRuleAttributeName(Label label, String exportedName)
throws EvalException {
String ruleAttrName =
getRuleAttrName(label, exportedName, attrName, descriptor.getValueSource());
return new SubruleAttribute(attrName, descriptor, ruleAttrName);
}
}
@VisibleForTesting
// _foo -> $//pkg:label%my_subrule%_foo
static String getRuleAttrName(
Label label, String exportedName, String attrName, AttributeValueSource valueSource)
throws EvalException {
return valueSource.convertToNativeName(
"_" + label.getCanonicalForm() + "%" + exportedName + "%" + attrName);
}
private static class SubruleFragmentCollection implements FragmentCollectionApi {
private final SubruleContext subruleContext;
private SubruleFragmentCollection(SubruleContext subruleContext) {
this.subruleContext = subruleContext;
}
@Override
@Nullable
public Object getValue(String name) throws EvalException {
Class<? extends Fragment> fragmentClass =
subruleContext.getRuleContext().getConfiguration().getStarlarkFragmentByName(name);
if (fragmentClass == null) {
return null;
}
if (!subruleContext.subrule.fragments.contains(name)) {
throw Starlark.errorf(
"%s has to declare '%s' as a required fragment in order to access it."
+ " Please update the 'fragments' argument of the subrule definition "
+ "(for example: fragments = [\"%s\"])",
subruleContext.subrule.getName(), name, name);
}
return subruleContext.getRuleContext().getConfiguration().getFragment(fragmentClass);
}
@Override
public ImmutableCollection<String> getFieldNames() {
return subruleContext.subrule.fragments;
}
@Override
public String toString() {
return "[ " + fieldsToString() + "]";
}
}
}