blob: a34b817e84dc9a8c450688674fb32d82c0badfa0 [file] [log] [blame]
// 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.starlark;
import com.google.common.base.Preconditions;
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.actions.MutableActionGraph.ActionConflictException;
import com.google.devtools.build.lib.analysis.ActionsProvider;
import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.DefaultInfo;
import com.google.devtools.build.lib.analysis.RequiredConfigFragmentsProvider;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.RunEnvironmentInfo;
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.StarlarkProviderValidationUtil;
import com.google.devtools.build.lib.analysis.test.CoverageCommon;
import com.google.devtools.build.lib.analysis.test.InstrumentedFilesInfo;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.packages.AdvertisedProviderSet;
import com.google.devtools.build.lib.packages.BuiltinProvider;
import com.google.devtools.build.lib.packages.Info;
import com.google.devtools.build.lib.packages.Provider;
import com.google.devtools.build.lib.packages.RuleClass;
import com.google.devtools.build.lib.packages.StarlarkInfo;
import com.google.devtools.build.lib.packages.StarlarkProviderIdentifier;
import com.google.devtools.build.lib.packages.StructImpl;
import com.google.devtools.build.lib.packages.StructProvider;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.packages.Type;
import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.errorprone.annotations.FormatMethod;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import net.starlark.java.eval.Dict;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkValue;
import net.starlark.java.syntax.Location;
/**
* A helper class to build Rule Configured Targets via runtime loaded rule implementations defined
* using the Starlark Build Extension Language.
*/
public final class StarlarkRuleConfiguredTargetUtil {
private StarlarkRuleConfiguredTargetUtil() {}
private static final ImmutableSet<String> DEFAULT_PROVIDER_FIELDS =
ImmutableSet.of("files", "runfiles", "data_runfiles", "default_runfiles", "executable");
/**
* Evaluates the rule's implementation function and returns what it returns (raw providers).
*
* <p>If there were errors during the evaluation or the type of the returned object is obviously
* wrong, it sets ruleErrors on the ruleContext and returns null.
*
* <p>Unchecked exception {@code UncheckedEvalException}s and {@code MissingDepException} may be
* thrown.
*/
// TODO(blaze-team): Legacy providers are preventing to change the return type to Sequence<Info>.
@Nullable
public static Object evalRule(RuleContext ruleContext) throws InterruptedException {
RuleClass ruleClass = ruleContext.getRule().getRuleClassObject();
if (ruleClass.getRuleClassType().equals(RuleClass.Builder.RuleClassType.WORKSPACE)) {
ruleContext.ruleError(
"Found reference to a workspace rule in a context where a build"
+ " rule was expected; probably a reference to a target in that external"
+ " repository, properly specified as @reponame//path/to/package:target,"
+ " should have been specified by the requesting rule.");
return null;
}
// TODO(blaze-team): expect_failure attribute is special for all rule classes, but it should
// be special only for analysis tests
String expectFailure = ruleContext.attributes().get("expect_failure", Type.STRING);
Object providersRaw;
try {
// call rule.implementation(ctx)
providersRaw =
Starlark.fastcall(
ruleContext.getStarlarkThread(),
ruleClass.getConfiguredTargetFunction(),
/* positional= */ new Object[] {ruleContext.getStarlarkRuleContext()},
/* named= */ new Object[0]);
} catch (Starlark.UncheckedEvalException ex) {
// MissingDepException is expected to transit through Starlark execution.
throw ex.getCause() instanceof CachingAnalysisEnvironment.MissingDepException
? (CachingAnalysisEnvironment.MissingDepException) ex.getCause()
: ex;
} catch (EvalException ex) {
// An error occurred during the rule.implementation call
// If the error was expected by an analysis test, return None, to produce an empty target.
if (!expectFailure.isEmpty() && ex.getMessage().matches(expectFailure)) {
return Starlark.NONE;
}
// Emit a single event that spans multiple lines:
// ERROR p/BUILD:1:1: in foo_library rule //p:p:
// Traceback:
// File foo.bzl, line 1, in foo_library_impl:
// ...
ruleContext.ruleError("\n" + ex.getMessageWithStack());
return null;
}
// Errors already reported?
if (ruleContext.hasErrors()) {
return null;
}
// Wrong result type?
if (!(providersRaw instanceof Info
|| providersRaw == Starlark.NONE
|| providersRaw instanceof Iterable)) {
ruleContext.ruleError(
String.format(
"Rule should return a struct or a list, but got %s", Starlark.type(providersRaw)));
return null;
}
// Did the Starlark implementation function fail to fail as expected?
if (!expectFailure.isEmpty()) {
ruleContext.ruleError("Expected failure not found: " + expectFailure);
return null;
}
return providersRaw;
}
private static void checkDeclaredProviders(
ConfiguredTarget configuredTarget, AdvertisedProviderSet advertisedProviders)
throws EvalException {
for (StarlarkProviderIdentifier providerId : advertisedProviders.getStarlarkProviders()) {
if (configuredTarget.get(providerId) == null) {
throw Starlark.errorf(
"rule advertised the '%s' provider, but this provider was not among those returned",
providerId);
}
}
}
/**
* Creates a Rule Configured Target from the raw providers returned by the rule's implementation
* function.
*
* <p>If there are problems with the raw providers, it sets ruleErrors on the ruleContext and
* returns null.
*/
@Nullable
public static ConfiguredTarget createTarget(
RuleContext context,
Object rawProviders,
AdvertisedProviderSet advertisedProviders,
boolean isDefaultExecutableCreated,
@Nullable RequiredConfigFragmentsProvider requiredConfigFragmentsProvider)
throws InterruptedException, ActionConflictException {
RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(context);
// Location of rule.implementation function.
Location implLoc =
context.getRule().getRuleClassObject().getConfiguredTargetFunction().getLocation();
// TODO(adonovan): clean up addProviders' error handling,
// reporting provider validity errors through ruleError
// where possible. This allows for multiple events, with independent
// locations, even for the same root cause.
// The required change is fiddly due to frequent and nested use of
// Structure.getField, Sequence.cast, and similar operators.
try {
addProviders(context, builder, rawProviders, implLoc, isDefaultExecutableCreated);
} catch (EvalException ex) {
// Emit a single event that spans two lines (see infoError).
// The message typically starts with another location, e.g. of provider creation.
// ERROR p/BUILD:1:1: in foo_library rule //p:p:
// ...message...
context.ruleError("\n" + ex.getMessage());
return null;
}
// This provider is kept out of `addProviders` method, because it's not generated by the
// Starlark rule and because `addProviders` will be simplified by the legacy providers removal
// RequiredConfigFragmentsProvider may be removed with removal of Android feature flags.
if (requiredConfigFragmentsProvider != null) {
builder.addProvider(requiredConfigFragmentsProvider);
}
ConfiguredTarget ct;
try {
// This also throws InterruptedException from a convoluted dependency:
// TestActionBuilder -> TestTargetExecutionSettings -> CommandLine -> Starlark.
ct = builder.build(); // may be null
} catch (IllegalArgumentException ex) {
// TODO(adonovan): eliminate this abuse of unchecked exceptions.
// Emit a single event that spans two lines (see infoError).
// The message typically starts with another location, e.g. of provider creation.
// ERROR p/BUILD:1:1: in foo_library rule //p:p:
// ...message...
context.ruleError("\n" + implLoc + ": " + ex.getMessage());
return null;
}
if (ct != null) {
// If there was error creating the ConfiguredTarget, no further validation is needed.
// Null will be returned and the errors thus reported.
try {
// Check all artifacts have actions. Despite signature, must be done after build().
StarlarkProviderValidationUtil.validateArtifacts(context);
// Check all advertised providers were created.
checkDeclaredProviders(ct, advertisedProviders);
} catch (EvalException ex) {
context.ruleError("\n" + implLoc + ": " + ex.getMessage());
return null;
}
}
return ct;
}
private static void addOutputGroups(Object outputGroups, RuleConfiguredTargetBuilder builder)
throws EvalException {
for (Map.Entry<String, StarlarkValue> entry :
Dict.cast(outputGroups, String.class, StarlarkValue.class, "output_groups").entrySet()) {
String outputGroup = entry.getKey();
NestedSet<Artifact> artifacts = convertToOutputGroupValue(outputGroup, entry.getValue());
builder.addOutputGroup(outputGroup, artifacts);
}
}
private static void addInstrumentedFiles(
StructImpl insStruct, RuleContext ruleContext, RuleConfiguredTargetBuilder builder)
throws EvalException {
List<String> extensions = null;
if (insStruct.getFieldNames().contains("extensions")) {
extensions = Sequence.cast(insStruct.getValue("extensions"), String.class, "extensions");
}
List<String> dependencyAttributes = Collections.emptyList();
if (insStruct.getFieldNames().contains("dependency_attributes")) {
dependencyAttributes =
Sequence.cast(
insStruct.getValue("dependency_attributes"), String.class, "dependency_attributes");
}
List<String> sourceAttributes = Collections.emptyList();
if (insStruct.getFieldNames().contains("source_attributes")) {
sourceAttributes =
Sequence.cast(insStruct.getValue("source_attributes"), String.class, "source_attributes");
}
InstrumentedFilesInfo instrumentedFilesProvider =
CoverageCommon.createInstrumentedFilesInfo(
ruleContext, sourceAttributes, dependencyAttributes, extensions);
builder.addNativeDeclaredProvider(instrumentedFilesProvider);
}
public static NestedSet<Artifact> convertToOutputGroupValue(String outputGroup, Object objects)
throws EvalException {
// regrettable preemptive allocation of error message
String what = "output group '" + outputGroup + "'";
return objects instanceof Sequence
? NestedSetBuilder.<Artifact>stableOrder()
.addAll(Sequence.cast(objects, Artifact.class, what))
.build()
: Depset.cast(objects, Artifact.class, what);
}
private static void addProviders(
RuleContext context,
RuleConfiguredTargetBuilder builder,
Object rawProviders,
Location implLoc,
boolean isDefaultExecutableCreated)
throws EvalException, InterruptedException {
StructImpl oldStyleProviders =
StarlarkInfo.create(StructProvider.STRUCT, ImmutableMap.of(), implLoc);
Map<Provider.Key, Info> declaredProviders = new LinkedHashMap<>();
if (rawProviders instanceof Info) {
// Either an old-style struct or a single declared provider (not in a list)
Info info = (Info) rawProviders;
if (getProviderKey(info).equals(StructProvider.STRUCT.getKey())) {
if (context
.getAnalysisEnvironment()
.getStarlarkSemantics()
.getBool(BuildLanguageOptions.INCOMPATIBLE_DISALLOW_STRUCT_PROVIDER_SYNTAX)) {
throw infoError(
info,
"Returning a struct from a rule implementation function is deprecated and will "
+ "be removed soon. It may be temporarily re-enabled by setting "
+ "--incompatible_disallow_struct_provider_syntax=false . See "
+ "https://github.com/bazelbuild/bazel/issues/7347 for details.");
}
// Old-style struct, but it may contain declared providers
StructImpl struct = (StructImpl) rawProviders;
oldStyleProviders = struct;
Object providersField = struct.getValue("providers");
if (providersField != null) {
for (Info provider : Sequence.cast(providersField, Info.class, "providers")) {
Provider.Key providerKey = getProviderKey(provider);
if (declaredProviders.put(providerKey, provider) != null) {
context.ruleError("Multiple conflicting returned providers with key " + providerKey);
}
}
}
} else {
if (info instanceof StarlarkInfo) {
info = ((StarlarkInfo) info).unsafeOptimizeMemoryLayout();
}
Provider.Key providerKey = getProviderKey(info);
// Single declared provider
declaredProviders.put(providerKey, info);
}
} else if (rawProviders instanceof Sequence) {
// Sequence of declared providers
for (Info provider :
Sequence.cast(rawProviders, Info.class, "result of rule implementation function")) {
if (provider instanceof StarlarkInfo) {
// Provider instances are optimised recursively, without optimising elements of the list.
// Tradeoff is that some object may be duplicated if they are reachable by more than one
// path, but we don't expect that much in practice.
provider = ((StarlarkInfo) provider).unsafeOptimizeMemoryLayout();
}
Provider.Key providerKey = getProviderKey(provider);
if (declaredProviders.put(providerKey, provider) != null) {
context.ruleError("Multiple conflicting returned providers with key " + providerKey);
}
}
}
boolean defaultProviderProvidedExplicitly = false;
for (Info declaredProvider : declaredProviders.values()) {
if (getProviderKey(declaredProvider).equals(DefaultInfo.PROVIDER.getKey())) {
parseDefaultProviderFields(
(DefaultInfo) declaredProvider, context, builder, isDefaultExecutableCreated);
defaultProviderProvidedExplicitly = true;
} else if (getProviderKey(declaredProvider).equals(RunEnvironmentInfo.PROVIDER.getKey())
&& !(context.getRule().getRuleClassObject().isExecutableStarlark()
|| context.isTestTarget())) {
String message =
"Returning RunEnvironmentInfo from a non-executable, non-test target has no effect";
RunEnvironmentInfo runEnvironmentInfo = (RunEnvironmentInfo) declaredProvider;
if (runEnvironmentInfo.shouldErrorOnNonExecutableRule()) {
context.ruleError(message);
} else {
context.ruleWarning(message);
builder.addStarlarkDeclaredProvider(declaredProvider);
}
} else {
builder.addStarlarkDeclaredProvider(declaredProvider);
}
}
if (!defaultProviderProvidedExplicitly) {
parseDefaultProviderFields(oldStyleProviders, context, builder, isDefaultExecutableCreated);
}
for (String field : oldStyleProviders.getFieldNames()) {
if (DEFAULT_PROVIDER_FIELDS.contains(field)) {
// These fields have already been parsed above.
// If a default provider has been provided explicitly then it's an error that they also
// occur here.
if (defaultProviderProvidedExplicitly) {
throw infoError(
oldStyleProviders,
"Provider '%s' should be specified in DefaultInfo if it's provided explicitly.",
field);
}
} else if (field.equals("output_groups")) {
addOutputGroups(oldStyleProviders.getValue(field), builder);
} else if (field.equals("instrumented_files")) {
addInstrumentedFiles(
oldStyleProviders.getValue("instrumented_files", StructImpl.class), context, builder);
} else if (!field.equals("providers")) { // "providers" already handled above.
addProviderFromLegacySyntax(
builder, oldStyleProviders, field, oldStyleProviders.getValue(field));
}
}
}
// Returns an EvalException whose message has the info's creation location as a prefix.
// The exception is intended to be reported as ruleErrors by createTarget.
@FormatMethod
private static EvalException infoError(Info info, String format, Object... args) {
return Starlark.errorf("%s: %s", info.getCreationLocation(), String.format(format, args));
}
@SuppressWarnings("deprecation") // For legacy migrations
private static void addProviderFromLegacySyntax(
RuleConfiguredTargetBuilder builder,
StructImpl oldStyleProviders,
String fieldName,
Object value)
throws EvalException {
builder.addStarlarkTransitiveInfo(fieldName, value);
if (value instanceof Info) {
Info info = (Info) value;
// To facilitate migration off legacy provider syntax, implicitly set the modern provider key
// and the canonical legacy provider key if applicable.
if (shouldAddWithModernKey(builder, oldStyleProviders, fieldName, info)) {
builder.addNativeDeclaredProvider(info);
}
if (info.getProvider() instanceof BuiltinProvider.WithLegacyStarlarkName) {
BuiltinProvider.WithLegacyStarlarkName providerWithLegacyName =
(BuiltinProvider.WithLegacyStarlarkName) info.getProvider();
if (shouldAddWithLegacyKey(oldStyleProviders, providerWithLegacyName)) {
builder.addStarlarkTransitiveInfo(providerWithLegacyName.getStarlarkName(), info);
}
}
}
}
@SuppressWarnings("deprecation") // For legacy migrations
private static boolean shouldAddWithModernKey(
RuleConfiguredTargetBuilder builder,
StructImpl oldStyleProviders,
String fieldName,
Info info)
throws EvalException {
// If the modern key is already set, do nothing.
if (builder.containsProviderKey(info.getProvider().getKey())) {
return false;
}
if (info.getProvider() instanceof BuiltinProvider.WithLegacyStarlarkName) {
String canonicalLegacyKey =
((BuiltinProvider.WithLegacyStarlarkName) info.getProvider()).getStarlarkName();
// Add info using its modern key if it was specified using its canonical legacy key, or
// if no provider was used using that canonical legacy key.
return fieldName.equals(canonicalLegacyKey)
|| oldStyleProviders.getValue(canonicalLegacyKey) == null;
} else {
return true;
}
}
@SuppressWarnings("deprecation") // For legacy migrations
private static boolean shouldAddWithLegacyKey(
StructImpl oldStyleProviders, BuiltinProvider.WithLegacyStarlarkName provider)
throws EvalException {
String canonicalLegacyKey = provider.getStarlarkName();
// Add info using its canonical legacy key if no provider was specified using that canonical
// legacy key.
return oldStyleProviders.getValue(canonicalLegacyKey) == null;
}
/**
* Returns the provider key from an info (provider instance).
*
* @throws EvalException if the provider for this info object has not been exported, which can
* occur if the provider was declared in a non-global scope (for example a rule implementation
* function)
*/
private static Provider.Key getProviderKey(Info info) throws EvalException {
Provider provider = info.getProvider();
if (!provider.isExported()) {
// TODO(adonovan): report separate error events at distinct locations:
// "cannot return non-exported provider" (at location of instantiation), and
// "provider definition not at top level" (at location of definition).
throw infoError(
info,
"The rule implementation function returned an instance of an unnamed provider. "
+ "A provider becomes named by being assigned to a global variable in a .bzl file. "
+ "(Provider defined at %s.)",
provider.getLocation());
}
return provider.getKey();
}
/**
* Parses fields of (not necessarily a default) provider. If it is an actual default provider,
* throws an {@link EvalException} if there are unknown fields.
*/
// TODO(brandjon): Consider refactoring this method by splitting it into two versions for the
// case of DefaultInfo and the case of a legacy struct. They'd differ in how they parse the
// fields, then dispatch to a common validation helper and finally the common
// addSimpleProviders call. Also rename this method to make clear that it mutates the builder.
// If lagacy struct providers are removed first, this is moot.
private static void parseDefaultProviderFields(
StructImpl info,
RuleContext context,
RuleConfiguredTargetBuilder builder,
boolean isDefaultExecutableCreated)
throws EvalException, InterruptedException {
Depset files = null;
Runfiles statelessRunfiles = null;
Runfiles dataRunfiles = null;
Runfiles defaultRunfiles = null;
Artifact executable = null;
if (getProviderKey(info).equals(DefaultInfo.PROVIDER.getKey())) {
DefaultInfo defaultInfo = (DefaultInfo) info;
files = defaultInfo.getFiles();
statelessRunfiles = defaultInfo.getStatelessRunfiles();
dataRunfiles = defaultInfo.getDataRunfiles();
defaultRunfiles = defaultInfo.getDefaultRunfiles();
executable = defaultInfo.getExecutable();
} else {
// Rule implementations aren't required to return default-info fields via a DefaultInfo
// provider. They can return them as fields on the returned struct. For example,
// 'return struct(executable = foo)' instead of 'return DefaultInfo(executable = foo)'.
// TODO(cparsons): Look into deprecating this option.
for (String field : info.getFieldNames()) {
if (field.equals("files")) {
Object x = info.getValue("files");
Depset.cast(x, Artifact.class, "files"); // may throw exception
files = (Depset) x;
} else if (field.equals("runfiles")) {
statelessRunfiles = info.getValue("runfiles", Runfiles.class);
} else if (field.equals("data_runfiles")) {
dataRunfiles = info.getValue("data_runfiles", Runfiles.class);
} else if (field.equals("default_runfiles")) {
defaultRunfiles = info.getValue("default_runfiles", Runfiles.class);
} else if (field.equals("executable") && info.getValue("executable") != null) {
executable = info.getValue("executable", Artifact.class);
}
}
if ((statelessRunfiles != null) && (dataRunfiles != null || defaultRunfiles != null)) {
throw infoError(
info,
"Cannot specify the provider 'runfiles' together with 'data_runfiles' or"
+ " 'default_runfiles'");
}
}
if (executable != null && !executable.getArtifactOwner().equals(context.getOwner())) {
throw infoError(
info,
"'executable' provided by an executable rule '%s' should be created "
+ "by the same rule.",
context.getRule().getRuleClass());
}
boolean isExecutable = context.getRule().getRuleClassObject().isExecutableStarlark();
if (executable != null && isExecutable && isDefaultExecutableCreated) {
Artifact defaultExecutable = context.createOutputArtifact();
if (!executable.equals(defaultExecutable)) {
throw infoError(
info,
"The rule '%s' both accesses 'ctx.outputs.executable' and provides "
+ "a different executable '%s'. Do not use 'ctx.output.executable'.",
context.getRule().getRuleClass(),
executable.getRootRelativePathString());
}
}
if (context.getRule().isAnalysisTest()) {
// The Starlark Build API should already throw exception if the rule implementation attempts
// to register any actions. This is just a check of this invariant.
Preconditions.checkState(
context.getAnalysisEnvironment().getRegisteredActions().isEmpty(),
"%s",
context.getLabel());
executable = context.createOutputArtifactScript();
}
if (executable == null && isExecutable) {
if (isDefaultExecutableCreated) {
// This doesn't actually create a new Artifact just returns the one
// created in StarlarkRuleContext.
executable = context.createOutputArtifact();
} else {
throw infoError(
info,
"The rule '%s' is executable. "
+ "It needs to create an executable File and pass it as the 'executable' "
+ "parameter to the DefaultInfo it returns.",
context.getRule().getRuleClass());
}
}
addSimpleProviders(
builder, context, executable, files, statelessRunfiles, dataRunfiles, defaultRunfiles);
}
private static void addSimpleProviders(
RuleConfiguredTargetBuilder builder,
RuleContext ruleContext,
Artifact executable,
@Nullable Depset files,
Runfiles statelessRunfiles,
Runfiles dataRunfiles,
Runfiles defaultRunfiles)
throws EvalException, InterruptedException {
// TODO(bazel-team) if both 'files' and 'executable' are provided, 'files' overrides
// 'executable'
NestedSetBuilder<Artifact> filesToBuild =
NestedSetBuilder.<Artifact>stableOrder().addAll(ruleContext.getOutputArtifacts());
if (executable != null) {
filesToBuild.add(executable);
}
builder.setFilesToBuild(filesToBuild.build());
if (files != null) {
// If we specify files_to_build we don't have the executable in it by default.
builder.setFilesToBuild(Depset.cast(files, Artifact.class, "files"));
}
if (statelessRunfiles == null && dataRunfiles == null && defaultRunfiles == null) {
// No runfiles specified, set default
statelessRunfiles = Runfiles.EMPTY;
}
RunfilesProvider runfilesProvider =
statelessRunfiles != null
? RunfilesProvider.simple(mergeFiles(statelessRunfiles, executable, ruleContext))
: RunfilesProvider.withData(
// The executable doesn't get into the default runfiles if we have runfiles states.
// This is to keep Starlark 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 Starlark.errorf("Test rules have to define runfiles");
}
if (executable != null || testRule) {
RunfilesSupport runfilesSupport = null;
if (!computedDefaultRunfiles.isEmpty()) {
Preconditions.checkNotNull(executable, "executable must not be null");
runfilesSupport =
RunfilesSupport.withExecutable(ruleContext, computedDefaultRunfiles, executable);
assertExecutableSymlinkPresent(runfilesSupport.getRunfiles(), executable);
}
builder.setRunfilesSupport(runfilesSupport, executable);
}
if (ruleContext.getRule().getRuleClassObject().isStarlarkTestable()) {
Info actions =
ActionsProvider.create(ruleContext.getAnalysisEnvironment().getRegisteredActions());
builder.addStarlarkDeclaredProvider(actions);
}
}
private static void assertExecutableSymlinkPresent(Runfiles runfiles, Artifact executable)
throws EvalException {
// Extracting the map from Runfiles flattens a depset.
// TODO(cparsons): Investigate: Avoiding this flattening may be an efficiency win.
Map<PathFragment, Artifact> symlinks = runfiles.asMapWithoutRootSymlinks();
if (!symlinks.containsValue(executable)) {
throw Starlark.errorf("main program %s not included in runfiles", executable);
}
}
private static Runfiles mergeFiles(
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();
}
}