// 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 static com.google.devtools.build.lib.syntax.SkylarkType.castMap; import static java.util.Collections.singleton; import static java.util.stream.Collectors.toCollection; import com.google.auto.value.AutoValue; 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.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.escape.Escaper; import com.google.common.escape.Escapers; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization; import com.google.devtools.build.lib.syntax.ClassObject; import com.google.devtools.build.lib.syntax.EvalException; import com.google.devtools.build.lib.syntax.Runtime; import com.google.devtools.build.lib.syntax.SkylarkCallbackFunction; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.StringUtil; import com.google.devtools.build.lib.vfs.FileSystemUtils; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * A function interface allowing rules to specify their set of implicit outputs in a more dynamic * way than just simple template-substitution. For example, the set of implicit outputs may be a * function of rule attributes. * *

In the case that attribute placeholders are configurable attributes, errors will be thrown as * output templates are expanded before configurable attributes are resolved. * *

In the case that attribute placeholders are invalid, the template string will be left * unexpanded. */ // TODO(http://b/69387932): refactor this entire class and all callers. public abstract class ImplicitOutputsFunction { /** * Implicit output functions for Skylark supporting key value access of expanded implicit outputs. */ public abstract static class SkylarkImplicitOutputsFunction extends ImplicitOutputsFunction { public abstract ImmutableMap calculateOutputs( EventHandler eventHandler, AttributeMap map) throws EvalException, InterruptedException; @Override public Iterable getImplicitOutputs(EventHandler eventHandler, AttributeMap map) throws EvalException, InterruptedException { return calculateOutputs(eventHandler, map).values(); } } /** Implicit output functions executing Skylark code. */ @AutoCodec public static final class SkylarkImplicitOutputsFunctionWithCallback extends SkylarkImplicitOutputsFunction { private final SkylarkCallbackFunction callback; private final Location loc; public SkylarkImplicitOutputsFunctionWithCallback( SkylarkCallbackFunction callback, Location loc) { this.callback = callback; this.loc = loc; } @Override public ImmutableMap calculateOutputs( EventHandler eventHandler, AttributeMap map) throws EvalException, InterruptedException { Map attrValues = new HashMap<>(); for (String attrName : map.getAttributeNames()) { Type attrType = map.getAttributeType(attrName); // Don't include configurable attributes: we don't know which value they might take // since we don't yet have a build configuration. if (!map.isConfigurable(attrName)) { Object value = map.get(attrName, attrType); attrValues.put(attrName, value == null ? Runtime.NONE : value); } } ClassObject attrs = StructProvider.STRUCT.create( attrValues, "Attribute '%s' either doesn't exist " + "or uses a select() (i.e. could have multiple values)"); try { ImmutableMap.Builder builder = ImmutableMap.builder(); for (Map.Entry entry : castMap( callback.call(eventHandler, attrs), String.class, String.class, "implicit outputs function return value") .entrySet()) { // Returns empty string only in case of invalid templates Iterable substitutions = fromTemplates(entry.getValue()).getImplicitOutputs(eventHandler, map); if (Iterables.isEmpty(substitutions)) { throw new EvalException( loc, String.format( "For attribute '%s' in outputs: %s", entry.getKey(), "Invalid placeholder(s) in template")); } builder.put(entry.getKey(), Iterables.getOnlyElement(substitutions)); } return builder.build(); } catch (IllegalArgumentException e) { throw new EvalException(loc, e.getMessage()); } } } /** Implicit output functions using a simple an output map. */ @AutoCodec public static final class SkylarkImplicitOutputsFunctionWithMap extends SkylarkImplicitOutputsFunction { private final ImmutableMap outputMap; public SkylarkImplicitOutputsFunctionWithMap(ImmutableMap outputMap) { this.outputMap = outputMap; } @Override public ImmutableMap calculateOutputs( EventHandler eventHandler, AttributeMap map) throws EvalException, InterruptedException { ImmutableMap.Builder builder = ImmutableMap.builder(); for (Map.Entry entry : outputMap.entrySet()) { // Empty iff invalid placeholders present. ImplicitOutputsFunction outputsFunction = fromUnsafeTemplates(ImmutableList.of(entry.getValue())); Iterable substitutions = outputsFunction.getImplicitOutputs(eventHandler, map); if (Iterables.isEmpty(substitutions)) { throw new EvalException( null, String.format( "For attribute '%s' in outputs: %s", entry.getKey(), "Invalid placeholder(s) in template")); } builder.put(entry.getKey(), Iterables.getOnlyElement(substitutions)); } return builder.build(); } } /** * Implicit output functions which can not throw an EvalException. */ public abstract static class SafeImplicitOutputsFunction extends ImplicitOutputsFunction { @Override public abstract Iterable getImplicitOutputs( EventHandler eventHandler, AttributeMap map); } /** * An interface to objects that can retrieve rule attributes. */ public interface AttributeValueGetter { /** * Returns the value(s) of attribute "attr" in "rule", or empty set if attribute unknown. */ Set get(AttributeMap rule, String attr); } /** * The default rule attribute retriever. * *

