// 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.BaseRuleClasses; 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.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.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.RuleClass.ExecutionPlatformConstraintsAllowed; 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.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.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, /* defaultToMain=*/ false, /* repositoryMapping= */ ImmutableMap.of()); } 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())) .dontCheckConstraints()) .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) { 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_wrapper", LABEL) .cfg(HostTransition.INSTANCE) .singleArtifact() .value(labelCache.getUnchecked(toolsRepository + "//tools/test:test_wrapper"))) .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("$xml_generator_script", LABEL) .cfg(HostTransition.INSTANCE) .singleArtifact() .value( labelCache.getUnchecked(toolsRepository + "//tools/test:test_xml_generator"))) .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( BaseRuleClasses.coverageSupportAttribute( labelCache.getUnchecked( toolsRepository + BaseRuleClasses.DEFAULT_COVERAGE_SUPPORT_VALUE)))) // Used in the one-per-build coverage report generation action. .add( attr(":coverage_report_generator", LABEL) .cfg(HostTransition.INSTANCE) .value( BaseRuleClasses.coverageReportGeneratorAttribute( labelCache.getUnchecked( toolsRepository + BaseRuleClasses.DEFAULT_COVERAGE_REPORT_GENERATOR_VALUE)))) .add(attr(":run_under", LABEL).value(RUN_UNDER)) .executionPlatformConstraintsAllowed(ExecutionPlatformConstraintsAllowed.PER_TARGET) .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"; } }; @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, Boolean executionPlatformConstraintsAllowed, SkylarkList execCompatibleWith, 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)) : (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.getContents(String.class, "toolchains"), ast.getLocation())); 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); } if (!execCompatibleWith.isEmpty()) { builder.addExecutionPlatformConstraints( collectConstraintLabels( execCompatibleWith.getContents(String.class, "exec_compatile_with"), ast.getLocation())); } if (executionPlatformConstraintsAllowed) { builder.executionPlatformConstraintsAllowed(ExecutionPlatformConstraintsAllowed.PER_TARGET); } 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