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() {