Custom {@link AttributeValueGetter} implementations may delegate to this object as a * fallback mechanism. */ public static final AttributeValueGetter DEFAULT_RULE_ATTRIBUTE_GETTER = new AttributeValueGetter() { @Override public Set get(AttributeMap rule, String attr) { return attributeValues(rule, attr); } }; private static final Escaper PERCENT_ESCAPER = Escapers.builder().addEscape('%', "%%").build(); /** * Given a newly-constructed Rule instance (with attributes populated), returns the list of output * files that this rule produces implicitly. */ public abstract Iterable getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) throws EvalException, InterruptedException; /** The implicit output function that returns no files. */ @AutoCodec public static final SafeImplicitOutputsFunction NONE = new SafeImplicitOutputsFunction() { @Override public Iterable getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) { return Collections.emptyList(); } }; /** * A convenience wrapper for {@link #fromTemplates(Iterable)}. */ public static SafeImplicitOutputsFunction fromTemplates(String... templates) { return fromTemplates(Arrays.asList(templates)); } /** * The implicit output function that generates files based on a set of template substitutions * using rule attribute values. * *

This is not, actually, safe, and any use of configurable attributes will cause a hard * failure. * * @param templates The templates used to construct the name of the implicit output file target. * The substring "%{foo}" will be replaced by the value of the attribute "foo". If multiple * %{} substrings exist, the cross-product of them is generated. */ public static SafeImplicitOutputsFunction fromTemplates(final Iterable templates) { return new TemplateImplicitOutputsFunction(templates); } @VisibleForSerialization @AutoCodec static class TemplateImplicitOutputsFunction extends SafeImplicitOutputsFunction { private final Iterable templates; @VisibleForSerialization TemplateImplicitOutputsFunction(Iterable templates) { this.templates = templates; } // TODO(bazel-team): parse the templates already here @Override public Iterable getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) { ImmutableSet.Builder result = new ImmutableSet.Builder<>(); for (String template : templates) { List substitutions = substitutePlaceholderIntoTemplate(template, rule); if (substitutions.isEmpty()) { continue; } result.addAll(substitutions); } return result.build(); } @Override public String toString() { return StringUtil.joinEnglishList(templates); } } @AutoCodec @VisibleForSerialization static class UnsafeTemplatesImplicitOutputsFunction extends ImplicitOutputsFunction { private final Iterable templates; @VisibleForSerialization UnsafeTemplatesImplicitOutputsFunction(Iterable templates) { this.templates = templates; } // TODO(bazel-team): parse the templates already here @Override public Iterable getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) throws EvalException { ImmutableSet.Builder result = new ImmutableSet.Builder<>(); for (String template : templates) { List substitutions = substitutePlaceholderIntoUnsafeTemplate( template, rule, DEFAULT_RULE_ATTRIBUTE_GETTER); if (substitutions.isEmpty()) { continue; } result.addAll(substitutions); } return result.build(); } @Override public String toString() { return StringUtil.joinEnglishList(templates); } } /** * The implicit output function that generates files based on a set of template substitutions * using rule attribute values. * *

This is not, actually, safe, and any use of configurable attributes will cause a hard * failure. * * @param templates The templates used to construct the name of the implicit output file target. * The substring "%{foo}" will be replaced by the value of the attribute "foo". If multiple * %{} substrings exist, the cross-product of them is generated. */ // It would be nice to unify this with fromTemplates above, but that's not possible because // substitutePlaceholderIntoUnsafeTemplate can throw an exception. public static ImplicitOutputsFunction fromUnsafeTemplates(Iterable templates) { return new UnsafeTemplatesImplicitOutputsFunction(templates); } /** A convenience wrapper for {@link #fromFunctions(Iterable)}. */ public static SafeImplicitOutputsFunction fromFunctions( SafeImplicitOutputsFunction... functions) { return fromFunctions(Arrays.asList(functions)); } @AutoCodec @VisibleForSerialization static class FunctionCombinationImplicitOutputsFunction extends SafeImplicitOutputsFunction { private final Iterable functions; @VisibleForSerialization FunctionCombinationImplicitOutputsFunction(Iterable functions) { this.functions = functions; } @Override public Iterable getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) { Collection result = new LinkedHashSet<>(); for (SafeImplicitOutputsFunction function : functions) { Iterables.addAll(result, function.getImplicitOutputs(eventHandler, rule)); } return result; } @Override public String toString() { return StringUtil.joinEnglishList(functions); } } /** * The implicit output function that generates files based on a set of * template substitutions using rule attribute values. * * @param functions The functions used to construct the name of the implicit * output file target. The substring "%{name}" will be replaced by the * actual name of the rule, the substring "%{srcs}" will be replaced by the * name of each source file without its extension. If multiple %{} * substrings exist, the cross-product of them is generated. */ public static SafeImplicitOutputsFunction fromFunctions( final Iterable functions) { return new FunctionCombinationImplicitOutputsFunction(functions); } /** * Coerces attribute "attrName" of the specified rule into a sequence of * strings. Helper function for {@link #fromTemplates(Iterable)}. */ private static Set attributeValues(AttributeMap rule, String attrName) { if (attrName.equals("dirname")) { PathFragment dir = PathFragment.create(rule.getName()).getParentDirectory(); return (dir.segmentCount() == 0) ? singleton("") : singleton(dir.getPathString() + "/"); } else if (attrName.equals("basename")) { return singleton(PathFragment.create(rule.getName()).getBaseName()); } Type attrType = rule.getAttributeType(attrName); if (attrType == null) { return Collections.emptySet(); } // String attributes and lists are easy. if (Type.STRING == attrType) { return singleton(rule.get(attrName, Type.STRING)); } else if (Type.STRING_LIST == attrType) { return Sets.newLinkedHashSet(rule.get(attrName, Type.STRING_LIST)); } else if (BuildType.LABEL == attrType) { // Labels are most often used to change the extension, // e.g. %.foo -> %.java, so we return the basename w/o extension. Label label = rule.get(attrName, BuildType.LABEL); return singleton(FileSystemUtils.removeExtension(label.getName())); } else if (BuildType.LABEL_LIST == attrType) { // Labels are most often used to change the extension, // e.g. %.foo -> %.java, so we return the basename w/o extension. return rule.get(attrName, BuildType.LABEL_LIST) .stream() .map(label -> FileSystemUtils.removeExtension(label.getName())) .collect(toCollection(LinkedHashSet::new)); } else if (BuildType.OUTPUT == attrType) { Label out = rule.get(attrName, BuildType.OUTPUT); return singleton(out.getName()); } else if (BuildType.OUTPUT_LIST == attrType) { return rule.get(attrName, BuildType.OUTPUT_LIST) .stream() .map(Label::getName) .collect(toCollection(LinkedHashSet::new)); } throw new IllegalArgumentException( "Don't know how to handle " + attrName + " : " + attrType); } /** * Collects all named placeholders from the template while replacing them with %s. * *

Example: for {@code template} "%{name}_%{locales}.foo", it will return "%s_%s.foo" and * store "name" and "locales" in {@code placeholders}. * *

Incomplete placeholders are treated like text: for "a-%{x}-%{y" this method returns * "a-%s-%%{y" and stores "x" in {@code placeholders}. * * @param template a string with placeholders of the format %{...} * @param placeholders a collection to collect placeholders into; may contain duplicates if not a * Set * @return a format string for {@link String#format}, created from the template string with every * placeholder replaced by %s */ public static String createPlaceholderSubstitutionFormatString(String template, Collection placeholders) { return createPlaceholderSubstitutionFormatStringRecursive(template, placeholders, new StringBuilder()); } private static String createPlaceholderSubstitutionFormatStringRecursive(String template, Collection placeholders, StringBuilder formatBuilder) { int start = template.indexOf("%{"); if (start < 0) { return formatBuilder.append(PERCENT_ESCAPER.escape(template)).toString(); } int end = template.indexOf('}', start + 2); if (end < 0) { return formatBuilder.append(PERCENT_ESCAPER.escape(template)).toString(); } formatBuilder.append(PERCENT_ESCAPER.escape(template.substring(0, start))).append("%s"); placeholders.add(template.substring(start + 2, end)); return createPlaceholderSubstitutionFormatStringRecursive(template.substring(end + 1), placeholders, formatBuilder); } /** * Given a template string, replaces all placeholders of the form %{...} with * the values from attributeSource. If there are multiple placeholders, then * the output is the cross product of substitutions. */ public static ImmutableList substitutePlaceholderIntoTemplate(String template, AttributeMap rule) { return substitutePlaceholderIntoTemplate(template, rule, DEFAULT_RULE_ATTRIBUTE_GETTER); } @AutoValue abstract static class ParsedTemplate { abstract String template(); abstract String formatStr(); abstract List attributeNames(); static ParsedTemplate parse(String rawTemplate) { List placeholders = Lists.newArrayList(); String formatStr = createPlaceholderSubstitutionFormatString(rawTemplate, placeholders); if (placeholders.isEmpty()) { placeholders = ImmutableList.of(); } return new AutoValue_ImplicitOutputsFunction_ParsedTemplate( rawTemplate, formatStr, placeholders); } ImmutableList substituteAttributes( AttributeMap attributeMap, AttributeValueGetter attributeGetter) { if (attributeNames().isEmpty()) { return ImmutableList.of(template()); } List> values = Lists.newArrayListWithCapacity(attributeNames().size()); for (String placeholder : attributeNames()) { Set attrValues = attributeGetter.get(attributeMap, placeholder); if (attrValues.isEmpty()) { return ImmutableList.of(); } values.add(attrValues); } ImmutableList.Builder out = new ImmutableList.Builder<>(); for (List combination : Sets.cartesianProduct(values)) { out.add(String.format(formatStr(), combination.toArray())); } return out.build(); } } /** * Substitutes attribute-placeholders in a template string, producing all possible combinations. * * @param template the template string, may contain named placeholders for rule attributes, like * %{name} or %{deps} * @param rule the rule whose attributes the placeholders correspond to * @param attributeGetter a helper for fetching attribute values * @return all possible combinations of the attributes referenced by the placeholders, substituted * into the template; empty if any of the placeholders expands to no values */ public static ImmutableList substitutePlaceholderIntoTemplate( String template, AttributeMap rule, AttributeValueGetter attributeGetter) { // Parse the template to get the attribute names and format string. ParsedTemplate parsedTemplate = ParsedTemplate.parse(template); // Return the substituted strings. return parsedTemplate.substituteAttributes(rule, attributeGetter); } private static ImmutableList substitutePlaceholderIntoUnsafeTemplate( String unsafeTemplate, AttributeMap rule, AttributeValueGetter attributeGetter) throws EvalException { // Parse the template to get the attribute names and format string. ParsedTemplate parsedTemplate = ParsedTemplate.parse(unsafeTemplate); // Make sure all attributes are valid. for (String placeholder : parsedTemplate.attributeNames()) { if (rule.isConfigurable(placeholder)) { throw new EvalException( rule.getAttributeLocation(placeholder), String.format( "Attribute %s is configurable and cannot be used in outputs", placeholder)); } } // Return the substituted strings. return parsedTemplate.substituteAttributes(rule, attributeGetter); } }