diff options
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/analysis')
5 files changed, 376 insertions, 1 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java index 541569c20a..ac85e0741c 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java @@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; +import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment; import com.google.devtools.build.lib.analysis.config.BuildOptions; import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory; import com.google.devtools.build.lib.analysis.config.DefaultsPackage; @@ -50,6 +51,7 @@ import com.google.devtools.build.lib.packages.RuleClassProvider; import com.google.devtools.build.lib.packages.RuleErrorConsumer; import com.google.devtools.build.lib.packages.Target; import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy; +import com.google.devtools.build.lib.skylarkinterface.SkylarkModule; import com.google.devtools.build.lib.syntax.Environment; import com.google.devtools.build.lib.syntax.Environment.Extension; import com.google.devtools.build.lib.syntax.Environment.Phase; @@ -296,6 +298,9 @@ public class ConfiguredRuleClassProvider implements RuleClassProvider { * between option classes, factories, and fragments, such that the factory depends only on the * options class and creates the fragment. This method provides a convenient way of adding both * the options class and the factory in a single call. + * + * <p>Note that configuration fragments annotated with a Skylark name must have a unique + * name; no two different configuration fragments can share the same name. */ public Builder addConfig( Class<? extends FragmentOptions> options, ConfigurationFragmentFactory factory) { @@ -312,6 +317,12 @@ public class ConfiguredRuleClassProvider implements RuleClassProvider { return this; } + /** + * Adds a configuration fragment factory. + * + * <p>Note that configuration fragments annotated with a Skylark name must have a unique + * name; no two different configuration fragments can share the same name. + */ public Builder addConfigurationFragment(ConfigurationFragmentFactory factory) { configurationFragmentFactories.add(factory); return this; @@ -549,6 +560,8 @@ public class ConfiguredRuleClassProvider implements RuleClassProvider { private final Environment.Frame globals; + private final ImmutableMap<String, Class<?>> configurationFragmentMap; + private ConfiguredRuleClassProvider( Label preludeLabel, String runfilesPrefix, @@ -581,6 +594,7 @@ public class ConfiguredRuleClassProvider implements RuleClassProvider { this.universalFragment = universalFragment; this.prerequisiteValidator = prerequisiteValidator; this.globals = createGlobals(skylarkAccessibleJavaClasses, skylarkModules); + this.configurationFragmentMap = createFragmentMap(configurationFragmentFactories); } public PrerequisiteValidator getPrerequisiteValidator() { @@ -700,6 +714,19 @@ public class ConfiguredRuleClassProvider implements RuleClassProvider { } } + private static ImmutableMap<String, Class<?>> createFragmentMap( + Iterable<ConfigurationFragmentFactory> configurationFragmentFactories) { + ImmutableMap.Builder<String, Class<?>> mapBuilder = ImmutableMap.builder(); + for (ConfigurationFragmentFactory fragmentFactory : configurationFragmentFactories) { + Class<? extends Fragment> fragmentClass = fragmentFactory.creates(); + String fragmentName = SkylarkModule.Resolver.resolveName(fragmentClass); + if (fragmentName != null) { + mapBuilder.put(fragmentName, fragmentClass); + } + } + return mapBuilder.build(); + } + private Environment createSkylarkRuleClassEnvironment( Mutability mutability, Environment.Frame globals, @@ -717,6 +744,7 @@ public class ConfiguredRuleClassProvider implements RuleClassProvider { .setPhase(Phase.LOADING) .build(); SkylarkUtils.setToolsRepository(env, toolsRepository); + SkylarkUtils.setFragmentMap(env, configurationFragmentMap); return env; } @@ -747,6 +775,11 @@ public class ConfiguredRuleClassProvider implements RuleClassProvider { return defaultWorkspaceFileSuffix; } + @Override + public Map<String, Class<?>> getConfigurationFragmentMap() { + return configurationFragmentMap; + } + /** * Returns all registered {@link BuildConfiguration.Fragment} classes. */ diff --git a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java index d406511d66..48113f2596 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/DependencyResolver.java @@ -488,7 +488,7 @@ public abstract class DependencyResolver { LateBoundDefault<FragmentT, ValueT> lateBoundDefault, Rule rule, AttributeMap attributeMap, - BuildConfiguration config) { + BuildConfiguration config) throws EvalException { Class<FragmentT> fragmentClass = lateBoundDefault.getFragmentClass(); // TODO(b/65746853): remove this when nothing uses it anymore if (BuildConfiguration.class.equals(fragmentClass)) { diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java index ad927c4801..9883e635e3 100644 --- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java +++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkAttr.java @@ -49,6 +49,7 @@ import com.google.devtools.build.lib.syntax.SkylarkDict; import com.google.devtools.build.lib.syntax.SkylarkList; import com.google.devtools.build.lib.syntax.SkylarkSignatureProcessor; import com.google.devtools.build.lib.syntax.SkylarkType; +import com.google.devtools.build.lib.syntax.SkylarkUtils; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.syntax.Type.ConversionException; import com.google.devtools.build.lib.syntax.Type.LabelClass; @@ -196,6 +197,8 @@ public final class SkylarkAttr implements SkylarkValue { builder.value( new SkylarkComputedDefaultTemplate( type, callback.getParameterNames(), callback, ast.getLocation())); + } else if (defaultValue instanceof SkylarkLateBoundDefault) { + builder.value((SkylarkLateBoundDefault) defaultValue); } else { builder.defaultValue(defaultValue, env.getGlobals().getTransitiveLabel(), DEFAULT_ARG); } @@ -519,6 +522,49 @@ public final class SkylarkAttr implements SkylarkValue { }; @SkylarkSignature( + name = "configuration_field", + returnType = SkylarkLateBoundDefault.class, + // TODO(cparsons): Provide a link to documentation for available SkylarkConfigurationFields. + doc = "References a late-bound default value for an attribute of type " + + "<a href=\"attr.html#label\">label</a>. A value is 'late-bound' if it requires " + + "the configuration to be built before determining the value. Any attribute using this " + + "as a value must <a href=\"../rules.html#private-attributes\">be private</a>.", + parameters = { + @Param( + name = "fragment", + type = String.class, + doc = "The name of a configuration fragment which contains the late-bound value." + ), + @Param( + name = "name", + type = String.class, + doc = "The name of the value to obtain from the configuration fragment."), + }, + useLocation = true, + useEnvironment = true + ) + private static final BuiltinFunction configurationField = + new BuiltinFunction("configuration_field") { + public SkylarkLateBoundDefault<?> invoke( + String fragment, String name, Location loc, Environment env) + throws EvalException { + Class<?> fragmentClass = SkylarkUtils.getFragmentMap(env).get(fragment); + + if (fragmentClass == null) { + throw new EvalException( + loc, + String.format("invalid configuration fragment name '%s'", fragment)); + } + try { + return SkylarkLateBoundDefault.forConfigurationField( + fragmentClass, name, SkylarkUtils.getToolsRepository(env)); + } catch (SkylarkLateBoundDefault.InvalidConfigurationFieldException exception) { + throw new EvalException(loc, exception); + } + } + }; + + @SkylarkSignature( name = "string", doc = "Creates an attribute of type <a href=\"string.html\">string</a>.", objectType = SkylarkAttr.class, @@ -598,6 +644,7 @@ public final class SkylarkAttr implements SkylarkValue { allowedTypes = { @ParamType(type = Label.class), @ParamType(type = String.class), + @ParamType(type = SkylarkLateBoundDefault.class) }, callbackEnabled = true, noneable = true, diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkConfigurationField.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkConfigurationField.java new file mode 100644 index 0000000000..153751c318 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkConfigurationField.java @@ -0,0 +1,71 @@ +// Copyright 2017 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.skylark; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A marker interface for Java methods of Skylark-exposed configuration fragments which denote + * Skylark "configuration fields": late-bound attribute defaults that depend on configuration. + * + * <p>Methods annotated with this annotation have a few constraints: + * <ul> + * <li>The annotated method must be on a configuration fragment exposed to skylark.</li> + * <li>The method must have return type Label.</li> + * <li>The method must be public.</li> + * <li>The method must have zero arguments.</li> + * <li>The method must not throw exceptions.</li> + * </ul> + */ +// TODO(b/68817606): Verify the above constraints using annotation processing. +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SkylarkConfigurationField { + + /** + * Name of the configuration field, as exposed to Skylark. + */ + String name(); + + /** + * The default label associated with this field, corresponding to the value of this configuration + * field with default command line flags. + * + * <p>If the default label is under the tools repository, omit the tools repository prefix + * from this default, but set {@link #defaultInToolRepository} to true.</p> + */ + String defaultLabel(); + + /** + * Whether the default label as defined in {@link #defaultLabel} should be prefixed with + * the tools repository. + */ + boolean defaultInToolRepository() default false; + + /** + * The documentation text in Skylark. It can contain HTML tags for special formatting. + * + * <p>It is allowed to be empty only if {@link #documented()} is false. + */ + String doc() default ""; + + /** + * If true, the function will appear in the Skylark documentation. Set this to false if the + * function is experimental or an overloading and doesn't need to be documented. + */ + boolean documented() default true; +} diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkLateBoundDefault.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkLateBoundDefault.java new file mode 100644 index 0000000000..51f694cc4d --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkLateBoundDefault.java @@ -0,0 +1,224 @@ +// Copyright 2017 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.skylark; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; +import com.google.devtools.build.lib.cmdline.Label; +import com.google.devtools.build.lib.packages.Attribute.LateBoundDefault; +import com.google.devtools.build.lib.packages.AttributeMap; +import com.google.devtools.build.lib.packages.Rule; +import com.google.devtools.build.lib.skylarkinterface.SkylarkModule; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import javax.annotation.concurrent.Immutable; + +/** + * An implementation of {@link LateBoundDefault} which obtains a late-bound attribute value + * (of type 'label') specifically by skylark configuration fragment name and field name, as + * registered by {@link SkylarkConfigurationField}. + * + * <p>For example, a SkylarkLateBoundDefault on "java" and "toolchain" would + * require a valid configuration fragment named "java" with a method annotated with + * {@link SkylarkConfigurationField} of name "toolchain". This {@link LateBoundDefault} would + * provide a late-bound dependency (defined by the label returned by that configuration field) + * in the current target configuration. + */ +@Immutable +public class SkylarkLateBoundDefault<FragmentT> extends LateBoundDefault<FragmentT, Label> { + + private final Method method; + private final String fragmentName; + private final String fragmentFieldName; + + @Override + public Label resolve(Rule rule, AttributeMap attributes, FragmentT config) { + Class<?> fragmentClass = config.getClass(); + try { + Object result = method.invoke(config); + return (Label) result; + } catch (IllegalAccessException | InvocationTargetException e) { + // Configuration field methods should not throw either of these exceptions. + throw new AssertionError("Method invocation failed: " + e); + } + } + + /** + * Returns the {@link SkylarkConfigurationField} annotation corresponding to this method. + */ + private static Label getDefaultLabel( + SkylarkConfigurationField annotation, String toolsRepository) { + Label defaultLabel = annotation.defaultInToolRepository() + ? Label.parseAbsoluteUnchecked(toolsRepository + annotation.defaultLabel()) + : Label.parseAbsoluteUnchecked(annotation.defaultLabel()); + return defaultLabel; + } + + private SkylarkLateBoundDefault(SkylarkConfigurationField annotation, + Class<FragmentT> fragmentClass, String fragmentName, Method method, String toolsRepository) { + super(false /* don't use host configuration */, + fragmentClass, + getDefaultLabel(annotation, toolsRepository)); + + this.method = method; + this.fragmentName = fragmentName; + this.fragmentFieldName = annotation.name(); + } + + /** + * Returns the skylark name of the configuration fragment that this late bound default requires. + */ + public String getFragmentName() { + return fragmentName; + } + + /** + * Returns the skylark name of the configuration field name, as registered by + * {@link SkylarkConfigurationField} annotation on the configuration fragment. + */ + public String getFragmentFieldName() { + return fragmentFieldName; + } + + /** + * An exception thrown if a user specifies an invalid configuration field identifier. + * + * @see SkylarkConfigurationField + **/ + public static class InvalidConfigurationFieldException extends Exception { + public InvalidConfigurationFieldException(String message) { + super(message); + } + } + + + private static class CacheKey { + private final Class<?> fragmentClass; + private final String toolsRepository; + + private CacheKey(Class<?> fragmentClass, + String toolsRepository) { + this.fragmentClass = fragmentClass; + this.toolsRepository = toolsRepository; + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } else if (!(object instanceof CacheKey)) { + return false; + } else { + CacheKey cacheKey = (CacheKey) object; + return fragmentClass.equals(cacheKey.fragmentClass) + && toolsRepository.equals(cacheKey.toolsRepository); + } + } + + @Override + public int hashCode() { + int result = fragmentClass.hashCode(); + result = 31 * result + toolsRepository.hashCode(); + return result; + } + } + + /** + * A cache for efficient {@link SkylarkLateBoundDefault} loading by configuration fragment. Each + * configuration fragment class key is mapped to a {@link Map} where keys are configuration field + * skylark names, and values are the {@link SkylarkLateBoundDefault}s. Methods must be annotated + * with {@link SkylarkConfigurationField} to be considered. + */ + private static final LoadingCache<CacheKey, Map<String, SkylarkLateBoundDefault<?>>> fieldCache = + CacheBuilder.newBuilder() + .initialCapacity(10) + .maximumSize(100) + .build( + new CacheLoader<CacheKey, Map<String, SkylarkLateBoundDefault<?>>>() { + @Override + public Map<String, SkylarkLateBoundDefault<?>> load(CacheKey key) throws Exception { + ImmutableMap.Builder<String, SkylarkLateBoundDefault<?>> lateBoundDefaultMap = + new ImmutableMap.Builder<>(); + Class<?> fragmentClass = key.fragmentClass; + String fragmentName = SkylarkModule.Resolver.resolveName(fragmentClass); + for (Method method : fragmentClass.getMethods()) { + if (method.isAnnotationPresent(SkylarkConfigurationField.class)) { + // TODO(b/68817606): Use annotation processors to verify these constraints. + Preconditions.checkArgument( + method.getReturnType() == Label.class, + String.format("Method %s must have return type 'Label'", method)); + Preconditions.checkArgument( + method.getParameterTypes().length == 0, + String.format("Method %s must not accept arguments", method)); + + SkylarkConfigurationField configField = + method.getAnnotation(SkylarkConfigurationField.class); + lateBoundDefaultMap.put( + configField.name(), + new SkylarkLateBoundDefault<>( + configField, + fragmentClass, + fragmentName, + method, + key.toolsRepository)); + } + } + return lateBoundDefaultMap.build(); + } + }); + + /** + * Returns a {@link LateBoundDefault} which obtains a late-bound attribute value + * (of type 'label') specifically by skylark configuration fragment name and field name, as + * registered by {@link SkylarkConfigurationField}. + * + * @param fragmentClass the configuration fragment class, which must have a valid skylark name + * @param fragmentFieldName the configuration field name, as registered by + * {@link SkylarkConfigurationField} annotation + * @param toolsRepository the Bazel tools repository path fragment + * + * @throws InvalidConfigurationFieldException if there is no valid configuration field with the + * given fragment class and field name + */ + public static <FragmentT> SkylarkLateBoundDefault<FragmentT> forConfigurationField( + Class<FragmentT> fragmentClass, + String fragmentFieldName, + String toolsRepository) throws InvalidConfigurationFieldException { + try { + CacheKey cacheKey = new CacheKey(fragmentClass, toolsRepository); + SkylarkLateBoundDefault resolver = + fieldCache.get(cacheKey).get(fragmentFieldName); + if (resolver == null) { + String fragmentName = SkylarkModule.Resolver.resolveName(fragmentClass); + if (Strings.isNullOrEmpty(fragmentName)) { + throw new AssertionError("fragment class must have a valid skylark name"); + } + throw new InvalidConfigurationFieldException( + String.format("invalid configuration field name '%s' on fragment '%s'", + fragmentFieldName, fragmentName)); + } + return resolver; + } catch (ExecutionException e) { + throw new IllegalStateException("method invocation failed: " + e); + } + } + +} |