| // Copyright 2024 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.packages; |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.devtools.build.lib.packages.BuildType.NODEP_LABEL_LIST; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Lists; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.packages.TargetRecorder.MacroFrame; |
| import com.google.devtools.build.lib.packages.TargetRecorder.NameConflictException; |
| import com.google.devtools.build.lib.server.FailureDetails.PackageLoading.Code; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import java.util.ArrayList; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import javax.annotation.Nullable; |
| import net.starlark.java.eval.EvalException; |
| import net.starlark.java.eval.Mutability; |
| import net.starlark.java.eval.Starlark; |
| import net.starlark.java.eval.StarlarkFunction; |
| import net.starlark.java.eval.StarlarkSemantics; |
| import net.starlark.java.eval.StarlarkThread; |
| import net.starlark.java.eval.SymbolGenerator; |
| import net.starlark.java.spelling.SpellChecker; |
| |
| /** |
| * Represents a symbolic macro, defined in a .bzl file, that may be instantiated during Package |
| * evaluation. |
| * |
| * <p>This is analogous to {@link RuleClass}. In essence, a {@code MacroClass} consists of the |
| * macro's schema and its implementation function. |
| */ |
| public final class MacroClass { |
| |
| /** |
| * Names that users may not pass as keys of the {@code attrs} dict when calling {@code macro()}. |
| * |
| * <p>Of these, {@code name} is special cased as an actual attribute, and the rest do not exist. |
| */ |
| // Keep in sync with `macro()`'s `attrs` user documentation in StarlarkRuleFunctionsApi. |
| // But we should avoid adding new entries here, since it's a backwards-incompatible change. |
| public static final ImmutableSet<String> RESERVED_MACRO_ATTR_NAMES = |
| ImmutableSet.of("name", "visibility"); |
| |
| /** |
| * "visibility" attribute present on all symbolic macros. |
| * |
| * <p>This is similar to the visibility attribute for rules, but lacks the exec transitions. |
| */ |
| public static final Attribute VISIBILITY_ATTRIBUTE = |
| Attribute.attr("visibility", NODEP_LABEL_LIST) |
| .orderIndependent() |
| .nonconfigurable("special attribute integrated more deeply into Bazel's core logic") |
| .build(); |
| |
| private final String name; |
| private final Label definingBzlLabel; |
| private final StarlarkFunction implementation; |
| // Implicit attributes are stored under their given name ("_foo"), not a mangled name ("$foo"). |
| private final ImmutableMap<String, Attribute> attributes; |
| private final boolean isFinalizer; |
| |
| private MacroClass( |
| String name, |
| Label definingBzlLabel, |
| StarlarkFunction implementation, |
| ImmutableMap<String, Attribute> attributes, |
| boolean isFinalizer) { |
| this.name = name; |
| this.definingBzlLabel = definingBzlLabel; |
| this.implementation = implementation; |
| this.attributes = attributes; |
| this.isFinalizer = isFinalizer; |
| } |
| |
| /** Returns the macro's exported name. */ |
| public String getName() { |
| return name; |
| } |
| |
| /** Returns the label of the .bzl file where the macro was exported. */ |
| public Label getDefiningBzlLabel() { |
| return definingBzlLabel; |
| } |
| |
| public StarlarkFunction getImplementation() { |
| return implementation; |
| } |
| |
| // NB: Order is preserved from what was passed to the constructor. |
| public ImmutableMap<String, Attribute> getAttributes() { |
| return attributes; |
| } |
| |
| /** |
| * Returns whether this symbolic macro is a finalizer. All finalizers are run deferred to the end |
| * of the BUILD file's evaluation, rather than synchronously with their instantiation. |
| */ |
| public boolean isFinalizer() { |
| return isFinalizer; |
| } |
| |
| /** Builder for {@link MacroClass}. */ |
| public static final class Builder { |
| @Nullable private String name = null; |
| @Nullable private Label definingBzlLabel = null; |
| private final StarlarkFunction implementation; |
| private final ImmutableMap.Builder<String, Attribute> attributes = ImmutableMap.builder(); |
| private boolean isFinalizer = false; |
| |
| public Builder(StarlarkFunction implementation) { |
| this.implementation = implementation; |
| |
| addAttribute(RuleClass.NAME_ATTRIBUTE); |
| addAttribute(VISIBILITY_ATTRIBUTE); |
| } |
| |
| @CanIgnoreReturnValue |
| public Builder setName(String name) { |
| this.name = name; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public Builder setDefiningBzlLabel(Label label) { |
| this.definingBzlLabel = label; |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public Builder addAttribute(Attribute attribute) { |
| attributes.put(attribute.getName(), attribute); |
| return this; |
| } |
| |
| @CanIgnoreReturnValue |
| public Builder setIsFinalizer() { |
| this.isFinalizer = true; |
| return this; |
| } |
| |
| public MacroClass build() { |
| Preconditions.checkNotNull(name); |
| Preconditions.checkNotNull(definingBzlLabel); |
| return new MacroClass( |
| name, |
| definingBzlLabel, |
| implementation, |
| attributes.buildOrThrow(), |
| /* isFinalizer= */ isFinalizer); |
| } |
| } |
| |
| /** |
| * Constructs and returns a new {@link MacroInstance} associated with this {@code MacroClass}. |
| * |
| * <p>See {@link #instantiateAndAddMacro}. |
| */ |
| // TODO(#19922): Consider reporting multiple events instead of failing on the first one. See |
| // analogous implementation in RuleClass#populateDefinedRuleAttributeValues. |
| private MacroInstance instantiateMacro(Package.Builder pkgBuilder, Map<String, Object> kwargs) |
| throws EvalException { |
| // A word on edge cases: |
| // - If an attr is implicit but does not have a default specified, its value is just the |
| // default value for its attr type (e.g. `[]` for `attr.label_list()`). |
| // - If an attr is implicit but also mandatory, it's impossible to instantiate it without |
| // error. |
| // - If an attr is mandatory but also has a default, the default is meaningless. |
| // These behaviors align with rule attributes. |
| |
| LinkedHashMap<String, Object> attrValues = new LinkedHashMap<>(); |
| |
| // For each given attr value, validate that the attr exists and can be set. |
| for (Map.Entry<String, Object> entry : kwargs.entrySet()) { |
| String attrName = entry.getKey(); |
| Object value = entry.getValue(); |
| Attribute attr = attributes.get(attrName); |
| |
| // Check for unknown attr. |
| if (attr == null) { |
| throw Starlark.errorf( |
| "no such attribute '%s' in '%s' macro%s", |
| attrName, |
| name, |
| SpellChecker.didYouMean( |
| attrName, |
| attributes.values().stream() |
| .filter(Attribute::isDocumented) |
| .map(Attribute::getName) |
| .collect(toImmutableList()))); |
| } |
| |
| // Setting an attr to None is the same as omitting it (except that it's still an error to set |
| // an unknown attr to None). If the attr is optional, skip adding it to the map now but put it |
| // in below when we realize it's missing. |
| if (value == Starlark.NONE) { |
| continue; |
| } |
| |
| // Can't set implicit default. |
| // (We don't check Attribute#isImplicit() because that assumes "_" -> "$" prefix mangling.) |
| // TODO: #19922 - The lack of "_" -> "$" mangling may impact the future feature of inheriting |
| // attributes from rules. We could consider just doing the mangling for macros too so they're |
| // consistent. |
| if (attr.getName().startsWith("_")) { |
| throw Starlark.errorf("cannot set value of implicit attribute '%s'", attr.getName()); |
| } |
| |
| attrValues.put(attrName, value); |
| } |
| |
| // Special processing of the "visibility" attribute. |
| @Nullable MacroFrame parentMacroFrame = pkgBuilder.getCurrentMacroFrame(); |
| @Nullable Object rawVisibility = attrValues.get("visibility"); |
| RuleVisibility parsedVisibility; |
| if (rawVisibility == null) { |
| // Visibility wasn't explicitly supplied. If we're not in another symbolic macro, use the |
| // package's default visibility, otherwise use private visibility. |
| if (parentMacroFrame == null) { |
| parsedVisibility = pkgBuilder.getPartialPackageArgs().defaultVisibility(); |
| } else { |
| parsedVisibility = RuleVisibility.PRIVATE; |
| } |
| } else { |
| @SuppressWarnings("unchecked") |
| List<Label> liftedVisibility = |
| (List<Label>) |
| BuildType.copyAndLiftStarlarkValue( |
| name, VISIBILITY_ATTRIBUTE, rawVisibility, pkgBuilder.getLabelConverter()); |
| parsedVisibility = RuleVisibility.parse(liftedVisibility); |
| } |
| // Concatenate the visibility (as previously populated) with the instantiation site's location. |
| PackageIdentifier instantiatingLoc; |
| if (parentMacroFrame == null) { |
| instantiatingLoc = pkgBuilder.getPackageIdentifier(); |
| } else { |
| instantiatingLoc = |
| parentMacroFrame |
| .macroInstance |
| .getMacroClass() |
| .getDefiningBzlLabel() |
| .getPackageIdentifier(); |
| } |
| parsedVisibility = RuleVisibility.concatWithPackage(parsedVisibility, instantiatingLoc); |
| attrValues.put("visibility", parsedVisibility.getDeclaredLabels()); |
| |
| // Populate defaults for the rest, and validate that no mandatory attr was missed. |
| for (Attribute attr : attributes.values()) { |
| if (attrValues.containsKey(attr.getName())) { |
| continue; |
| } |
| if (attr.isMandatory()) { |
| throw Starlark.errorf( |
| "missing value for mandatory attribute '%s' in '%s' macro", attr.getName(), name); |
| } else { |
| // Already validated at schema creation time that the default is not a computed default or |
| // late-bound default |
| Object defaultValue = attr.getDefaultValueUnchecked(); |
| if (defaultValue == null) { |
| // Null values can occur for some types of attributes (e.g. LabelType). |
| defaultValue = Starlark.NONE; |
| } |
| attrValues.put(attr.getName(), defaultValue); |
| } |
| } |
| |
| // Normalize and validate all attr values. (E.g., convert strings to labels, promote |
| // configurable attribute values to select()s, fail if bool was passed instead of label, ensure |
| // values are immutable.) This applies to default values, even Nones (default value of |
| // LabelType). |
| for (Map.Entry<String, Object> entry : ImmutableMap.copyOf(attrValues).entrySet()) { |
| String attrName = entry.getKey(); |
| Object value = entry.getValue(); |
| Attribute attribute = attributes.get(attrName); |
| Object normalizedValue = |
| // copyAndLiftStarlarkValue ensures immutability. |
| BuildType.copyAndLiftStarlarkValue( |
| name, attribute, value, pkgBuilder.getLabelConverter()); |
| // TODO(#19922): Validate that LABEL_LIST type attributes don't contain duplicates, to match |
| // the behavior of rules. This probably requires factoring out logic from |
| // AggregatingAttributeMapper. |
| if (attribute.isConfigurable() && !(normalizedValue instanceof SelectorList)) { |
| normalizedValue = SelectorList.wrapSingleValue(normalizedValue); |
| } |
| attrValues.put(attrName, normalizedValue); |
| } |
| |
| // Type and existence enforced by RuleClass.NAME_ATTRIBUTE. |
| String name = (String) Preconditions.checkNotNull(attrValues.get("name")); |
| // Determine the id for this macro. If we're in another macro by the same name, increment the |
| // number, otherwise use 1 for the number. |
| int sameNameDepth = |
| parentMacroFrame == null || !name.equals(parentMacroFrame.macroInstance.getName()) |
| ? 1 |
| : parentMacroFrame.macroInstance.getSameNameDepth() + 1; |
| |
| return pkgBuilder.createMacro(this, attrValues, sameNameDepth); |
| } |
| |
| /** |
| * Constructs a new {@link MacroInstance} associated with this {@code MacroClass}, adds it to the |
| * package, and returns it. |
| * |
| * @param pkgBuilder The builder corresponding to the package in which this instance will live. |
| * @param kwargs A map from attribute name to its given Starlark value, such as passed in a BUILD |
| * file (i.e., prior to attribute type conversion, {@code select()} promotion, default value |
| * substitution, or even validation that the attribute exists). |
| */ |
| public MacroInstance instantiateAndAddMacro( |
| Package.Builder pkgBuilder, Map<String, Object> kwargs) throws EvalException { |
| MacroInstance macroInstance = instantiateMacro(pkgBuilder, kwargs); |
| try { |
| pkgBuilder.addMacro(macroInstance); |
| } catch (NameConflictException e) { |
| throw new EvalException(e); |
| } |
| return macroInstance; |
| } |
| |
| /** |
| * Executes a symbolic macro's implementation function, in a new Starlark thread, mutating the |
| * given package under construction. |
| */ |
| // TODO: #19922 - Take a new type, PackagePiece.Builder, in place of Package.Builder. PackagePiece |
| // would represent the collection of targets/macros instantiated by expanding a single symbolic |
| // macro. |
| public static void executeMacroImplementation( |
| MacroInstance macro, Package.Builder builder, StarlarkSemantics semantics) |
| throws InterruptedException { |
| // Ensure we're not expanding a (possibly indirect) recursive macro. This is morally analogous |
| // to StarlarkThread#isRecursiveCall, except in this context, recursion is through the chain of |
| // macro instantiations, which may or may not actually be concurrently executing on the stack |
| // depending on whether the evaluation is eager or deferred. |
| @Nullable String recursionMsg = getRecursionErrorMessage(macro); |
| if (recursionMsg != null) { |
| builder |
| .getLocalEventHandler() |
| .handle(Package.error(/* location= */ null, recursionMsg, Code.STARLARK_EVAL_ERROR)); |
| builder.setContainsErrors(); |
| // Don't try to evaluate this macro again. |
| builder.markMacroComplete(macro); |
| return; |
| } |
| |
| try (Mutability mu = |
| Mutability.create("macro", builder.getPackageIdentifier(), macro.getName())) { |
| StarlarkThread thread = |
| StarlarkThread.create( |
| mu, |
| semantics, |
| /* contextDescription= */ "", |
| SymbolGenerator.create( |
| MacroInstance.UniqueId.create( |
| macro.getPackage().getPackageIdentifier(), macro.getId()))); |
| thread.setPrintHandler(Event.makeDebugPrintHandler(builder.getLocalEventHandler())); |
| |
| // TODO: #19922 - Technically the embedded SymbolGenerator field should use a different key |
| // than the one in the main BUILD thread, but that'll be fixed when we change the type to |
| // PackagePiece.Builder. |
| builder.storeInThread(thread); |
| |
| // TODO: #19922 - If we want to support creating analysis_test rules inside symbolic macros, |
| // we'd need to call `thread.setThreadLocal(RuleDefinitionEnvironment.class, |
| // ruleClassProvider)`. In that case we'll need to consider how to get access to the |
| // ConfiguredRuleClassProvider. For instance, we could put it in the builder. |
| |
| MacroFrame childMacroFrame = new MacroFrame(macro); |
| @Nullable MacroFrame parentMacroFrame = builder.setCurrentMacroFrame(childMacroFrame); |
| try { |
| Starlark.call( |
| thread, |
| macro.getMacroClass().getImplementation(), |
| /* args= */ ImmutableList.of(), |
| /* kwargs= */ macro.getAttrValues()); |
| } catch (EvalException ex) { |
| builder |
| .getLocalEventHandler() |
| .handle( |
| Package.error( |
| /* location= */ null, ex.getMessageWithStack(), Code.STARLARK_EVAL_ERROR)); |
| builder.setContainsErrors(); |
| } finally { |
| // Restore the previously running symbolic macro's state (if any). |
| @Nullable MacroFrame top = builder.setCurrentMacroFrame(parentMacroFrame); |
| Preconditions.checkState(top == childMacroFrame, "inconsistent macro stack state"); |
| // Mark the macro as having completed, even if it was in error (or interrupted?). |
| builder.markMacroComplete(macro); |
| } |
| } |
| } |
| |
| /** |
| * If the instantiation of {@code macro} was recursive, i.e. if it was transitively declared by |
| * another macro instance having the same macro class, then returns an error string identifying |
| * this macro's name and a "traceback" of the instantiating macros. Otherwise, returns null. |
| */ |
| @Nullable |
| private static String getRecursionErrorMessage(MacroInstance macro) { |
| MacroInstance ancestor = macro.getParent(); |
| boolean foundRecursion = false; |
| boolean onImmediateParent = true; |
| while (ancestor != null) { |
| // TODO: #19922 - We're checking based on object identity here. If we need to worry about |
| // macro classes being serialized and deserialized in a context that also does macro |
| // evaluation, then we should use the more durable identifier of its definition label + name. |
| if (ancestor.getMacroClass() == macro.getMacroClass()) { |
| foundRecursion = true; |
| break; |
| } |
| ancestor = ancestor.getParent(); |
| onImmediateParent = false; |
| } |
| if (!foundRecursion) { |
| return null; |
| } |
| |
| StringBuilder msg = new StringBuilder(); |
| msg.append( |
| String.format( |
| "macro '%s' is %s recursive call of '%s'. Macro instantiation traceback (most" |
| + " recent call last):", |
| macro.getName(), onImmediateParent ? "a direct" : "an indirect", ancestor.getName())); |
| |
| // Materialize the stack as an ArrayList, since we want to output it in reverse order (outermost |
| // first). |
| ArrayList<MacroInstance> allAncestors = new ArrayList<>(); |
| ancestor = macro; |
| while (ancestor != null) { |
| allAncestors.add(ancestor); |
| ancestor = ancestor.getParent(); |
| } |
| for (MacroInstance item : Lists.reverse(allAncestors)) { |
| String pkg = item.getPackage().getPackageIdentifier().getCanonicalForm(); |
| String type = |
| item.getMacroClass().getDefiningBzlLabel().getCanonicalForm() |
| + "%" |
| + item.getMacroClass().getName(); |
| msg.append(String.format("\n\tPackage %s, macro '%s' of type %s", pkg, item.getName(), type)); |
| } |
| return msg.toString(); |
| } |
| } |