diff options
author | 2017-10-23 19:14:02 +0200 | |
---|---|---|
committer | 2017-10-24 10:39:41 +0200 | |
commit | 2ce9844612e120b099369d923e132d6c9c209854 (patch) | |
tree | 33f6b53b5008926fdcd3b847c77df19d941630bf | |
parent | 98cd82cbdcac7c48164a611c5a9aa8fc2f1720ef (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
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" |