aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar dslomov <dslomov@google.com>2017-10-23 19:14:02 +0200
committerGravatar Dmitry Lomov <dslomov@google.com>2017-10-24 10:39:41 +0200
commit2ce9844612e120b099369d923e132d6c9c209854 (patch)
tree33f6b53b5008926fdcd3b847c77df19d941630bf
parent98cd82cbdcac7c48164a611c5a9aa8fc2f1720ef (diff)
Implement custom executable API.
Design is https://github.com/bazelbuild/bazel/issues/3826, specifically https://github.com/bazelbuild/bazel/issues/3826#issuecomment-336220897. Fixes #3826. RELNOTES: ctx.outputs.executable is deprecated. Use DefaultInfo(executable = ...) instead. PiperOrigin-RevId: 173132066
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java3
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java64
-rw-r--r--src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java169
-rw-r--r--src/main/java/com/google/devtools/build/lib/packages/RuleClass.java18
-rw-r--r--src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java211
-rwxr-xr-xsrc/test/shell/integration/run_test.sh40
6 files changed, 448 insertions, 57 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
index 2ed3cc3a84..b47fb3bc6f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
@@ -222,6 +222,7 @@ public class SkylarkRuleClassFunctions {
"A provider that is provided by every rule, even if it is not returned explicitly. "
+ "A <code>DefaultInfo</code> accepts the following parameters:"
+ "<ul>"
+ + "<li><code>executable</code></li>"
+ "<li><code>files</code></li>"
+ "<li><code>runfiles</code></li>"
+ "<li><code>data_runfiles</code></li>"
@@ -515,7 +516,7 @@ public class SkylarkRuleClassFunctions {
.value(true)
.nonconfigurable("Called from RunCommand.isExecutable, which takes a Target")
.build());
- builder.setOutputsDefaultExecutable();
+ builder.setExecutableSkylark();
}
if (implicitOutputs != Runtime.NONE) {
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java
index a04501889e..deefbcb972 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleConfiguredTargetUtil.java
@@ -108,14 +108,14 @@ public final class SkylarkRuleConfiguredTargetUtil {
ruleContext.ruleError("Expected failure not found: " + expectFailure);
return null;
}
- ConfiguredTarget configuredTarget = createTarget(ruleContext, target);
+ ConfiguredTarget configuredTarget = createTarget(skylarkRuleContext, target);
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)
+ return new RuleConfiguredTargetBuilder(ruleContext)
.add(RunfilesProvider.class, RunfilesProvider.EMPTY)
.build();
}
@@ -151,14 +151,19 @@ public final class SkylarkRuleConfiguredTargetUtil {
return ex.getMessage();
}
- private static ConfiguredTarget createTarget(RuleContext ruleContext, Object target)
+ private static ConfiguredTarget createTarget(SkylarkRuleContext context, Object target)
throws EvalException {
- RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext);
+ RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(
+ context.getRuleContext());
// Set the default files to build.
Location loc =
- ruleContext.getRule().getRuleClassObject().getConfiguredTargetFunction().getLocation();
- addProviders(ruleContext, builder, target, loc);
+ context.getRuleContext()
+ .getRule()
+ .getRuleClassObject()
+ .getConfiguredTargetFunction()
+ .getLocation();
+ addProviders(context, builder, target, loc);
try {
return builder.build();
@@ -260,7 +265,7 @@ public final class SkylarkRuleConfiguredTargetUtil {
}
private static void addProviders(
- RuleContext ruleContext, RuleConfiguredTargetBuilder builder, Object target, Location loc)
+ SkylarkRuleContext context, RuleConfiguredTargetBuilder builder, Object target, Location loc)
throws EvalException {
Info oldStyleProviders = NativeProvider.STRUCT.create(loc);
@@ -312,7 +317,7 @@ public final class SkylarkRuleConfiguredTargetUtil {
.getProvider()
.getKey()
.equals(DefaultInfo.PROVIDER.getKey())) {
- parseDefaultProviderKeys(declaredProvider, ruleContext, builder);
+ parseDefaultProviderKeys(declaredProvider, context, builder);
defaultProviderProvidedExplicitly = true;
} else {
Location creationLoc = declaredProvider.getCreationLocOrNull();
@@ -322,7 +327,7 @@ public final class SkylarkRuleConfiguredTargetUtil {
}
if (!defaultProviderProvidedExplicitly) {
- parseDefaultProviderKeys(oldStyleProviders, ruleContext, builder);
+ parseDefaultProviderKeys(oldStyleProviders, context, builder);
}
for (String key : oldStyleProviders.getKeys()) {
@@ -341,7 +346,7 @@ public final class SkylarkRuleConfiguredTargetUtil {
addOutputGroups(oldStyleProviders.getValue(key), loc, builder);
} else if (key.equals("instrumented_files")) {
Info insStruct = cast("instrumented_files", oldStyleProviders, Info.class, loc);
- addInstrumentedFiles(insStruct, ruleContext, builder);
+ addInstrumentedFiles(insStruct, context.getRuleContext(), builder);
} else if (isNativeDeclaredProviderWithLegacySkylarkName(oldStyleProviders.getValue(key))) {
builder.addNativeDeclaredProvider((Info) oldStyleProviders.getValue(key));
} else if (!key.equals("providers")) {
@@ -363,18 +368,13 @@ public final class SkylarkRuleConfiguredTargetUtil {
* throws an {@link EvalException} if there are unknown keys.
*/
private static void parseDefaultProviderKeys(
- Info provider, RuleContext ruleContext, RuleConfiguredTargetBuilder builder)
+ Info provider, SkylarkRuleContext context, RuleConfiguredTargetBuilder builder)
throws EvalException {
SkylarkNestedSet files = null;
Runfiles statelessRunfiles = null;
Runfiles dataRunfiles = null;
Runfiles defaultRunfiles = null;
- Artifact executable =
- ruleContext.getRule().getRuleClassObject().outputsDefaultExecutable()
- // This doesn't actually create a new Artifact just returns the one
- // created in SkylarkRuleContext.
- ? ruleContext.createOutputArtifact()
- : null;
+ Artifact executable = null;
Location loc = provider.getCreationLoc();
@@ -397,9 +397,37 @@ public final class SkylarkRuleConfiguredTargetUtil {
throw new EvalException(loc, "Invalid key for default provider: " + key);
}
}
+
+ if (executable != null && context.isExecutable() && context.isDefaultExecutableCreated()) {
+ Artifact defaultExecutable = context.getRuleContext().createOutputArtifact();
+ if (!executable.equals(defaultExecutable)) {
+ throw new EvalException(loc,
+ String.format(
+ "The rule '%s' both accesses 'ctx.outputs.executable' and provides "
+ + "a different executable '%s'. Do not use 'ctx.output.executable'.",
+ context.getRuleContext().getRule().getRuleClass(),
+ executable.getRootRelativePathString())
+ );
+ }
+ }
+
+ if (executable == null && context.isExecutable()) {
+ if (context.isDefaultExecutableCreated()) {
+ // This doesn't actually create a new Artifact just returns the one
+ // created in SkylarkRuleContext.
+ executable = context.getRuleContext().createOutputArtifact();
+ } else {
+ throw new EvalException(loc,
+ String.format("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.getRuleContext().getRule().getRuleClass()));
+ }
+ }
+
addSimpleProviders(
builder,
- ruleContext,
+ context.getRuleContext(),
loc,
executable,
files,
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
index 9c7c5b7acb..dfccf8dd9a 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
@@ -16,11 +16,13 @@ package com.google.devtools.build.lib.analysis.skylark;
import com.google.common.base.Function;
import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Root;
import com.google.devtools.build.lib.analysis.ActionsProvider;
@@ -62,6 +64,7 @@ import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
+import com.google.devtools.build.lib.syntax.ClassObject;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.FuncallExpression.FuncallException;
import com.google.devtools.build.lib.syntax.Runtime;
@@ -146,10 +149,9 @@ public final class SkylarkRuleContext implements SkylarkValue {
+ "attr struct, but their values will be single lists with all the branches of the split "
+ "merged together.";
public static final String OUTPUTS_DOC =
- "A <code>struct</code> containing all the output files."
- + " The struct is generated the following way:<br>"
- + "<ul><li>If the rule is marked as <code>executable=True</code> the struct has an "
- + "\"executable\" field with the rules default executable <code>file</code> value."
+ "A pseudo-struct containing all the pre-declared output files."
+ + " It is generated the following way:<br>"
+ + "<ul>" + ""
+ "<li>For every entry in the rule's <code>outputs</code> dict an attr is generated with "
+ "the same name and the corresponding <code>file</code> value."
+ "<li>For every output type attribute a struct attribute is generated with the "
@@ -157,7 +159,14 @@ public final class SkylarkRuleContext implements SkylarkValue {
+ "if no value is specified in the rule."
+ "<li>For every output list type attribute a struct attribute is generated with the "
+ "same name and corresponding <code>list</code> of <code>file</code>s value "
- + "(an empty list if no value is specified in the rule).</ul>";
+ + "(an empty list if no value is specified in the rule).</li>"
+ + "<li>DEPRECATED: If the rule is marked as <code>executable=True</code>, a field "
+ + "\"executable\" can be accessed. That will declare the rule's default executable "
+ + "<code>File</code> value. The recommended alternative is to declare an executable with "
+ + "<a href=\"actions.html#declare_file\"><code>ctx.actions.declare_file</code></a> "
+ + "and return it as the <code>executable</code> field of the rule's "
+ + "<a href=\"globals.html#DefaultInfo\"><code>DefaultInfo</code></a> provider."
+ + "</ul>";
public static final Function<Attribute, Object> ATTRIBUTE_VALUE_EXTRACTOR_FOR_ASPECT =
new Function<Attribute, Object>() {
@Nullable
@@ -166,6 +175,7 @@ public final class SkylarkRuleContext implements SkylarkValue {
return attribute.getDefaultValue(null);
}
};
+ public static final String EXECUTABLE_OUTPUT_NAME = "executable";
// This field is a copy of the info from ruleContext, stored separately so it can be accessed
// after this object has been nullified.
@@ -190,7 +200,7 @@ public final class SkylarkRuleContext implements SkylarkValue {
// TODO(bazel-team): we only need this because of the css_binary rule.
private ImmutableMap<Artifact, Label> artifactsLabelMap;
- private Info outputsObject;
+ private Outputs outputsObject;
/**
* Creates a new SkylarkRuleContext using ruleContext.
@@ -213,10 +223,8 @@ public final class SkylarkRuleContext implements SkylarkValue {
if (aspectDescriptor == null) {
this.isForAspect = false;
Collection<Attribute> attributes = ruleContext.getRule().getAttributes();
- HashMap<String, Object> outputsBuilder = new HashMap<>();
- if (ruleContext.getRule().getRuleClassObject().outputsDefaultExecutable()) {
- addOutput(outputsBuilder, "executable", ruleContext.createOutputArtifact());
- }
+ Outputs outputs = new Outputs(this);
+
ImplicitOutputsFunction implicitOutputsFunction =
ruleContext.getRule().getImplicitOutputsFunction();
@@ -225,8 +233,7 @@ public final class SkylarkRuleContext implements SkylarkValue {
(SkylarkImplicitOutputsFunction) implicitOutputsFunction;
for (Map.Entry<String, String> entry :
func.calculateOutputs(RawAttributeMapper.of(ruleContext.getRule())).entrySet()) {
- addOutput(
- outputsBuilder,
+ outputs.addOutput(
entry.getKey(),
ruleContext.getImplicitOutputArtifact(entry.getValue()));
}
@@ -249,12 +256,12 @@ public final class SkylarkRuleContext implements SkylarkValue {
if (type == BuildType.OUTPUT) {
if (artifacts.size() == 1) {
- addOutput(outputsBuilder, attrName, Iterables.getOnlyElement(artifacts));
+ outputs.addOutput(attrName, Iterables.getOnlyElement(artifacts));
} else {
- addOutput(outputsBuilder, attrName, Runtime.NONE);
+ outputs.addOutput(attrName, Runtime.NONE);
}
} else if (type == BuildType.OUTPUT_LIST) {
- addOutput(outputsBuilder, attrName, SkylarkList.createImmutable(artifacts));
+ outputs.addOutput(attrName, SkylarkList.createImmutable(artifacts));
} else {
throw new IllegalArgumentException(
"Type of " + attrName + "(" + type + ") is not output type ");
@@ -262,10 +269,7 @@ public final class SkylarkRuleContext implements SkylarkValue {
}
this.artifactsLabelMap = artifactLabelMapBuilder.build();
- this.outputsObject =
- NativeProvider.STRUCT.create(
- outputsBuilder,
- "No attribute '%s' in outputs. Make sure you declared a rule output with this name.");
+ this.outputsObject = outputs;
this.attributesCollection =
buildAttributesCollection(
@@ -295,6 +299,123 @@ public final class SkylarkRuleContext implements SkylarkValue {
}
/**
+ * Represents `ctx.outputs`.
+ *
+ * <p>A {@link ClassObject} (struct-like data structure) with "executable" field created
+ * lazily on-demand.
+ *
+ * <p>Note: There is only one {@code Outputs} object per rule context, so default
+ * (object identity) equals and hashCode suffice.
+ */
+ private static class Outputs implements ClassObject, SkylarkValue {
+ private final Map<String, Object> outputs;
+ private final SkylarkRuleContext context;
+ private boolean executableCreated = false;
+
+ public Outputs(SkylarkRuleContext context) {
+ this.outputs = new LinkedHashMap<>();
+ this.context = context;
+ }
+
+ private void addOutput(String key, Object value)
+ throws EvalException {
+ Preconditions.checkState(!context.isImmutable(),
+ "Cannot add outputs to immutable Outputs object");
+ if (outputs.containsKey(key)
+ || (context.isExecutable() && EXECUTABLE_OUTPUT_NAME.equals(key))) {
+ throw new EvalException(null, "Multiple outputs with the same key: " + key);
+ }
+ outputs.put(key, value);
+ }
+
+
+ @Override
+ public boolean isImmutable() {
+ return context.isImmutable();
+ }
+
+ @Override
+ public ImmutableCollection<String> getKeys() throws EvalException {
+ checkMutable();
+ ImmutableList.Builder<String> result = ImmutableList.builder();
+ if (context.isExecutable() && executableCreated) {
+ result.add(EXECUTABLE_OUTPUT_NAME);
+ }
+ result.addAll(outputs.keySet());
+ return result.build();
+ }
+
+ @Nullable
+ @Override
+ public Object getValue(String name) throws EvalException {
+ checkMutable();
+ if (context.isExecutable() && EXECUTABLE_OUTPUT_NAME.equals(name)) {
+ executableCreated = true;
+ // createOutputArtifact() will cache the created artifact.
+ return context.ruleContext.createOutputArtifact();
+ }
+
+ return outputs.get(name);
+ }
+
+ @Nullable
+ @Override
+ public String errorMessage(String name) {
+ return String.format(
+ "No attribute '%s' in outputs. Make sure you declared a rule output with this name.",
+ name);
+ }
+
+ @Override
+ public void repr(SkylarkPrinter printer) {
+ if (isImmutable()) {
+ printer.append("ctx.outputs(for ");
+ printer.append(context.ruleLabelCanonicalName);
+ printer.append(")");
+ return;
+ }
+ boolean first = true;
+ printer.append("ctx.outputs(");
+ // Sort by key to ensure deterministic output.
+ try {
+ for (String key : Ordering.natural().sortedCopy(getKeys())) {
+ if (!first) {
+ printer.append(", ");
+ }
+ first = false;
+ printer.append(key);
+ printer.append(" = ");
+ printer.repr(getValue(key));
+ }
+ printer.append(")");
+ } catch (EvalException e) {
+ throw new AssertionError("mutable ctx.outputs should not throw", e);
+ }
+ }
+
+ private void checkMutable() throws EvalException {
+ if (isImmutable()) {
+ throw new EvalException(
+ null,
+ String.format(
+ "cannot access outputs of rule '%s' outside of its own "
+ + "rule implementation function",
+ context.ruleLabelCanonicalName));
+ }
+ }
+
+ }
+
+ public boolean isExecutable() {
+ return ruleContext.getRule().getRuleClassObject().isExecutableSkylark();
+ }
+
+ public boolean isDefaultExecutableCreated() {
+ return this.outputsObject.executableCreated;
+ }
+
+
+ /**
* Nullifies fields of the object when it's not supposed to be used anymore to free unused memory
* and to make sure this object is not accessed when it's not supposed to (after the corresponding
* rule implementation function has exited).
@@ -584,14 +705,6 @@ public final class SkylarkRuleContext implements SkylarkValue {
}
}
- private void addOutput(HashMap<String, Object> outputsBuilder, String key, Object value)
- throws EvalException {
- if (outputsBuilder.containsKey(key)) {
- throw new EvalException(null, "Multiple outputs with the same key: " + key);
- }
- outputsBuilder.put(key, value);
- }
-
@Override
public boolean isImmutable() {
return ruleContext == null;
@@ -787,7 +900,7 @@ public final class SkylarkRuleContext implements SkylarkValue {
}
@SkylarkCallable(structField = true, doc = OUTPUTS_DOC)
- public Info outputs() throws EvalException {
+ public ClassObject outputs() throws EvalException {
checkMutable("outputs");
if (outputsObject == null) {
throw new EvalException(Location.BUILTIN, "'outputs' is not defined");
diff --git a/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java b/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
index a031f2c748..7dd8acba2d 100644
--- a/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
+++ b/src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
@@ -462,7 +462,7 @@ public class RuleClass {
private boolean publicByDefault = false;
private boolean binaryOutput = true;
private boolean workspaceOnly = false;
- private boolean outputsDefaultExecutable = false;
+ private boolean isExecutableSkylark = false;
private boolean isConfigMatcher = false;
private ImplicitOutputsFunction implicitOutputsFunction = ImplicitOutputsFunction.NONE;
private RuleTransitionFactory transitionFactory;
@@ -582,7 +582,7 @@ public class RuleClass {
publicByDefault,
binaryOutput,
workspaceOnly,
- outputsDefaultExecutable,
+ isExecutableSkylark,
implicitOutputsFunction,
isConfigMatcher,
transitionFactory,
@@ -929,8 +929,8 @@ public class RuleClass {
* This rule class outputs a default executable for every rule with the same name as
* the rules's. Only works for Skylark.
*/
- public <TYPE> Builder setOutputsDefaultExecutable() {
- this.outputsDefaultExecutable = true;
+ public <TYPE> Builder setExecutableSkylark() {
+ this.isExecutableSkylark = true;
return this;
}
@@ -1042,7 +1042,7 @@ public class RuleClass {
private final boolean publicByDefault;
private final boolean binaryOutput;
private final boolean workspaceOnly;
- private final boolean outputsDefaultExecutable;
+ private final boolean isExecutableSkylark;
private final boolean isConfigMatcher;
/**
@@ -1164,7 +1164,7 @@ public class RuleClass {
boolean publicByDefault,
boolean binaryOutput,
boolean workspaceOnly,
- boolean outputsDefaultExecutable,
+ boolean isExecutableSkylark,
ImplicitOutputsFunction implicitOutputsFunction,
boolean isConfigMatcher,
RuleTransitionFactory transitionFactory,
@@ -1207,7 +1207,7 @@ public class RuleClass {
validateNoClashInPublicNames(attributes);
this.attributes = ImmutableList.copyOf(attributes);
this.workspaceOnly = workspaceOnly;
- this.outputsDefaultExecutable = outputsDefaultExecutable;
+ this.isExecutableSkylark = isExecutableSkylark;
this.configurationFragmentPolicy = configurationFragmentPolicy;
this.supportsConstraintChecking = supportsConstraintChecking;
this.requiredToolchains = ImmutableSet.copyOf(requiredToolchains);
@@ -2021,8 +2021,8 @@ public class RuleClass {
/**
* Returns true if this rule class outputs a default executable for every rule.
*/
- public boolean outputsDefaultExecutable() {
- return outputsDefaultExecutable;
+ public boolean isExecutableSkylark() {
+ return isExecutableSkylark;
}
public ImmutableSet<Label> getRequiredToolchains() {
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
index 2aa78fa5cd..3b8c91eeec 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkIntegrationTest.java
@@ -24,6 +24,7 @@ import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.FileProvider;
+import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.OutputGroupProvider;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.configuredtargets.FileConfiguredTarget;
@@ -37,6 +38,7 @@ import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
import com.google.devtools.build.lib.packages.Info;
import com.google.devtools.build.lib.packages.Provider;
import com.google.devtools.build.lib.packages.SkylarkProvider;
+import com.google.devtools.build.lib.packages.SkylarkProvider.SkylarkKey;
import com.google.devtools.build.lib.skyframe.PackageFunction;
import com.google.devtools.build.lib.skyframe.SkyFunctions;
import com.google.devtools.build.lib.skyframe.SkylarkImportLookupFunction;
@@ -1328,6 +1330,214 @@ public class SkylarkIntegrationTest extends BuildViewTestCase {
assertContainsEvent("integer division by zero");
}
+ @Test
+ public void testOutputsObjectOrphanExecutableReportError() throws Exception {
+ scratch.file(
+ "test/rule.bzl",
+ "def _impl(ctx):",
+ " o = ctx.outputs.executable",
+ " return [DefaultInfo(executable = o)]",
+ "my_rule = rule(_impl, executable = True)"
+ );
+
+ scratch.file(
+ "test/BUILD",
+ "load(':rule.bzl', 'my_rule')",
+ "my_rule(name = 'xxx')"
+ );
+
+ reporter.removeHandler(failFastHandler);
+ getConfiguredTarget("//test:xxx");
+ assertContainsEvent("ERROR /workspace/test/BUILD:2:1: in my_rule rule //test:xxx: ");
+ assertContainsEvent("The following files have no generating action:");
+ assertContainsEvent("test/xxx");
+ }
+
+ @Test
+ public void testCustomExecutableUsed() throws Exception {
+ scratch.file(
+ "test/rule.bzl",
+ "def _impl(ctx):",
+ " o = ctx.actions.declare_file('x.sh')",
+ " ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+ " return [DefaultInfo(executable = o)]",
+ "my_rule = rule(_impl, executable = True)"
+ );
+
+ scratch.file(
+ "test/BUILD",
+ "load(':rule.bzl', 'my_rule')",
+ "my_rule(name = 'xxx')"
+ );
+
+ ConfiguredTarget configuredTarget = getConfiguredTarget("//test:xxx");
+ Artifact executable = configuredTarget.getProvider(FilesToRunProvider.class).getExecutable();
+ assertThat(executable.getRootRelativePathString()).isEqualTo("test/x.sh");
+ }
+
+ @Test
+ public void testCustomAndDefaultExecutableReportsError() throws Exception {
+ scratch.file(
+ "test/rule.bzl",
+ "def _impl(ctx):",
+ " e = ctx.outputs.executable",
+ " o = ctx.actions.declare_file('x.sh')",
+ " ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+ " return [DefaultInfo(executable = o)]",
+ "my_rule = rule(_impl, executable = True)"
+ );
+
+ scratch.file(
+ "test/BUILD",
+ "load(':rule.bzl', 'my_rule')",
+ "my_rule(name = 'xxx')"
+ );
+ reporter.removeHandler(failFastHandler);
+ getConfiguredTarget("//test:xxx");
+ assertContainsEvent("ERROR /workspace/test/BUILD:2:1: in my_rule rule //test:xxx: ");
+ assertContainsEvent("/workspace/test/rule.bzl:5:12: The rule 'my_rule' both accesses "
+ + "'ctx.outputs.executable' and provides a different executable 'test/x.sh'. "
+ + "Do not use 'ctx.output.executable'.");
+ }
+
+
+ @Test
+ public void testCustomExecutableStrNoEffect() throws Exception {
+ scratch.file(
+ "test/rule.bzl",
+ "def _impl(ctx):",
+ " o = ctx.actions.declare_file('x.sh')",
+ " ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+ " print(str(ctx.outputs))",
+ " return [DefaultInfo(executable = o)]",
+ "my_rule = rule(_impl, executable = True)"
+ );
+
+ scratch.file(
+ "test/BUILD",
+ "load(':rule.bzl', 'my_rule')",
+ "my_rule(name = 'xxx')"
+ );
+
+ ConfiguredTarget configuredTarget = getConfiguredTarget("//test:xxx");
+ Artifact executable = configuredTarget.getProvider(FilesToRunProvider.class).getExecutable();
+ assertThat(executable.getRootRelativePathString()).isEqualTo("test/x.sh");
+ }
+
+ @Test
+ public void testCustomExecutableDirNoEffect() throws Exception {
+ scratch.file(
+ "test/rule.bzl",
+ "def _impl(ctx):",
+ " o = ctx.actions.declare_file('x.sh')",
+ " ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+ " print(dir(ctx.outputs))",
+ " return [DefaultInfo(executable = o)]",
+ "my_rule = rule(_impl, executable = True)"
+ );
+
+ scratch.file(
+ "test/BUILD",
+ "load(':rule.bzl', 'my_rule')",
+ "my_rule(name = 'xxx')"
+ );
+
+ ConfiguredTarget configuredTarget = getConfiguredTarget("//test:xxx");
+ Artifact executable = configuredTarget.getProvider(FilesToRunProvider.class).getExecutable();
+ assertThat(executable.getRootRelativePathString()).isEqualTo("test/x.sh");
+ }
+
+ @Test
+ public void testOutputsObjectInDifferentRuleInaccessible() throws Exception {
+ scratch.file(
+ "test/rule.bzl",
+ "PInfo = provider(fields = ['outputs'])",
+ "def _impl(ctx):",
+ " o = ctx.actions.declare_file('x.sh')",
+ " ctx.actions.write(o, 'echo Stuff', is_executable = True)",
+ " return [PInfo(outputs = ctx.outputs), DefaultInfo(executable = o)]",
+ "my_rule = rule(_impl, executable = True)",
+ "def _dep_impl(ctx):",
+ " o = ctx.attr.dep[PInfo].outputs.executable",
+ " pass",
+ "my_dep_rule = rule(_dep_impl, attrs = { 'dep' : attr.label() })"
+ );
+
+ scratch.file(
+ "test/BUILD",
+ "load(':rule.bzl', 'my_rule', 'my_dep_rule')",
+ "my_rule(name = 'xxx')",
+ "my_dep_rule(name = 'yyy', dep = ':xxx')"
+ );
+
+ reporter.removeHandler(failFastHandler);
+ getConfiguredTarget("//test:yyy");
+ assertContainsEvent("ERROR /workspace/test/BUILD:3:1: in my_dep_rule rule //test:yyy: ");
+ assertContainsEvent("File \"/workspace/test/rule.bzl\", line 8, in _dep_impl");
+ assertContainsEvent("ctx.attr.dep[PInfo].outputs.executable");
+ assertContainsEvent("cannot access outputs of rule '//test:xxx' outside "
+ + "of its own rule implementation function");
+ }
+
+ @Test
+ public void testOutputsObjectStringRepresentation() throws Exception {
+ scratch.file(
+ "test/rule.bzl",
+ "PInfo = provider(fields = ['outputs', 's'])",
+ "def _impl(ctx):",
+ " ctx.actions.write(ctx.outputs.executable, 'echo Stuff', is_executable = True)",
+ " ctx.actions.write(ctx.outputs.other, 'Other')",
+ " return [PInfo(outputs = ctx.outputs, s = str(ctx.outputs))]",
+ "my_rule = rule(_impl, executable = True, outputs = { 'other' : '%{name}.other' })",
+ "def _dep_impl(ctx):",
+ " return [PInfo(s = str(ctx.attr.dep[PInfo].outputs))]",
+ "my_dep_rule = rule(_dep_impl, attrs = { 'dep' : attr.label() })"
+ );
+
+ scratch.file(
+ "test/BUILD",
+ "load(':rule.bzl', 'my_rule', 'my_dep_rule')",
+ "my_rule(name = 'xxx')",
+ "my_dep_rule(name = 'yyy', dep = ':xxx')"
+ );
+
+ SkylarkKey pInfoKey = new SkylarkKey(Label.parseAbsolute("//test:rule.bzl"), "PInfo");
+
+ ConfiguredTarget targetXXX = getConfiguredTarget("//test:xxx");
+ assertThat(targetXXX.get(pInfoKey).getValue("s"))
+ .isEqualTo(
+ "ctx.outputs(executable = <generated file test/xxx>, "
+ + "other = <generated file test/xxx.other>)");
+
+ ConfiguredTarget targetYYY = getConfiguredTarget("//test:yyy");
+ assertThat(targetYYY.get(pInfoKey).getValue("s"))
+ .isEqualTo("ctx.outputs(for //test:xxx)");
+ }
+
+ @Test
+ public void testExecutableRuleWithNoExecutableReportsError() throws Exception {
+ scratch.file(
+ "test/rule.bzl",
+ "def _impl(ctx):",
+ " pass",
+ "my_rule = rule(_impl, executable = True)"
+ );
+
+ scratch.file(
+ "test/BUILD",
+ "load(':rule.bzl', 'my_rule')",
+ "my_rule(name = 'xxx')"
+ );
+
+ reporter.removeHandler(failFastHandler);
+ getConfiguredTarget("//test:xxx");
+ assertContainsEvent("ERROR /workspace/test/BUILD:2:1: in my_rule rule //test:xxx: ");
+ assertContainsEvent("/rule.bzl:1:5: The rule 'my_rule' is executable. "
+ + "It needs to create an executable File and pass it as the 'executable' "
+ + "parameter to the DefaultInfo it returns.");
+ }
+
+
/**
* Skylark integration test that forces inlining.
*/
@@ -1396,6 +1606,5 @@ public class SkylarkIntegrationTest extends BuildViewTestCase {
assertThat(e).hasMessageThat().contains("//test/skylark:ext4.bzl");
}
}
-
}
}
diff --git a/src/test/shell/integration/run_test.sh b/src/test/shell/integration/run_test.sh
index afa84f0d91..5c4ec2c5bf 100755
--- a/src/test/shell/integration/run_test.sh
+++ b/src/test/shell/integration/run_test.sh
@@ -329,4 +329,44 @@ EOF
expect_log "Dancing with wolves"
}
+function test_run_for_custom_executable() {
+ mkdir -p a
+ cat > a/x.bzl <<EOF
+def _impl(ctx):
+ f = ctx.actions.declare_file("x.sh")
+ ctx.actions.write(f,
+ "#!/bin/sh\n"
+ + "if [ -z \$1 ]; then\\n"
+ + " echo Run Forest run\\n"
+ + "else\\n"
+ + " echo Run Forest run > \$1\\n"
+ + "fi",
+ is_executable=True)
+ return [DefaultInfo(executable=f)]
+
+my_rule = rule(_impl, executable = True)
+
+def _tool_impl(ctx):
+ f = ctx.actions.declare_file("output")
+ ctx.actions.run(executable = ctx.executable.tool,
+ inputs = [],
+ outputs = [f],
+ arguments = [f.path]
+ )
+ return DefaultInfo(files = depset([f]))
+my_tool_rule = rule(_tool_impl, attrs = { 'tool' : attr.label(executable = True, cfg = "host") })
+EOF
+
+cat > a/BUILD <<EOF
+load(":x.bzl", "my_rule", "my_tool_rule")
+my_rule(name = "zzz")
+my_tool_rule(name = "kkk", tool = ":zzz")
+EOF
+ bazel run //a:zzz > "$TEST_log" || fail "Expected success"
+ expect_log "Run Forest run"
+ bazel build //a:kkk > "$TEST_log" || fail "Expected success"
+ grep "Run Forest run" bazel-bin/a/output || fail "Output file wrong"
+}
+
+
run_suite "'${PRODUCT_NAME} run' integration tests"