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
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 2ed3cc3..b47fb3b 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 @@
         "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 @@
                     .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 a045018..deefbcb 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 @@
         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 @@
     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 @@
   }
 
   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 @@
           .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 @@
     }
 
     if (!defaultProviderProvidedExplicitly) {
-      parseDefaultProviderKeys(oldStyleProviders, ruleContext, builder);
+      parseDefaultProviderKeys(oldStyleProviders, context, builder);
     }
 
     for (String key : oldStyleProviders.getKeys()) {
@@ -341,7 +346,7 @@
         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 @@
    * 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 @@
         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 9c7c5b7..dfccf8d 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 @@
 
 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.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 @@
           + "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 @@
           + "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 @@
           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 @@
 
   // 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 @@
     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 @@
             (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 @@
 
         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 @@
       }
 
       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 @@
   }
 
   /**
+   * 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 @@
     }
   }
 
-  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 @@
   }
 
   @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 a031f2c..7dd8acb 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 @@
     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 @@
           publicByDefault,
           binaryOutput,
           workspaceOnly,
-          outputsDefaultExecutable,
+          isExecutableSkylark,
           implicitOutputsFunction,
           isConfigMatcher,
           transitionFactory,
@@ -929,8 +929,8 @@
      * 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 @@
   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 @@
       boolean publicByDefault,
       boolean binaryOutput,
       boolean workspaceOnly,
-      boolean outputsDefaultExecutable,
+      boolean isExecutableSkylark,
       ImplicitOutputsFunction implicitOutputsFunction,
       boolean isConfigMatcher,
       RuleTransitionFactory transitionFactory,
@@ -1207,7 +1207,7 @@
     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 @@
   /**
    * 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 2aa78fa..3b8c91e 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.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.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 @@
     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 @@
         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 afa84f0..5c4ec2c 100755
--- a/src/test/shell/integration/run_test.sh
+++ b/src/test/shell/integration/run_test.sh
@@ -329,4 +329,44 @@
   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"