| // Copyright 2021 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 com.google.common.base.Function; |
| 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.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.Objects; |
| import javax.annotation.Nullable; |
| import net.starlark.java.eval.EvalException; |
| import net.starlark.java.eval.Starlark; |
| |
| /** |
| * AspectsList represents the list of aspects specified via --aspects command line option or |
| * declared in attribute aspects list. The class is responsible for wrapping the information |
| * necessary for constructing those aspects. |
| */ |
| public final class AspectsList { |
| private final ImmutableList<AspectDetails<?>> aspects; |
| |
| private AspectsList(ImmutableList<AspectDetails<?>> aspects) { |
| this.aspects = aspects; |
| } |
| |
| public boolean hasAspects() { |
| return !aspects.isEmpty(); |
| } |
| |
| /** Returns the list of aspects required for dependencies through this attribute. */ |
| public ImmutableList<Aspect> getAspects(Rule rule) { |
| if (aspects.isEmpty()) { |
| return ImmutableList.of(); |
| } |
| ImmutableList.Builder<Aspect> builder = null; |
| for (AspectDetails<?> aspect : aspects) { |
| Aspect a = aspect.getAspect(rule); |
| if (a != null) { |
| if (builder == null) { |
| builder = ImmutableList.builder(); |
| } |
| builder.add(a); |
| } |
| } |
| return builder == null ? ImmutableList.of() : builder.build(); |
| } |
| |
| public ImmutableList<AspectClass> getAspectClasses() { |
| ImmutableList.Builder<AspectClass> result = ImmutableList.builder(); |
| for (AspectDetails<?> aspect : aspects) { |
| result.add(aspect.getAspectClass()); |
| } |
| return result.build(); |
| } |
| |
| /** Returns a list of Aspect objects for top level aspects. */ |
| public ImmutableList<Aspect> buildAspects(ImmutableMap<String, String> aspectsParameters) |
| throws EvalException { |
| Preconditions.checkArgument(aspectsParameters != null, "aspectsParameters cannot be null"); |
| |
| ImmutableList.Builder<Aspect> aspectsList = ImmutableList.builder(); |
| for (AspectDetails<?> aspect : aspects) { |
| aspectsList.add(aspect.getTopLevelAspect(aspectsParameters)); |
| } |
| return aspectsList.build(); |
| } |
| |
| public void validateRulePropagatedAspectsParameters(RuleClass ruleClass) throws EvalException { |
| for (AspectDetails<?> aspect : aspects) { |
| ImmutableSet<String> requiredAspectParameters = aspect.getRequiredParameters(); |
| for (Attribute aspectAttribute : aspect.getAspectAttributes()) { |
| String aspectAttrName = aspectAttribute.getPublicName(); |
| Type<?> aspectAttrType = aspectAttribute.getType(); |
| |
| // When propagated from a rule, explicit aspect attributes must be of type boolean, int |
| // or string. Integer and string attributes must have the `values` restriction. |
| if (!aspectAttribute.isImplicit() && !aspectAttribute.isLateBound()) { |
| if (aspectAttrType != Type.BOOLEAN && !aspectAttribute.checkAllowedValues()) { |
| throw Starlark.errorf( |
| "Aspect %s: Aspect parameter attribute '%s' must use the 'values' restriction.", |
| aspect.getName(), aspectAttrName); |
| } |
| } |
| |
| // Required aspect parameters must be specified by the rule propagating the aspect with |
| // the same parameter type. |
| if (requiredAspectParameters.contains(aspectAttrName)) { |
| if (!ruleClass.hasAttr(aspectAttrName, aspectAttrType)) { |
| throw Starlark.errorf( |
| "Aspect %s requires rule %s to specify attribute '%s' with type %s.", |
| aspect.getName(), ruleClass.getName(), aspectAttrName, aspectAttrType); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Validates top-level aspects parameters and reports error in the following cases: |
| * |
| * <p>If a parameter name is specified in command line but no aspect has a parameter with that |
| * name. |
| * |
| * <p>If a mandatory aspect attribute is not given a value in the top-level parameters list. |
| */ |
| public void validateTopLevelAspectsParameters(ImmutableMap<String, String> aspectsParameters) |
| throws EvalException { |
| Preconditions.checkArgument(aspectsParameters != null, "aspectsParameters cannot be null"); |
| |
| ImmutableSet.Builder<String> usedParametersBuilder = ImmutableSet.builder(); |
| for (AspectDetails<?> aspectDetails : aspects) { |
| if (aspectDetails instanceof StarlarkAspectDetails) { |
| ImmutableList<Attribute> aspectAttributes = |
| ((StarlarkAspectDetails) aspectDetails).aspect.getAttributes(); |
| for (Attribute attr : aspectAttributes) { |
| if (attr.isImplicit() || attr.isLateBound()) { |
| continue; |
| } |
| String attrName = attr.getName(); |
| if (aspectsParameters.containsKey(attrName)) { |
| usedParametersBuilder.add(attrName); |
| } else if (attr.isMandatory()) { |
| throw Starlark.errorf( |
| "Missing mandatory attribute '%s' for aspect '%s'.", |
| attrName, aspectDetails.getName()); |
| } |
| } |
| } |
| } |
| ImmutableSet<String> usedParameters = usedParametersBuilder.build(); |
| ImmutableList<String> unusedParameters = |
| aspectsParameters.keySet().stream() |
| .filter(p -> !usedParameters.contains(p)) |
| .collect(toImmutableList()); |
| if (!unusedParameters.isEmpty()) { |
| throw Starlark.errorf( |
| "Parameters '%s' are not parameters of any of the top-level aspects but they are" |
| + " specified in --aspects_parameters.", |
| unusedParameters); |
| } |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| AspectsList aspectsList = (AspectsList) o; |
| return Objects.equals(aspects, aspectsList.aspects); |
| } |
| |
| @Override |
| public int hashCode() { |
| return aspects.hashCode(); |
| } |
| |
| /** Wraps the information necessary to construct an Aspect. */ |
| private abstract static class AspectDetails<C extends AspectClass> { |
| final C aspectClass; |
| final Function<Rule, AspectParameters> parametersExtractor; |
| final String requiredByAspect; |
| |
| private AspectDetails(C aspectClass, Function<Rule, AspectParameters> parametersExtractor) { |
| this.aspectClass = aspectClass; |
| this.parametersExtractor = parametersExtractor; |
| this.requiredByAspect = null; |
| } |
| |
| private AspectDetails( |
| C aspectClass, |
| Function<Rule, AspectParameters> parametersExtractor, |
| String requiredByAspect) { |
| this.aspectClass = aspectClass; |
| this.parametersExtractor = parametersExtractor; |
| this.requiredByAspect = requiredByAspect; |
| } |
| |
| public String getName() { |
| return this.aspectClass.getName(); |
| } |
| |
| public ImmutableSet<String> getRequiredParameters() { |
| return ImmutableSet.of(); |
| } |
| |
| public ImmutableList<Attribute> getAspectAttributes() { |
| return ImmutableList.of(); |
| } |
| |
| protected abstract Aspect getAspect(Rule rule); |
| |
| protected abstract Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters) |
| throws EvalException; |
| |
| C getAspectClass() { |
| return aspectClass; |
| } |
| } |
| |
| private static class NativeAspectDetails extends AspectDetails<NativeAspectClass> { |
| NativeAspectDetails( |
| NativeAspectClass aspectClass, Function<Rule, AspectParameters> parametersExtractor) { |
| super(aspectClass, parametersExtractor); |
| } |
| |
| NativeAspectDetails( |
| NativeAspectClass aspectClass, |
| Function<Rule, AspectParameters> parametersExtractor, |
| String requiredByAspect) { |
| super(aspectClass, parametersExtractor, requiredByAspect); |
| } |
| |
| @Nullable |
| @Override |
| public Aspect getAspect(Rule rule) { |
| AspectParameters params = parametersExtractor.apply(rule); |
| return params == null ? null : Aspect.forNative(aspectClass, params); |
| } |
| |
| @Override |
| protected Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters) |
| throws EvalException { |
| // Native aspects ignore their top-level parameters values for now. |
| return Aspect.forNative(aspectClass, AspectParameters.EMPTY); |
| } |
| } |
| |
| private static class StarlarkAspectDetails extends AspectDetails<StarlarkAspectClass> { |
| private final StarlarkDefinedAspect aspect; |
| |
| private StarlarkAspectDetails(StarlarkDefinedAspect aspect, String requiredByAspect) { |
| super(aspect.getAspectClass(), aspect.getDefaultParametersExtractor(), requiredByAspect); |
| this.aspect = aspect; |
| } |
| |
| @Override |
| public ImmutableSet<String> getRequiredParameters() { |
| return aspect.getParamAttributes(); |
| } |
| |
| @Override |
| public ImmutableList<Attribute> getAspectAttributes() { |
| return aspect.getAttributes(); |
| } |
| |
| @Override |
| public Aspect getAspect(Rule rule) { |
| AspectParameters params = parametersExtractor.apply(rule); |
| return Aspect.forStarlark(aspectClass, aspect.getDefinition(params), params); |
| } |
| |
| @Override |
| public Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters) |
| throws EvalException { |
| AspectParameters params = aspect.extractTopLevelParameters(aspectParameters); |
| return Aspect.forStarlark(aspectClass, aspect.getDefinition(params), params); |
| } |
| } |
| |
| /** Aspect details that just wrap a pre-existing Aspect that doesn't vary with the Rule. */ |
| private static class PredefinedAspectDetails extends AspectDetails<AspectClass> { |
| private final Aspect aspect; |
| |
| PredefinedAspectDetails(Aspect aspect) { |
| super(aspect.getAspectClass(), null); |
| this.aspect = aspect; |
| } |
| |
| @Override |
| public Aspect getAspect(Rule rule) { |
| return aspect; |
| } |
| |
| @Override |
| public Aspect getTopLevelAspect(ImmutableMap<String, String> aspectParameters) |
| throws EvalException { |
| return aspect; |
| } |
| } |
| |
| @SerializationConstant @VisibleForSerialization |
| static final Function<Rule, AspectParameters> EMPTY_FUNCTION = input -> AspectParameters.EMPTY; |
| |
| /** A builder for AspectsList */ |
| public static class Builder { |
| private final HashMap<String, AspectDetails<?>> aspects = new LinkedHashMap<>(); |
| |
| public Builder() {} |
| |
| public Builder(AspectsList aspectsList) { |
| for (AspectDetails<?> aspect : aspectsList.aspects) { |
| aspects.put(aspect.getName(), aspect); |
| } |
| } |
| |
| public AspectsList build() { |
| return new AspectsList(ImmutableList.copyOf(aspects.values())); |
| } |
| |
| /** |
| * Adds a native aspect with its parameters extraction function to the aspects list. |
| * |
| * @param aspect the native aspect to be added |
| * @param evaluator function that extracts aspect parameters from rule. |
| */ |
| public void addAspect(NativeAspectClass aspect, Function<Rule, AspectParameters> evaluator) { |
| NativeAspectDetails nativeAspectDetails = new NativeAspectDetails(aspect, evaluator); |
| AspectDetails<?> oldAspect = |
| this.aspects.put(nativeAspectDetails.getName(), nativeAspectDetails); |
| if (oldAspect != null) { |
| throw new AssertionError( |
| String.format("Aspect %s has already been added", oldAspect.getName())); |
| } |
| } |
| |
| /** |
| * Adds a native aspect that does not need a parameters extractor to the aspects list. |
| * |
| * @param aspect the native aspect to be added |
| */ |
| public void addAspect(NativeAspectClass aspect) { |
| addAspect(aspect, EMPTY_FUNCTION); |
| } |
| |
| /** Attaches this aspect and its required aspects */ |
| public void addAspect(StarlarkAspect starlarkAspect) throws EvalException { |
| addAspect(starlarkAspect, null); |
| } |
| |
| private void addAspect(StarlarkAspect starlarkAspect, @Nullable String requiredByAspect) |
| throws EvalException { |
| if (starlarkAspect instanceof StarlarkDefinedAspect) { |
| StarlarkDefinedAspect starlarkDefinedAspect = (StarlarkDefinedAspect) starlarkAspect; |
| if (!starlarkDefinedAspect.isExported()) { |
| throw Starlark.errorf( |
| "Aspects should be top-level values in extension files that define them."); |
| } |
| |
| for (StarlarkAspect requiredAspect : starlarkDefinedAspect.getRequiredAspects()) { |
| addAspect(requiredAspect, starlarkDefinedAspect.getName()); |
| } |
| } |
| |
| boolean needsToAdd = needsToBeAdded(starlarkAspect.getName(), requiredByAspect); |
| if (needsToAdd) { |
| final AspectDetails<?> aspectDetails; |
| |
| if (starlarkAspect instanceof StarlarkDefinedAspect) { |
| aspectDetails = |
| new StarlarkAspectDetails((StarlarkDefinedAspect) starlarkAspect, requiredByAspect); |
| } else if (starlarkAspect instanceof StarlarkNativeAspect) { |
| aspectDetails = |
| new NativeAspectDetails( |
| (StarlarkNativeAspect) starlarkAspect, |
| starlarkAspect.getDefaultParametersExtractor(), |
| requiredByAspect); |
| } else { |
| throw new IllegalArgumentException(); |
| } |
| this.aspects.put(starlarkAspect.getName(), aspectDetails); |
| } |
| } |
| |
| /** Should only be used for deserialization. */ |
| public void addAspect(final Aspect aspect) { |
| PredefinedAspectDetails predefinedAspectDetails = new PredefinedAspectDetails(aspect); |
| AspectDetails<?> oldAspect = |
| this.aspects.put(predefinedAspectDetails.getName(), predefinedAspectDetails); |
| if (oldAspect != null) { |
| throw new AssertionError( |
| String.format("Aspect %s has already been added", oldAspect.getName())); |
| } |
| } |
| |
| /** |
| * Adds all aspect from the list. |
| * |
| * <p>The function is intended for extended Starlark rules, where aspect list is already built |
| * and may include aspects required by other aspects. |
| */ |
| public void addAspects(AspectsList aspectsList) throws EvalException { |
| for (AspectDetails<?> aspect : aspectsList.aspects) { |
| boolean needsToAdd = needsToBeAdded(aspect.getName(), aspect.requiredByAspect); |
| if (needsToAdd) { |
| aspects.put(aspect.getName(), aspect); |
| } |
| } |
| } |
| |
| private boolean needsToBeAdded(String aspectName, @Nullable String requiredByAspect) |
| throws EvalException { |
| |
| AspectDetails<?> oldAspect = this.aspects.get(aspectName); |
| |
| if (oldAspect != null) { |
| if (requiredByAspect != null) { |
| // If the aspect to be added already exists and it is required by another aspect, no need |
| // to |
| // add it again. |
| return false; |
| } else { |
| // If the aspect to be added is not required by another aspect, then we should throw error |
| String oldAspectBaseAspectName = oldAspect.requiredByAspect; |
| if (oldAspectBaseAspectName != null) { |
| throw Starlark.errorf( |
| "aspect %s was added before as a required aspect of aspect %s", |
| oldAspect.getName(), oldAspectBaseAspectName); |
| } |
| throw Starlark.errorf("aspect %s added more than once", oldAspect.getName()); |
| } |
| } |
| |
| return true; // we need to add the new aspect |
| } |
| } |
| } |