// 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.analysis.skylark; import static com.google.devtools.build.lib.analysis.BaseRuleClasses.RUN_UNDER; import static com.google.devtools.build.lib.packages.Attribute.attr; import static com.google.devtools.build.lib.packages.BuildType.LABEL; import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST; import static com.google.devtools.build.lib.packages.BuildType.LICENSE; import static com.google.devtools.build.lib.syntax.SkylarkType.castMap; import static com.google.devtools.build.lib.syntax.Type.BOOLEAN; import static com.google.devtools.build.lib.syntax.Type.INTEGER; import static com.google.devtools.build.lib.syntax.Type.STRING; import static com.google.devtools.build.lib.syntax.Type.STRING_LIST; import com.google.common.base.Preconditions; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.ActionsProvider; import com.google.devtools.build.lib.analysis.BaseRuleClasses; import com.google.devtools.build.lib.analysis.DefaultInfo; import com.google.devtools.build.lib.analysis.TemplateVariableInfo; import com.google.devtools.build.lib.analysis.config.ConfigAwareRuleClassBuilder; import com.google.devtools.build.lib.analysis.config.HostTransition; import com.google.devtools.build.lib.analysis.config.transitions.PatchTransition; import com.google.devtools.build.lib.analysis.skylark.SkylarkAttr.Descriptor; import com.google.devtools.build.lib.analysis.test.TestConfiguration; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.LabelSyntaxException; import com.google.devtools.build.lib.cmdline.LabelValidator; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.packages.Attribute; import com.google.devtools.build.lib.packages.AttributeMap; import com.google.devtools.build.lib.packages.AttributeValueSource; import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunctionWithCallback; import com.google.devtools.build.lib.packages.ImplicitOutputsFunction.SkylarkImplicitOutputsFunctionWithMap; import com.google.devtools.build.lib.packages.NativeProvider; import com.google.devtools.build.lib.packages.Package.NameConflictException; import com.google.devtools.build.lib.packages.PackageFactory; import com.google.devtools.build.lib.packages.PackageFactory.PackageContext; import com.google.devtools.build.lib.packages.PredicateWithMessage; import com.google.devtools.build.lib.packages.Provider; import com.google.devtools.build.lib.packages.RuleClass; import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType; import com.google.devtools.build.lib.packages.RuleFactory; import com.google.devtools.build.lib.packages.RuleFactory.BuildLangTypedAttributeValuesMap; import com.google.devtools.build.lib.packages.RuleFactory.InvalidRuleException; import com.google.devtools.build.lib.packages.RuleFunction; import com.google.devtools.build.lib.packages.SkylarkAspect; import com.google.devtools.build.lib.packages.SkylarkDefinedAspect; import com.google.devtools.build.lib.packages.SkylarkExportable; import com.google.devtools.build.lib.packages.SkylarkProvider; import com.google.devtools.build.lib.packages.SkylarkProviderIdentifier; import com.google.devtools.build.lib.packages.TargetUtils; import com.google.devtools.build.lib.packages.TestSize; import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; import com.google.devtools.build.lib.skylarkbuildapi.SkylarkRuleFunctionsApi; import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter; import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature; import com.google.devtools.build.lib.syntax.BaseFunction; import com.google.devtools.build.lib.syntax.Environment; import com.google.devtools.build.lib.syntax.EvalException; import com.google.devtools.build.lib.syntax.EvalUtils; import com.google.devtools.build.lib.syntax.FuncallExpression; import com.google.devtools.build.lib.syntax.FunctionSignature; import com.google.devtools.build.lib.syntax.Runtime; import com.google.devtools.build.lib.syntax.SkylarkCallbackFunction; 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.util.FileTypeSet; import com.google.devtools.build.lib.util.Pair; import java.util.Map; import java.util.concurrent.ExecutionException; /** * A helper class to provide an easier API for Skylark rule definitions. */ public class SkylarkRuleClassFunctions implements SkylarkRuleFunctionsApi { // TODO(bazel-team): Copied from ConfiguredRuleClassProvider for the transition from built-in // rules to skylark extensions. Using the same instance would require a large refactoring. // If we don't want to support old built-in rules and Skylark simultaneously // (except for transition phase) it's probably OK. private static final LoadingCache labelCache = CacheBuilder.newBuilder().build(new CacheLoader() { @Override public Label load(String from) throws Exception { try { return Label.parseAbsolute(from, false); } catch (LabelSyntaxException e) { throw new Exception(from); } } }); // TODO(bazel-team): Remove the code duplication (BaseRuleClasses and this class). /** Parent rule class for non-executable non-test Skylark rules. */ public static final RuleClass baseRule = BaseRuleClasses.commonCoreAndSkylarkAttributes( BaseRuleClasses.nameAttribute( new RuleClass.Builder("$base_rule", RuleClassType.ABSTRACT, true)) .add(attr("expect_failure", STRING))) // TODO(skylark-team): Allow Skylark rules to extend native rules and remove duplication. .add( attr("toolchains", LABEL_LIST) .allowedFileTypes(FileTypeSet.NO_FILE) .mandatoryProviders(ImmutableList.of(TemplateVariableInfo.PROVIDER.id()))) .build(); /** Parent rule class for executable non-test Skylark rules. */ public static final RuleClass binaryBaseRule = new RuleClass.Builder("$binary_base_rule", RuleClassType.ABSTRACT, true, baseRule) .add(attr("args", STRING_LIST)) .add(attr("output_licenses", LICENSE)) .build(); /** Parent rule class for test Skylark rules. */ public static final RuleClass getTestBaseRule(String toolsRepository, PatchTransition lipoDataTransition) { return new RuleClass.Builder("$test_base_rule", RuleClassType.ABSTRACT, true, baseRule) .requiresConfigurationFragments(TestConfiguration.class) .add( attr("size", STRING) .value("medium") .taggable() .nonconfigurable("used in loading phase rule validation logic")) .add( attr("timeout", STRING) .taggable() .nonconfigurable("used in loading phase rule validation logic") .value(timeoutAttribute)) .add( attr("flaky", BOOLEAN) .value(false) .taggable() .nonconfigurable("taggable - called in Rule.getRuleTags")) .add(attr("shard_count", INTEGER).value(-1)) .add( attr("local", BOOLEAN) .value(false) .taggable() .nonconfigurable( "policy decision: this should be consistent across configurations")) .add(attr("args", STRING_LIST)) // Input files for every test action .add( attr("$test_runtime", LABEL_LIST) .cfg(HostTransition.INSTANCE) .value( ImmutableList.of( labelCache.getUnchecked(toolsRepository + "//tools/test:runtime")))) .add( attr("$test_setup_script", LABEL) .cfg(HostTransition.INSTANCE) .singleArtifact() .value(labelCache.getUnchecked(toolsRepository + "//tools/test:test_setup"))) .add( attr("$collect_coverage_script", LABEL) .cfg(HostTransition.INSTANCE) .singleArtifact() .value(labelCache.getUnchecked(toolsRepository + "//tools/test:collect_coverage"))) // Input files for test actions collecting code coverage .add( attr("$coverage_support", LABEL) .cfg(HostTransition.INSTANCE) .value(labelCache.getUnchecked("//tools/defaults:coverage_support"))) // Used in the one-per-build coverage report generation action. .add( attr("$coverage_report_generator", LABEL) .cfg(HostTransition.INSTANCE) .value(labelCache.getUnchecked("//tools/defaults:coverage_report_generator")) .singleArtifact()) .add(attr(":run_under", LABEL).cfg(lipoDataTransition).value(RUN_UNDER)) .build(); } @AutoCodec @AutoCodec.VisibleForSerialization static final Attribute.ComputedDefault timeoutAttribute = new Attribute.ComputedDefault() { @Override public Object getDefault(AttributeMap rule) { TestSize size = TestSize.getTestSize(rule.get("size", Type.STRING)); if (size != null) { String timeout = size.getDefaultTimeout().toString(); if (timeout != null) { return timeout; } } return "illegal"; } }; @SkylarkSignature( name = "DefaultInfo", returnType = Provider.class, doc = "A provider that gives general information about a target's direct and transitive files. " + "Every rule type has this provider, even if it is not returned explicitly by the " + "rule's implementation function." + "

