Add `java_binary.use_launcher` as a launcher opt-out mechanism

and also allow setting an empty `--{host_,}java_launcher=` to disable the
launcher.

PiperOrigin-RevId: 339366794
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java
index 81b7d8f..7c0975e 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/java/BazelJavaRuleClasses.java
@@ -420,6 +420,15 @@
                   .allowedFileTypes(FileTypeSet.NO_FILE)
                   .mandatoryProviders(
                       StarlarkProviderIdentifier.forKey(CcLauncherInfo.PROVIDER.getKey())))
+          /* <!-- #BLAZE_RULE($base_java_binary).ATTRIBUTE(use_launcher) -->
+          Whether the binary should use a custom launcher.
+
+          <p>If this attribute is set to false, the
+          <a href="${link java_binary.launcher}">launcher</a> attribute  and the related
+          <a href="../user-manual.html#flag--java_launcher"><code>--java_launcher</code></a> flag
+          will be ignored for this target.
+          <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
+          .add(attr("use_launcher", BOOLEAN).value(true))
           .add(attr(":java_launcher", LABEL).value(JavaSemantics.JAVA_LAUNCHER)) // blaze flag
           .add(
               attr("$launcher", LABEL)
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaBinary.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaBinary.java
index caabbbf..32ef494 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaBinary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaBinary.java
@@ -103,6 +103,11 @@
       }
     }
 
+    if (!ruleContext.attributes().get("use_launcher", Type.BOOLEAN)
+        && ruleContext.attributes().isAttributeValueExplicitlySpecified("launcher")) {
+      ruleContext.ruleError("launcher specified but use_launcher is false");
+    }
+
     semantics.checkRule(ruleContext, common);
     semantics.checkForProtoLibraryAndJavaProtoLibraryOnSameProto(ruleContext, common);
     String mainClass = semantics.getMainClass(ruleContext, common.getSrcsArtifacts());
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaHelper.java
index 3b6fb81..3b20d26 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaHelper.java
@@ -54,7 +54,10 @@
 
   /**
    * Control structure abstraction for safely extracting a prereq from the launcher attribute or
-   * --java_launcher flag.
+   * {@code --java_launcher} flag.
+   *
+   * <p>Returns {@code null} if either {@code create_executable} or {@code use_launcher} are
+   * disabled.
    */
   private static String filterLauncherForTarget(RuleContext ruleContext) {
     // create_executable=0 disables the launcher
@@ -62,6 +65,11 @@
         && !ruleContext.attributes().get("create_executable", Type.BOOLEAN)) {
       return null;
     }
+    // use_launcher=False disables the launcher
+    if (ruleContext.getRule().isAttrDefined("use_launcher", Type.BOOLEAN)
+        && !ruleContext.attributes().get("use_launcher", Type.BOOLEAN)) {
+      return null;
+    }
     // BUILD rule "launcher" attribute
     if (ruleContext.getRule().isAttrDefined("launcher", BuildType.LABEL)
         && ruleContext.attributes().get("launcher", BuildType.LABEL) != null) {
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaOptions.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaOptions.java
index 7318b1f..6875016 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaOptions.java
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.rules.java;
 
 import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.config.CoreOptionConverters.EmptyToNullLabelConverter;
 import com.google.devtools.build.lib.analysis.config.CoreOptionConverters.LabelConverter;
 import com.google.devtools.build.lib.analysis.config.CoreOptionConverters.LabelListConverter;
 import com.google.devtools.build.lib.analysis.config.CoreOptionConverters.LabelMapConverter;
@@ -333,7 +334,7 @@
   @Option(
       name = "host_java_launcher",
       defaultValue = "null",
-      converter = LabelConverter.class,
+      converter = EmptyToNullLabelConverter.class,
       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
       effectTags = {OptionEffectTag.UNKNOWN},
       help = "The Java launcher used by tools that are executed during a build.")
@@ -342,7 +343,7 @@
   @Option(
       name = "java_launcher",
       defaultValue = "null",
-      converter = LabelConverter.class,
+      converter = EmptyToNullLabelConverter.class,
       documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
       effectTags = {OptionEffectTag.UNKNOWN},
       help =
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaSemantics.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaSemantics.java
index b677745..fe90877 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaSemantics.java
@@ -139,6 +139,11 @@
                 return null;
               }
 
+              // use_launcher=False disables the launcher
+              if (attributes.has("use_launcher") && !attributes.get("use_launcher", Type.BOOLEAN)) {
+                return null;
+              }
+
               // don't read --java_launcher if this target overrides via a launcher attribute
               if (attributes.isAttributeValueExplicitlySpecified("launcher")) {
                 return attributes.get("launcher", LABEL);