// 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.packages; import com.google.common.base.Function; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.LabelSyntaxException; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.packages.Attribute.SkylarkComputedDefaultTemplate.CannotPrecomputeDefaultsException; import com.google.devtools.build.lib.packages.Package.NameConflictException; import com.google.devtools.build.lib.packages.PackageFactory.PackageContext; import com.google.devtools.build.lib.syntax.BaseFunction; import com.google.devtools.build.lib.syntax.Environment; import com.google.devtools.build.lib.syntax.FuncallExpression; import com.google.devtools.build.lib.syntax.UserDefinedFunction; import com.google.devtools.build.lib.util.Pair; import com.google.devtools.build.lib.util.Preconditions; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * Given a {@link RuleClass} and a set of attribute values, returns a {@link Rule} instance. Also * performs a number of checks and associates the {@link Rule} and the owning {@link Package} * with each other. * * <p>Note: the code that actually populates the RuleClass map has been moved to {@link * RuleClassProvider}. */ public class RuleFactory { /** * Maps rule class name to the metaclass instance for that rule. */ private final ImmutableMap<String, RuleClass> ruleClassMap; private final Function<RuleClass, AttributeContainer> attributeContainerFactory; /** * Constructs a RuleFactory instance. */ public RuleFactory( RuleClassProvider provider, Function<RuleClass, AttributeContainer> attributeContainerFactory) { this.attributeContainerFactory = attributeContainerFactory; this.ruleClassMap = ImmutableMap.copyOf(provider.getRuleClassMap()); } /** * Returns the (immutable, unordered) set of names of all the known rule classes. */ public Set<String> getRuleClassNames() { return ruleClassMap.keySet(); } /** * Returns the RuleClass for the specified rule class name. */ public RuleClass getRuleClass(String ruleClassName) { return ruleClassMap.get(ruleClassName); } AttributeContainer getAttributeContainer(RuleClass ruleClass) { return attributeContainerFactory.apply(ruleClass); } Function<RuleClass, AttributeContainer> getAttributeContainerFactory() { return attributeContainerFactory; } /** * Creates and returns a rule instance. * * <p>It is the caller's responsibility to add the rule to the package (the caller may choose not * to do so if, for example, the rule has errors). */ static Rule createRule( Package.Builder pkgBuilder, RuleClass ruleClass, BuildLangTypedAttributeValuesMap attributeValues, EventHandler eventHandler, @Nullable FuncallExpression ast, Location location, @Nullable Environment env, AttributeContainer attributeContainer) throws InvalidRuleException, InterruptedException { Preconditions.checkNotNull(ruleClass); String ruleClassName = ruleClass.getName(); Object nameObject = attributeValues.getAttributeValue("name"); if (nameObject == null) { throw new InvalidRuleException(ruleClassName + " rule has no 'name' attribute"); } else if (!(nameObject instanceof String)) { throw new InvalidRuleException(ruleClassName + " 'name' attribute must be a string"); } String name = (String) nameObject; Label label; try { // Test that this would form a valid label name -- in particular, this // catches cases where Makefile variables $(foo) appear in "name". label = pkgBuilder.createLabel(name); } catch (LabelSyntaxException e) { throw new InvalidRuleException("illegal rule name: " + name + ": " + e.getMessage()); } boolean inWorkspaceFile = pkgBuilder.isWorkspace(); if (ruleClass.getWorkspaceOnly() && !inWorkspaceFile) { throw new RuleFactory.InvalidRuleException( ruleClass + " must be in the WORKSPACE file " + "(used by " + label + ")"); } else if (!ruleClass.getWorkspaceOnly() && inWorkspaceFile) { throw new RuleFactory.InvalidRuleException( ruleClass + " cannot be in the WORKSPACE file " + "(used by " + label + ")"); } AttributesAndLocation generator = generatorAttributesForMacros(attributeValues, env, location, label); try { return ruleClass.createRule( pkgBuilder, label, generator.attributes, eventHandler, ast, generator.location, attributeContainer); } catch (LabelSyntaxException | CannotPrecomputeDefaultsException e) { throw new RuleFactory.InvalidRuleException(ruleClass + " " + e.getMessage()); } } /** * Creates a {@link Rule} instance, adds it to the {@link Package.Builder} and returns it. * * @param pkgBuilder the under-construction {@link Package.Builder} to which the rule belongs * @param ruleClass the {@link RuleClass} of the rule * @param attributeValues a {@link BuildLangTypedAttributeValuesMap} mapping attribute names to * attribute values of build-language type. Each attribute must be defined for this class of * rule, and have a build-language-typed value which can be converted to the appropriate * native type of the attribute (i.e. via {@link BuildType#selectableConvert}). There must * be a map entry for each non-optional attribute of this class of rule. * @param eventHandler a eventHandler on which errors and warnings are reported during * rule creation * @param ast the abstract syntax tree of the rule expression (optional) * @param location the location at which this rule was declared * @param env the lexical environment of the function call which declared this rule (optional) * @param attributeContainer the {@link AttributeContainer} the rule will contain * @throws InvalidRuleException if the rule could not be constructed for any * reason (e.g. no {@code name} attribute is defined) * @throws NameConflictException if the rule's name or output files conflict with others in this * package * @throws InterruptedException if interrupted */ static Rule createAndAddRule( Package.Builder pkgBuilder, RuleClass ruleClass, BuildLangTypedAttributeValuesMap attributeValues, EventHandler eventHandler, @Nullable FuncallExpression ast, Location location, @Nullable Environment env, AttributeContainer attributeContainer) throws InvalidRuleException, NameConflictException, InterruptedException { Rule rule = createRule( pkgBuilder, ruleClass, attributeValues, eventHandler, ast, location, env, attributeContainer); pkgBuilder.addRule(rule); return rule; } /** * Creates a {@link Rule} instance, adds it to the {@link Package.Builder} and returns it. * * @param context the package-building context in which this rule was declared * @param ruleClass the {@link RuleClass} of the rule * @param attributeValues a {@link BuildLangTypedAttributeValuesMap} mapping attribute names to * attribute values of build-language type. Each attribute must be defined for this class * of rule, and have a build-language-typed value which can be converted to the appropriate * native type of the attribute (i.e. via {@link BuildType#selectableConvert}). There must * be a map entry for each non-optional attribute of this class of rule. * @param ast the abstract syntax tree of the rule expression (mandatory because this looks up a * {@link Location} from the {@code ast}) * @param env the lexical environment of the function call which declared this rule (optional) * @param attributeContainer the {@link AttributeContainer} the rule will contain * @throws InvalidRuleException if the rule could not be constructed for any reason (e.g. no * {@code name} attribute is defined) * @throws NameConflictException if the rule's name or output files conflict with others in this * package * @throws InterruptedException if interrupted */ public static Rule createAndAddRule( PackageContext context, RuleClass ruleClass, BuildLangTypedAttributeValuesMap attributeValues, FuncallExpression ast, @Nullable Environment env, AttributeContainer attributeContainer) throws InvalidRuleException, NameConflictException, InterruptedException { return createAndAddRule( context.pkgBuilder, ruleClass, attributeValues, context.eventHandler, ast, ast.getLocation(), env, attributeContainer); } /** * InvalidRuleException is thrown by {@link Rule} creation methods if the {@link Rule} could * not be constructed. It contains an error message. */ public static class InvalidRuleException extends Exception { private InvalidRuleException(String message) { super(message); } } /** A pair of attributes and location. */ private static final class AttributesAndLocation { final BuildLangTypedAttributeValuesMap attributes; final Location location; AttributesAndLocation(BuildLangTypedAttributeValuesMap attributes, Location location) { this.attributes = attributes; this.location = location; } } /** * A wrapper around an map of named attribute values that specifies whether the map's values * are of "build-language" or of "native" types. */ public interface AttributeValuesMap { /** * Returns {@code true} if all the map's values are "build-language typed", i.e., resulting * from the evaluation of an expression in the build language. Returns {@code false} if all * the map's values are "natively typed", i.e. of a type returned by {@link * BuildType#selectableConvert}. */ boolean valuesAreBuildLanguageTyped(); Iterable<String> getAttributeNames(); Object getAttributeValue(String attributeName); boolean isAttributeExplicitlySpecified(String attributeName); } /** A {@link AttributeValuesMap} of explicit "build-language" values. */ public static final class BuildLangTypedAttributeValuesMap implements AttributeValuesMap { private final Map<String, Object> attributeValues; public BuildLangTypedAttributeValuesMap(Map<String, Object> attributeValues) { this.attributeValues = attributeValues; } private boolean containsAttributeNamed(String attributeName) { return attributeValues.containsKey(attributeName); } @Override public boolean valuesAreBuildLanguageTyped() { return true; } @Override public Iterable<String> getAttributeNames() { return attributeValues.keySet(); } @Override public Object getAttributeValue(String attributeName) { return attributeValues.get(attributeName); } @Override public boolean isAttributeExplicitlySpecified(String attributeName) { return true; } } /** * If the rule was created by a macro, this method sets the appropriate values for the * attributes generator_{name, function, location} and returns all attributes. * * <p>Otherwise, it returns the given attributes without any changes. */ private static AttributesAndLocation generatorAttributesForMacros( BuildLangTypedAttributeValuesMap args, @Nullable Environment env, Location location, Label label) { // Returns the original arguments if a) there is only the rule itself on the stack // trace (=> no macro) or b) the attributes have already been set by Python pre-processing. if (env == null) { return new AttributesAndLocation(args, location); } boolean hasName = args.containsAttributeNamed("generator_name"); boolean hasFunc = args.containsAttributeNamed("generator_function"); // TODO(bazel-team): resolve cases in our code where hasName && !hasFunc, or hasFunc && !hasName if (hasName || hasFunc) { return new AttributesAndLocation(args, location); } Pair<FuncallExpression, BaseFunction> topCall = env.getTopCall(); if (topCall == null || !(topCall.second instanceof UserDefinedFunction)) { return new AttributesAndLocation(args, location); } FuncallExpression generator = topCall.first; BaseFunction function = topCall.second; String name = generator.getNameArg(); ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder(); for (String attributeName : args.getAttributeNames()) { builder.put(attributeName, args.getAttributeValue(attributeName)); } builder.put("generator_name", (name == null) ? args.getAttributeValue("name") : name); builder.put("generator_function", function.getName()); if (generator.getLocation() != null) { location = generator.getLocation(); } String relativePath = maybeGetRelativeLocation(location, label); if (relativePath != null) { builder.put("generator_location", relativePath); } try { return new AttributesAndLocation( new BuildLangTypedAttributeValuesMap(builder.build()), location); } catch (IllegalArgumentException ex) { // We just fall back to the default case and swallow any messages. return new AttributesAndLocation(args, location); } } /** * Uses the given label to retrieve the workspace-relative path of the given location (including * the line number). * * <p>For example, the location /usr/local/workspace/my/cool/package/BUILD:3:1 and the label * //my/cool/package:BUILD would lead to "my/cool/package:BUILD:3". * * @return The workspace-relative path of the given location, or null if it could not be computed. */ @Nullable private static String maybeGetRelativeLocation(@Nullable Location location, Label label) { if (location == null) { return null; } // Determining the workspace root only works reliably if both location and label point to files // in the same package. // It would be preferable to construct the path from the label itself, but this doesn't work for // rules created from function calls in a subincluded file, even if both files share a path // prefix (for example, when //a/package:BUILD subincludes //a/package/with/a/subpackage:BUILD). // We can revert to that approach once subincludes aren't supported anymore. String absolutePath = Location.printPathAndLine(location); int pos = absolutePath.indexOf(label.getPackageName()); return (pos < 0) ? null : absolutePath.substring(pos); } }