The DefaultInfo constructor accepts the following parameters:" + "

    " + "
  • executable: If this rule is marked " + "executable or " + "test, this is a " + "File object representing the file that should " + "be executed to run the target. By default it is the predeclared output " + "ctx.outputs.executable." + "
  • files: A depset of " + "File objects representing the default outputs " + "to build when this target is specified on the blaze command line. By default it is " + "all predeclared outputs." + "
  • runfiles: set of files acting as both the " + "data_runfiles and default_runfiles." + "
  • data_runfiles: are the files that are added to the runfiles of a " + "target that depends on the rule via the data attribute." + "
  • default_runfiles: are the files that are added to the runfiles of " + "a target that depends on the rule via anything but the data attribute." + "
" + "Each DefaultInfo instance has the following fields: " + "
    " + "
  • files" + "
  • files_to_run" + "
  • data_runfiles" + "
  • default_runfiles" + "
" + "See the rules page for more information." ) private static final NativeProvider defaultInfo = DefaultInfo.PROVIDER; // TODO(bazel-team): Move to a "testing" namespace module. Normally we'd pass an objectType // to @SkylarkSignature to do this, but that doesn't work here because we're exposing an already- // configured BaseFunction, rather than defining a new BuiltinFunction. This should wait for // better support from the Skylark/Java interface, or perhaps support for first-class modules. @SkylarkSignature( name = "Actions", returnType = SkylarkProvider.class, doc = "(Note: This is a provider type. Don't instantiate it yourself; use it to retrieve a " + "provider object from a Target.)" + "

" + "Provides access to the actions generated by a rule. " + "There is one field, by_file, which is a dictionary from an output " + "of the rule to its corresponding generating action. " + "

" + "This is designed for testing rules, and should not be accessed outside " + "of test logic. This provider is only available for targets generated by rules" + " that have _skylark_testable " + "set to True." ) private static final NativeProvider actions = ActionsProvider.SKYLARK_CONSTRUCTOR; @Override public Provider provider(String doc, Object fields, Location location) throws EvalException { Iterable fieldNames = null; if (fields instanceof SkylarkList) { @SuppressWarnings("unchecked") SkylarkList list = (SkylarkList) SkylarkType.cast( fields, SkylarkList.class, String.class, location, "Expected list of strings or dictionary of string -> string for 'fields'"); fieldNames = list; } else if (fields instanceof SkylarkDict) { Map dict = SkylarkType.castMap( fields, String.class, String.class, "Expected list of strings or dictionary of string -> string for 'fields'"); fieldNames = dict.keySet(); } return SkylarkProvider.createUnexportedSchemaful(fieldNames, location); } // TODO(bazel-team): implement attribute copy and other rule properties @Override @SuppressWarnings({"rawtypes", "unchecked"}) // castMap produces // an Attribute.Builder instead of a Attribute.Builder but it's OK. public BaseFunction rule( BaseFunction implementation, Boolean test, Object attrs, Object implicitOutputs, Boolean executable, Boolean outputToGenfiles, SkylarkList fragments, SkylarkList hostFragments, Boolean skylarkTestable, SkylarkList toolchains, String doc, SkylarkList providesArg, FuncallExpression ast, Environment funcallEnv) throws EvalException, ConversionException { funcallEnv.checkLoadingOrWorkspacePhase("rule", ast.getLocation()); RuleClassType type = test ? RuleClassType.TEST : RuleClassType.NORMAL; RuleClass parent = test ? getTestBaseRule( SkylarkUtils.getToolsRepository(funcallEnv), (PatchTransition) SkylarkUtils.getLipoDataTransition(funcallEnv)) : (executable ? binaryBaseRule : baseRule); // We'll set the name later, pass the empty string for now. RuleClass.Builder builder = new RuleClass.Builder("", type, true, parent); ImmutableList> attributes = attrObjectToAttributesList(attrs, ast); if (skylarkTestable) { builder.setSkylarkTestable(); } if (executable || test) { addAttribute( ast.getLocation(), builder, attr("$is_executable", BOOLEAN) .value(true) .nonconfigurable("Called from RunCommand.isExecutable, which takes a Target") .build()); builder.setExecutableSkylark(); } if (implicitOutputs != Runtime.NONE) { if (implicitOutputs instanceof BaseFunction) { BaseFunction func = (BaseFunction) implicitOutputs; SkylarkCallbackFunction callback = new SkylarkCallbackFunction(func, ast, funcallEnv.getSemantics()); builder.setImplicitOutputsFunction( new SkylarkImplicitOutputsFunctionWithCallback(callback, ast.getLocation())); } else { builder.setImplicitOutputsFunction( new SkylarkImplicitOutputsFunctionWithMap( ImmutableMap.copyOf( castMap( implicitOutputs, String.class, String.class, "implicit outputs of the rule class")))); } } if (outputToGenfiles) { builder.setOutputToGenfiles(); } builder.requiresConfigurationFragmentsBySkylarkModuleName( fragments.getContents(String.class, "fragments")); ConfigAwareRuleClassBuilder.of(builder) .requiresHostConfigurationFragmentsBySkylarkModuleName( hostFragments.getContents(String.class, "host_fragments")); builder.setConfiguredTargetFunction(implementation); builder.setRuleDefinitionEnvironmentLabelAndHashCode( funcallEnv.getGlobals().getTransitiveLabel(), funcallEnv.getTransitiveContentHashCode()); builder.addRequiredToolchains(collectToolchainLabels(toolchains, ast)); for (Object o : providesArg) { if (!SkylarkAttr.isProvider(o)) { throw new EvalException( ast.getLocation(), String.format( "Illegal argument: element in 'provides' is of unexpected type. " + "Should be list of providers, but got item of type %s.", EvalUtils.getDataTypeName(o, true))); } } for (SkylarkProviderIdentifier skylarkProvider : SkylarkAttr.getSkylarkProviderIdentifiers(providesArg, ast.getLocation())) { builder.advertiseSkylarkProvider(skylarkProvider); } return new SkylarkRuleFunction(builder, type, attributes, ast.getLocation()); } protected static ImmutableList> attrObjectToAttributesList( Object attrs, FuncallExpression ast) throws EvalException { ImmutableList.Builder> attributes = ImmutableList.builder(); if (attrs != Runtime.NONE) { for (Map.Entry attr : castMap(attrs, String.class, Descriptor.class, "attrs").entrySet()) { Descriptor attrDescriptor = attr.getValue(); AttributeValueSource source = attrDescriptor.getValueSource(); String attrName = source.convertToNativeName(attr.getKey(), ast.getLocation()); attributes.add(Pair.of(attrName, attrDescriptor)); } } return attributes.build(); } private static void addAttribute( Location location, RuleClass.Builder builder, Attribute attribute) throws EvalException { try { builder.addOrOverrideAttribute(attribute); } catch (IllegalArgumentException ex) { throw new EvalException(location, ex); } } private static ImmutableList