| // 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.rules; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.analysis.ActionsProvider; | |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; | |
| import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder; | |
| import com.google.devtools.build.lib.analysis.RuleContext; | |
| import com.google.devtools.build.lib.analysis.Runfiles; | |
| import com.google.devtools.build.lib.analysis.RunfilesProvider; | |
| import com.google.devtools.build.lib.analysis.RunfilesSupport; | |
| import com.google.devtools.build.lib.analysis.SkylarkProviderValidationUtil; | |
| import com.google.devtools.build.lib.analysis.TransitiveInfoProvider; | |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; | |
| import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; | |
| import com.google.devtools.build.lib.events.Location; | |
| import com.google.devtools.build.lib.packages.Rule; | |
| import com.google.devtools.build.lib.packages.SkylarkClassObject; | |
| import com.google.devtools.build.lib.packages.TargetUtils; | |
| import com.google.devtools.build.lib.rules.SkylarkRuleContext.Kind; | |
| import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector; | |
| import com.google.devtools.build.lib.rules.test.InstrumentedFilesCollector.InstrumentationSpec; | |
| import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider; | |
| import com.google.devtools.build.lib.skylarkinterface.SkylarkValue; | |
| import com.google.devtools.build.lib.syntax.BaseFunction; | |
| import com.google.devtools.build.lib.syntax.ClassObject; | |
| import com.google.devtools.build.lib.syntax.Environment; | |
| import com.google.devtools.build.lib.syntax.EvalException; | |
| import com.google.devtools.build.lib.syntax.EvalExceptionWithStackTrace; | |
| import com.google.devtools.build.lib.syntax.EvalUtils; | |
| import com.google.devtools.build.lib.syntax.Mutability; | |
| import com.google.devtools.build.lib.syntax.Runtime; | |
| import com.google.devtools.build.lib.syntax.SkylarkList; | |
| import com.google.devtools.build.lib.syntax.SkylarkNestedSet; | |
| import com.google.devtools.build.lib.syntax.SkylarkType; | |
| import com.google.devtools.build.lib.syntax.Type; | |
| import com.google.devtools.build.lib.util.FileType; | |
| import com.google.devtools.build.lib.util.FileTypeSet; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import java.util.Map; | |
| /** | |
| * A helper class to build Rule Configured Targets via runtime loaded rule implementations | |
| * defined using the Skylark Build Extension Language. | |
| */ | |
| public final class SkylarkRuleConfiguredTargetBuilder { | |
| /** | |
| * Create a Rule Configured Target from the ruleContext and the ruleImplementation. The | |
| * registeredProviderTypes map indicates which keys in structs returned by skylark rules | |
| * should be interpreted as native TransitiveInfoProvider instances of type (map value). | |
| */ | |
| public static ConfiguredTarget buildRule( | |
| RuleContext ruleContext, | |
| BaseFunction ruleImplementation, | |
| Map<String, Class<? extends TransitiveInfoProvider>> registeredProviderTypes) | |
| throws InterruptedException { | |
| String expectFailure = ruleContext.attributes().get("expect_failure", Type.STRING); | |
| try (Mutability mutability = Mutability.create("configured target")) { | |
| SkylarkRuleContext skylarkRuleContext = new SkylarkRuleContext(ruleContext, Kind.RULE); | |
| Environment env = Environment.builder(mutability) | |
| .setSkylark() | |
| .setCallerLabel(ruleContext.getLabel()) | |
| .setGlobals( | |
| ruleContext.getRule().getRuleClassObject().getRuleDefinitionEnvironment().getGlobals()) | |
| .setEventHandler(ruleContext.getAnalysisEnvironment().getEventHandler()) | |
| .build(); // NB: loading phase functions are not available: this is analysis already, | |
| // so we do *not* setLoadingPhase(). | |
| Object target = ruleImplementation.call( | |
| ImmutableList.<Object>of(skylarkRuleContext), | |
| ImmutableMap.<String, Object>of(), | |
| /*ast=*/null, | |
| env); | |
| if (ruleContext.hasErrors()) { | |
| return null; | |
| } else if ( | |
| !(target instanceof SkylarkClassObject) && target != Runtime.NONE | |
| && !(target instanceof Iterable)) { | |
| ruleContext.ruleError( | |
| String.format( | |
| "Rule should return a return a struct or a list, but got %s", | |
| SkylarkType.typeOf(target))); | |
| return null; | |
| } else if (!expectFailure.isEmpty()) { | |
| ruleContext.ruleError("Expected failure not found: " + expectFailure); | |
| return null; | |
| } | |
| ConfiguredTarget configuredTarget = | |
| createTarget(ruleContext, target, registeredProviderTypes); | |
| SkylarkProviderValidationUtil.checkOrphanArtifacts(ruleContext); | |
| return configuredTarget; | |
| } catch (EvalException e) { | |
| addRuleToStackTrace(e, ruleContext.getRule(), ruleImplementation); | |
| // If the error was expected, return an empty target. | |
| if (!expectFailure.isEmpty() && getMessageWithoutStackTrace(e).matches(expectFailure)) { | |
| return new com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder(ruleContext) | |
| .add(RunfilesProvider.class, RunfilesProvider.EMPTY) | |
| .build(); | |
| } | |
| ruleContext.ruleError("\n" + e.print()); | |
| return null; | |
| } | |
| } | |
| /** | |
| * Adds the given rule to the stack trace of the exception (if there is one). | |
| */ | |
| private static void addRuleToStackTrace(EvalException ex, Rule rule, BaseFunction ruleImpl) { | |
| if (ex instanceof EvalExceptionWithStackTrace) { | |
| ((EvalExceptionWithStackTrace) ex) | |
| .registerPhantomFuncall( | |
| String.format("%s(name = '%s')", rule.getRuleClass(), rule.getName()), | |
| rule.getLocation(), | |
| ruleImpl); | |
| } | |
| } | |
| /** | |
| * Returns the message of the given exception after removing the stack trace, if present. | |
| */ | |
| private static String getMessageWithoutStackTrace(EvalException ex) { | |
| if (ex instanceof EvalExceptionWithStackTrace) { | |
| return ((EvalExceptionWithStackTrace) ex).getOriginalMessage(); | |
| } | |
| return ex.getMessage(); | |
| } | |
| // TODO(bazel-team): this whole defaulting - overriding executable, runfiles and files_to_build | |
| // is getting out of hand. Clean this whole mess up. | |
| private static ConfiguredTarget createTarget( | |
| RuleContext ruleContext, | |
| Object target, | |
| Map<String, Class<? extends TransitiveInfoProvider>> registeredProviderTypes) | |
| throws EvalException { | |
| Artifact executable = getExecutable(ruleContext, target); | |
| RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext); | |
| // Set the default files to build. | |
| NestedSetBuilder<Artifact> filesToBuild = NestedSetBuilder.<Artifact>stableOrder() | |
| .addAll(ruleContext.getOutputArtifacts()); | |
| if (executable != null) { | |
| filesToBuild.add(executable); | |
| } | |
| builder.setFilesToBuild(filesToBuild.build()); | |
| return addStructFieldsAndBuild( | |
| ruleContext, builder, target, executable, registeredProviderTypes); | |
| } | |
| private static Artifact getExecutable(RuleContext ruleContext, Object target) | |
| throws EvalException { | |
| Artifact executable = ruleContext.getRule().getRuleClassObject().outputsDefaultExecutable() | |
| // This doesn't actually create a new Artifact just returns the one | |
| // created in SkylarkruleContext. | |
| ? ruleContext.createOutputArtifact() : null; | |
| if (target instanceof SkylarkClassObject) { | |
| SkylarkClassObject struct = (SkylarkClassObject) target; | |
| if (struct.getValue("executable") != null) { | |
| // We need this because of genrule.bzl. This overrides the default executable. | |
| executable = cast("executable", struct, Artifact.class, struct.getCreationLoc()); | |
| } | |
| } | |
| return executable; | |
| } | |
| private static void addOutputGroups(Object value, Location loc, | |
| RuleConfiguredTargetBuilder builder) | |
| throws EvalException { | |
| Map<String, SkylarkValue> outputGroups = | |
| SkylarkType.castMap(value, String.class, SkylarkValue.class, "output_groups"); | |
| for (String outputGroup : outputGroups.keySet()) { | |
| SkylarkValue objects = outputGroups.get(outputGroup); | |
| NestedSet<Artifact> artifacts; | |
| String typeErrorMessage = | |
| "Output group '%s' is of unexpected type. " | |
| + "Should be list or set of Files, but got '%s' instead."; | |
| if (objects instanceof SkylarkList) { | |
| NestedSetBuilder<Artifact> nestedSetBuilder = NestedSetBuilder.stableOrder(); | |
| for (Object o : (SkylarkList) objects) { | |
| if (o instanceof Artifact) { | |
| nestedSetBuilder.add((Artifact) o); | |
| } else { | |
| throw new EvalException( | |
| loc, | |
| String.format( | |
| typeErrorMessage, | |
| outputGroup, | |
| "list with an element of " + EvalUtils.getDataTypeNameFromClass(o.getClass()))); | |
| } | |
| } | |
| artifacts = nestedSetBuilder.build(); | |
| } else { | |
| artifacts = | |
| SkylarkType.cast( | |
| objects, | |
| SkylarkNestedSet.class, | |
| Artifact.class, | |
| loc, | |
| typeErrorMessage, | |
| outputGroup, | |
| EvalUtils.getDataTypeName(objects, true)) | |
| .getSet(Artifact.class); | |
| } | |
| builder.addOutputGroup(outputGroup, artifacts); | |
| } | |
| } | |
| private static ConfiguredTarget addStructFieldsAndBuild( | |
| RuleContext ruleContext, | |
| RuleConfiguredTargetBuilder builder, | |
| Object target, | |
| Artifact executable, | |
| Map<String, Class<? extends TransitiveInfoProvider>> registeredProviderTypes) | |
| throws EvalException { | |
| Location loc = null; | |
| Runfiles statelessRunfiles = null; | |
| Runfiles dataRunfiles = null; | |
| Runfiles defaultRunfiles = null; | |
| if (target instanceof SkylarkClassObject) { | |
| SkylarkClassObject struct = (SkylarkClassObject) target; | |
| loc = struct.getCreationLoc(); | |
| for (String key : struct.getKeys()) { | |
| if (key.equals("files")) { | |
| // If we specify files_to_build we don't have the executable in it by default. | |
| builder.setFilesToBuild(cast("files", struct, SkylarkNestedSet.class, Artifact.class, loc) | |
| .getSet(Artifact.class)); | |
| } else if (key.equals("runfiles")) { | |
| statelessRunfiles = cast("runfiles", struct, Runfiles.class, loc); | |
| } else if (key.equals("data_runfiles")) { | |
| dataRunfiles = cast("data_runfiles", struct, Runfiles.class, loc); | |
| } else if (key.equals("default_runfiles")) { | |
| defaultRunfiles = cast("default_runfiles", struct, Runfiles.class, loc); | |
| } else if (key.equals("output_groups")) { | |
| addOutputGroups(struct.getValue(key), loc, builder); | |
| } else if (key.equals("instrumented_files")) { | |
| SkylarkClassObject insStruct = | |
| cast("instrumented_files", struct, SkylarkClassObject.class, loc); | |
| Location insLoc = insStruct.getCreationLoc(); | |
| FileTypeSet fileTypeSet = FileTypeSet.ANY_FILE; | |
| if (insStruct.getKeys().contains("extensions")) { | |
| @SuppressWarnings("unchecked") | |
| List<String> exts = cast( | |
| "extensions", insStruct, SkylarkList.class, String.class, insLoc); | |
| if (exts.isEmpty()) { | |
| fileTypeSet = FileTypeSet.NO_FILE; | |
| } else { | |
| FileType[] fileTypes = new FileType[exts.size()]; | |
| for (int i = 0; i < fileTypes.length; i++) { | |
| fileTypes[i] = FileType.of(exts.get(i)); | |
| } | |
| fileTypeSet = FileTypeSet.of(fileTypes); | |
| } | |
| } | |
| List<String> dependencyAttributes = Collections.emptyList(); | |
| if (insStruct.getKeys().contains("dependency_attributes")) { | |
| dependencyAttributes = | |
| cast("dependency_attributes", insStruct, SkylarkList.class, String.class, insLoc); | |
| } | |
| List<String> sourceAttributes = Collections.emptyList(); | |
| if (insStruct.getKeys().contains("source_attributes")) { | |
| sourceAttributes = | |
| cast("source_attributes", insStruct, SkylarkList.class, String.class, insLoc); | |
| } | |
| InstrumentationSpec instrumentationSpec = | |
| new InstrumentationSpec(fileTypeSet) | |
| .withSourceAttributes(sourceAttributes.toArray(new String[0])) | |
| .withDependencyAttributes(dependencyAttributes.toArray(new String[0])); | |
| InstrumentedFilesProvider instrumentedFilesProvider = | |
| InstrumentedFilesCollector.collect( | |
| ruleContext, | |
| instrumentationSpec, | |
| InstrumentedFilesCollector.NO_METADATA_COLLECTOR, | |
| Collections.<Artifact>emptySet()); | |
| builder.addProvider(InstrumentedFilesProvider.class, instrumentedFilesProvider); | |
| } else if (registeredProviderTypes.containsKey(key)) { | |
| Class<? extends TransitiveInfoProvider> providerType = registeredProviderTypes.get(key); | |
| TransitiveInfoProvider provider = cast(key, struct, providerType, loc); | |
| builder.addProvider(providerType, provider); | |
| } else if (key.equals("providers")) { | |
| Iterable iterable = cast(key, struct, Iterable.class, loc); | |
| for (Object o : iterable) { | |
| SkylarkClassObject declaredProvider = SkylarkType.cast(o, SkylarkClassObject.class, loc, | |
| "The value of 'providers' should be a sequence of declared providers"); | |
| builder.addSkylarkDeclaredProvider(declaredProvider, loc); | |
| } | |
| } else if (!key.equals("executable")) { | |
| // We handled executable already. | |
| builder.addSkylarkTransitiveInfo(key, struct.getValue(key), loc); | |
| } | |
| } | |
| } else if (target instanceof Iterable) { | |
| loc = ruleContext.getRule().getRuleClassObject().getConfiguredTargetFunction().getLocation(); | |
| for (Object o : (Iterable) target) { | |
| SkylarkClassObject declaredProvider = SkylarkType.cast(o, SkylarkClassObject.class, loc, | |
| "A return value of rule implementation function should be " | |
| + "a sequence of declared providers"); | |
| builder.addSkylarkDeclaredProvider(declaredProvider, declaredProvider.getCreationLoc()); | |
| } | |
| } | |
| if ((statelessRunfiles != null) && (dataRunfiles != null || defaultRunfiles != null)) { | |
| throw new EvalException(loc, "Cannot specify the provider 'runfiles' " | |
| + "together with 'data_runfiles' or 'default_runfiles'"); | |
| } | |
| if (statelessRunfiles == null && dataRunfiles == null && defaultRunfiles == null) { | |
| // No runfiles specified, set default | |
| statelessRunfiles = Runfiles.EMPTY; | |
| } | |
| RunfilesProvider runfilesProvider = statelessRunfiles != null | |
| ? RunfilesProvider.simple(merge(statelessRunfiles, executable, ruleContext)) | |
| : RunfilesProvider.withData( | |
| // The executable doesn't get into the default runfiles if we have runfiles states. | |
| // This is to keep skylark genrule consistent with the original genrule. | |
| defaultRunfiles != null ? defaultRunfiles : Runfiles.EMPTY, | |
| dataRunfiles != null ? dataRunfiles : Runfiles.EMPTY); | |
| builder.addProvider(RunfilesProvider.class, runfilesProvider); | |
| Runfiles computedDefaultRunfiles = runfilesProvider.getDefaultRunfiles(); | |
| // This works because we only allowed to call a rule *_test iff it's a test type rule. | |
| boolean testRule = TargetUtils.isTestRuleName(ruleContext.getRule().getRuleClass()); | |
| if (testRule && computedDefaultRunfiles.isEmpty()) { | |
| throw new EvalException(loc, "Test rules have to define runfiles"); | |
| } | |
| if (executable != null || testRule) { | |
| RunfilesSupport runfilesSupport = computedDefaultRunfiles.isEmpty() | |
| ? null : RunfilesSupport.withExecutable(ruleContext, computedDefaultRunfiles, executable); | |
| builder.setRunfilesSupport(runfilesSupport, executable); | |
| } | |
| if (ruleContext.getRule().getRuleClassObject().isSkylarkTestable()) { | |
| SkylarkClassObject actions = ActionsProvider.create( | |
| ruleContext.getAnalysisEnvironment().getRegisteredActions()); | |
| builder.addSkylarkDeclaredProvider(actions, loc); | |
| } | |
| try { | |
| return builder.build(); | |
| } catch (IllegalArgumentException e) { | |
| throw new EvalException(loc, e.getMessage()); | |
| } | |
| } | |
| private static <T> T cast(String paramName, ClassObject struct, Class<T> expectedGenericType, | |
| Class<?> expectedArgumentType, Location loc) throws EvalException { | |
| Object value = struct.getValue(paramName); | |
| return SkylarkType.cast(value, expectedGenericType, expectedArgumentType, loc, | |
| "expected %s for '%s' but got %s instead: %s", | |
| SkylarkType.of(expectedGenericType, expectedArgumentType), | |
| paramName, EvalUtils.getDataTypeName(value, true), value); | |
| } | |
| private static <T> T cast(String paramName, ClassObject struct, Class<T> expectedType, | |
| Location loc) throws EvalException { | |
| Object value = struct.getValue(paramName); | |
| return SkylarkType.cast(value, expectedType, loc, | |
| "expected %s for '%s' but got %s instead: %s", | |
| SkylarkType.of(expectedType), | |
| paramName, EvalUtils.getDataTypeName(value, false), value); | |
| } | |
| private static Runfiles merge(Runfiles runfiles, Artifact executable, RuleContext ruleContext) { | |
| if (executable == null) { | |
| return runfiles; | |
| } | |
| return new Runfiles.Builder( | |
| ruleContext.getWorkspaceName(), ruleContext.getConfiguration().legacyExternalRunfiles()) | |
| .addArtifact(executable) | |
| .merge(runfiles).build(); | |
| } | |
| } |