Apply native binary launcher to sh_binary

This change:
1. Added launcher to @bazel_tools
  If the host platform is Windows, we use a prebuilt launcher.exe
  , otherwise the launcher needs to be built with MSVC first.

2. Launching sh_binary using native launcher.

Change-Id: I5a63135455057fbfe04ff0cce7ec7994ef0c347a
PiperOrigin-RevId: 163442540
diff --git a/src/BUILD b/src/BUILD
index 581b88b..041d690 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -147,6 +147,7 @@
         "//src/tools/android/java/com/google/devtools/build/android/idlclass:embedded_tools",
         "//src/tools/android/java/com/google/devtools/build/android/dexer:embedded_tools",
         "//src/tools/android/java/com/google/devtools/build/android/ziputils:embedded_tools",
+        "//src/tools/launcher:srcs",
         "//src/tools/singlejar:embedded_tools",
         "//src/main/cpp/util:embedded_tools",
         "//src/main/native:embedded_tools",
@@ -179,12 +180,15 @@
         # is resolved, use cc implementation of singlejar on windows
         ":windows": [
             "//src/java_tools/singlejar:SingleJar_deploy.jar",
+            "//src/tools/launcher:launcher",
         ],
         ":windows_msys": [
             "//src/java_tools/singlejar:SingleJar_deploy.jar",
+            "//src/tools/launcher:launcher",
         ],
         ":windows_msvc": [
             "//src/java_tools/singlejar:SingleJar_deploy.jar",
+            "//src/tools/launcher:launcher",
         ],
         ":arm": [
             "//src/java_tools/singlejar:SingleJar_deploy.jar",
diff --git a/src/BUILD.tools b/src/BUILD.tools
index d4e0a41..46a13e6 100644
--- a/src/BUILD.tools
+++ b/src/BUILD.tools
@@ -21,3 +21,21 @@
     values = {"cpu": "x64_windows_msys"},
     visibility = ["//visibility:public"],
 )
+
+config_setting(
+    name = "host_windows",
+    values = {"host_cpu": "x64_windows"},
+    visibility = ["//visibility:public"],
+)
+
+config_setting(
+    name = "host_windows_msvc",
+    values = {"host_cpu": "x64_windows_msvc"},
+    visibility = ["//visibility:public"],
+)
+
+config_setting(
+    name = "host_windows_msys",
+    values = {"host_cpu": "x64_windows_msys"},
+    visibility = ["//visibility:public"],
+)
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 291bb98..6236d6b 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -732,7 +732,6 @@
         "bazel/rules/java/java_stub_template.txt",
         "bazel/rules/java/java_stub_template_windows.txt",
         "bazel/rules/python/python_stub_template.txt",
-        "bazel/rules/sh/sh_stub_template_windows.txt",
     ],
     deps = [
         ":bazel",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
index 4282652..2d13abd 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/ConfiguredRuleClassProvider.java
@@ -227,11 +227,19 @@
         registeredSkylarkProviders = ImmutableBiMap.builder();
     private Map<String, String> platformRegexps = new TreeMap<>();
 
+    // TODO(pcloudy): Remove this field after Bazel rule definitions are not used internally.
+    private String nativeLauncherLabel;
+
     public Builder setProductName(String productName) {
       this.productName = productName;
       return this;
     }
 
+    public Builder setNativeLauncherLabel(String label) {
+      this.nativeLauncherLabel = label;
+      return this;
+    }
+
     public void addWorkspaceFilePrefix(String contents) {
       defaultWorkspaceFilePrefix.append(contents);
     }
@@ -455,6 +463,14 @@
     }
 
     @Override
+    public Label getLauncherLabel() {
+      if (nativeLauncherLabel == null) {
+        return null;
+      }
+      return getToolsLabel(nativeLauncherLabel);
+    }
+
+    @Override
     public String getToolsRepository() {
       return toolsRepository;
     }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinitionEnvironment.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinitionEnvironment.java
index 5880e95..e388b86 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinitionEnvironment.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleDefinitionEnvironment.java
@@ -16,6 +16,8 @@
 
 import com.google.devtools.build.lib.cmdline.Label;
 
+import javax.annotation.Nullable;
+
 /**
  * Encapsulates the services available for implementors of the {@link RuleDefinition}
  * interface.
@@ -37,4 +39,14 @@
    * Returns the tools repository prefix.
    */
   String getToolsRepository();
+
+  /**
+   * Returns the label for Bazel binary launcher.
+   * In bazel, it should be //tools/launcher:launcher, otherwise it should be null.
+   *
+   * TODO(pcloudy): Remove this after Bazel rule definitions are not used internally anymore.
+   * Related bug b/63658220
+   */
+  @Nullable
+  Label getLauncherLabel();
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
index e88ff5a..e93a154 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/BazelRuleClassProvider.java
@@ -213,6 +213,7 @@
               .setProductName("bazel")
               .setConfigurationCollectionFactory(new BazelConfigurationCollection())
               .setPrelude("//tools/build_rules:prelude_bazel")
+              .setNativeLauncherLabel("//tools/launcher:launcher")
               .setRunfilesPrefix(Label.DEFAULT_REPOSITORY_DIRECTORY)
               .setPrerequisiteValidator(new BazelPrerequisiteValidator());
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShBinaryRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShBinaryRule.java
index 02bcffa..7ba0859 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShBinaryRule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShBinaryRule.java
@@ -13,10 +13,15 @@
 // limitations under the License.
 package com.google.devtools.build.lib.bazel.rules.sh;
 
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL;
+
 import com.google.devtools.build.lib.analysis.BaseRuleClasses;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
 import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
 import com.google.devtools.build.lib.bazel.rules.sh.BazelShRuleClasses.ShRule;
+import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.packages.RuleClass;
 import com.google.devtools.build.lib.packages.RuleClass.Builder;
 
@@ -26,6 +31,10 @@
 public final class BazelShBinaryRule implements RuleDefinition {
   @Override
   public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+    Label launcher = environment.getLauncherLabel();
+    if (launcher != null) {
+      builder.add(attr("$launcher", LABEL).cfg(HOST).value(launcher));
+    }
     return builder.build();
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShTestRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShTestRule.java
index aed6ef5..8001a4e 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShTestRule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/BazelShTestRule.java
@@ -13,10 +13,15 @@
 // limitations under the License.
 package com.google.devtools.build.lib.bazel.rules.sh;
 
+import static com.google.devtools.build.lib.packages.Attribute.ConfigurationTransition.HOST;
+import static com.google.devtools.build.lib.packages.Attribute.attr;
+import static com.google.devtools.build.lib.packages.BuildType.LABEL;
+
 import com.google.devtools.build.lib.analysis.BaseRuleClasses;
 import com.google.devtools.build.lib.analysis.RuleDefinition;
 import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
 import com.google.devtools.build.lib.bazel.rules.sh.BazelShRuleClasses.ShRule;
+import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.packages.RuleClass;
 import com.google.devtools.build.lib.packages.RuleClass.Builder;
 import com.google.devtools.build.lib.packages.RuleClass.Builder.RuleClassType;
@@ -27,6 +32,10 @@
 public final class BazelShTestRule implements RuleDefinition {
   @Override
   public RuleClass build(Builder builder, RuleDefinitionEnvironment environment) {
+    Label launcher = environment.getLauncherLabel();
+    if (launcher != null) {
+      builder.add(attr("$launcher", LABEL).cfg(HOST).value(launcher));
+    }
     return builder.build();
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShBinary.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShBinary.java
index 66b0ec4..47a15de 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShBinary.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShBinary.java
@@ -13,7 +13,11 @@
 // limitations under the License.
 package com.google.devtools.build.lib.bazel.rules.sh;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.ByteSource;
 import com.google.devtools.build.lib.actions.Artifact;
 import com.google.devtools.build.lib.analysis.ConfiguredTarget;
 import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
@@ -22,23 +26,23 @@
 import com.google.devtools.build.lib.analysis.Runfiles;
 import com.google.devtools.build.lib.analysis.RunfilesProvider;
 import com.google.devtools.build.lib.analysis.RunfilesSupport;
+import com.google.devtools.build.lib.analysis.actions.BinaryFileWriteAction;
 import com.google.devtools.build.lib.analysis.actions.ExecutableSymlinkAction;
-import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
-import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution;
-import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Template;
+import com.google.devtools.build.lib.analysis.actions.SpawnAction;
 import com.google.devtools.build.lib.bazel.rules.BazelConfiguration;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
 import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
-import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
 import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory;
 import com.google.devtools.build.lib.util.OS;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 /**
  * Implementation for the sh_binary rule.
  */
 public class ShBinary implements RuleConfiguredTargetFactory {
-  private static final Template STUB_SCRIPT_WINDOWS =
-      Template.forResource(ShBinary.class, "sh_stub_template_windows.txt");
 
   @Override
   public ConfiguredTarget create(RuleContext ruleContext) throws RuleErrorException {
@@ -68,7 +72,7 @@
             ruleContext.getConfiguration().legacyExternalRunfiles());
 
     Artifact mainExecutable =
-        (OS.getCurrent() == OS.WINDOWS) ? wrapperForWindows(ruleContext, symlink, src) : symlink;
+        (OS.getCurrent() == OS.WINDOWS) ? launcherForWindows(ruleContext, symlink, src) : symlink;
     if (symlink != mainExecutable) {
       filesToBuildBuilder.add(mainExecutable);
       runfilesBuilder.addArtifact(symlink);
@@ -92,33 +96,98 @@
         .build();
   }
 
-  private static Artifact wrapperForWindows(
-      RuleContext ruleContext, Artifact primaryOutput, Artifact mainFile) {
-    if (primaryOutput.getFilename().endsWith(".exe")
-        || primaryOutput.getFilename().endsWith(".bat")
-        || primaryOutput.getFilename().endsWith(".cmd")) {
-      String suffix =
-          primaryOutput.getFilename().substring(primaryOutput.getFilename().length() - 4);
-      if (mainFile.getFilename().endsWith(suffix)) {
+  // Write launch info to buffer, return the number of bytes written.
+  private static int writeLaunchInfo(ByteArrayOutputStream buffer, String key, String value)
+      throws IOException {
+    byte[] keyBytes = key.getBytes(UTF_8);
+    byte[] valueBytes = value.getBytes(UTF_8);
+    buffer.write(keyBytes);
+    buffer.write('=');
+    buffer.write(valueBytes);
+    buffer.write('\0');
+    return keyBytes.length + valueBytes.length + 2;
+  }
+
+  private static boolean isWindowsExecutable(Artifact artifact) {
+    return artifact.getExtension().equals("exe")
+        || artifact.getExtension().equals("cmd")
+        || artifact.getExtension().equals("bat");
+  }
+
+  private static Artifact launcherForWindows(
+      RuleContext ruleContext, Artifact primaryOutput, Artifact mainFile)
+      throws RuleErrorException {
+    if (isWindowsExecutable(mainFile)) {
+      // If the extensions don't match, we should always respect mainFile's extension.
+      if (mainFile.getExtension().equals(primaryOutput.getExtension())) {
         return primaryOutput;
+      } else {
+        ruleContext.ruleError(
+            "Source file is a Windows executable file,"
+                + " target name extension should match source file extension");
+        throw new RuleErrorException();
       }
     }
 
-    Artifact wrapper =
-        ruleContext.getImplicitOutputArtifact(ruleContext.getTarget().getName() + ".cmd");
+    // The launcher file consists of a base launcher binary and the launch information appended to
+    // the binary. The length of launch info is a signed 64-bit integer written at the end of
+    // the binary in little endian.
+    Artifact launcher = ruleContext.getPrerequisiteArtifact("$launcher", Mode.HOST);
+    Artifact bashLauncher =
+        ruleContext.getImplicitOutputArtifact(ruleContext.getTarget().getName() + ".exe");
+    Artifact launchInfoFile =
+        ruleContext.getRelatedArtifact(bashLauncher.getRootRelativePath(), ".launch_info");
+
+    ByteArrayOutputStream launchInfo = new ByteArrayOutputStream();
+    Long dataSize = 0L;
+    try {
+      dataSize += writeLaunchInfo(launchInfo, "binary_type", "Bash");
+      dataSize += writeLaunchInfo(launchInfo, "workspace_name", ruleContext.getWorkspaceName());
+      dataSize +=
+          writeLaunchInfo(
+              launchInfo,
+              "bash_bin_path",
+              ruleContext
+                  .getFragment(BazelConfiguration.class)
+                  .getShellExecutable()
+                  .getPathString());
+      dataSize += writeLaunchInfo(launchInfo, "bash_main_file", mainFile.getRunfilesPathString());
+
+      ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
+      // All Windows versions are little endian.
+      buffer.order(ByteOrder.LITTLE_ENDIAN);
+      buffer.putLong(dataSize);
+
+      launchInfo.write(buffer.array());
+    } catch (IOException e) {
+      ruleContext.ruleError(e.getMessage());
+      throw new RuleErrorException();
+    }
+
     ruleContext.registerAction(
-        new TemplateExpansionAction(
+        new BinaryFileWriteAction(
             ruleContext.getActionOwner(),
-            wrapper,
-            STUB_SCRIPT_WINDOWS,
-            ImmutableList.of(
-                Substitution.of(
-                    "%bash_exe_path%",
-                    ruleContext
-                        .getFragment(BazelConfiguration.class)
-                        .getShellExecutable()
-                        .getPathString())),
-            true));
-    return wrapper;
+            launchInfoFile,
+            ByteSource.wrap(launchInfo.toByteArray()),
+            /*makeExecutable=*/ false));
+    String path = ruleContext.getConfiguration().getActionEnvironment().getFixedEnv().get("PATH");
+    ruleContext.registerAction(
+        new SpawnAction.Builder()
+            .addInput(launcher)
+            .addInput(launchInfoFile)
+            .addOutput(bashLauncher)
+            .setShellCommand(
+                "cmd.exe /c \"copy /Y /B "
+                    + launcher.getExecPathString().replace('/', '\\')
+                    + "+"
+                    + launchInfoFile.getExecPathString().replace('/', '\\')
+                    + " "
+                    + bashLauncher.getExecPathString().replace('/', '\\')
+                    + " > nul\"")
+            .setEnvironment(ImmutableMap.of("PATH", path))
+            .setMnemonic("BuildBashLauncher")
+            .build(ruleContext));
+
+    return bashLauncher;
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/sh_stub_template_windows.txt b/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/sh_stub_template_windows.txt
deleted file mode 100644
index 5c30e7f..0000000
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/sh/sh_stub_template_windows.txt
+++ /dev/null
@@ -1,33 +0,0 @@
-@rem Copyright 2016 The Bazel Authors. All rights reserved.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem    http://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-@rem This script was generated from sh_stub_template_windows.txt.  Please
-@rem don't edit it directly.
-@rem See the header comments in the accompanying shell script (same path as that
-@rem of this file, minus the ".cmd" extension) for available command line flags.
-
-@echo off
-SETLOCAL ENABLEEXTENSIONS
-
-set bash_path=%bash_exe_path%
-
-rem launcher=${$0%.cmd}
-set launcher=%~dp0%~n0
-
-set sh_path=%launcher:\=/%
-
-rem replaces $ with \$ in $*, then puts it on the command line
-rem Cribbed from here: http://ss64.com/nt/syntax-replace.html
-set all_args=%*
-call %bash_path% -c "'%sh_path%' %all_args:$=\$%"
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
index a840876..a8aaa97 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/mock/BazelAnalysisMock.java
@@ -147,6 +147,12 @@
         "package(default_visibility=['//visibility:public'])",
         "exports_files(['precompile.py'])",
         "cc_binary(name='zipper', srcs=['zip_main.cc'])");
+
+    config.create(
+        "/bazel_tools_workspace/tools/launcher/BUILD",
+        "package(default_visibility=['//visibility:public'])",
+        "cc_binary(name='launcher', srcs=['launcher_main.cc'])");
+
     ccSupport().setup(config);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
index e8d3d5b..5610d42 100644
--- a/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skylark/SkylarkRuleContextTest.java
@@ -642,7 +642,7 @@
   public void testGetExecutablePrerequisite() throws Exception {
     SkylarkRuleContext ruleContext = createRuleContext("//foo:androidlib");
     Object result = evalRuleContextCode(ruleContext, "ruleContext.executable._jarjar_bin");
-    assertThat(((Artifact) result).getFilename()).matches("^jarjar_bin(\\.cmd){0,1}$");
+    assertThat(((Artifact) result).getFilename()).matches("^jarjar_bin(\\.exe){0,1}$");
   }
 
   @Test
@@ -659,7 +659,7 @@
         (SpawnAction)
             Iterables.getOnlyElement(
                 ruleContext.getRuleContext().getAnalysisEnvironment().getRegisteredActions());
-    assertThat(action.getCommandFilename()).matches("^.*/jarjar_bin(\\.cmd){0,1}$");
+    assertThat(action.getCommandFilename()).matches("^.*/jarjar_bin(\\.exe){0,1}$");
   }
 
   @Test
diff --git a/src/test/py/bazel/BUILD b/src/test/py/bazel/BUILD
index f744c21..73bd4a3 100644
--- a/src/test/py/bazel/BUILD
+++ b/src/test/py/bazel/BUILD
@@ -65,8 +65,8 @@
 )
 
 py_test(
-    name = "launcher_script_test",
+    name = "launcher_test",
     size = "medium",
-    srcs = ["launcher_script_test.py"],
+    srcs = ["launcher_test.py"],
     deps = [":test_base"],
 )
diff --git a/src/test/py/bazel/launcher_script_test.py b/src/test/py/bazel/launcher_test.py
similarity index 79%
rename from src/test/py/bazel/launcher_script_test.py
rename to src/test/py/bazel/launcher_test.py
index 690c7fb..6c92dd7 100644
--- a/src/test/py/bazel/launcher_script_test.py
+++ b/src/test/py/bazel/launcher_test.py
@@ -19,7 +19,7 @@
 from src.test.py.bazel import test_base
 
 
-class LauncherScriptTest(test_base.TestBase):
+class LauncherTest(test_base.TestBase):
 
   def testJavaBinaryLauncher(self):
     self.ScratchFile('WORKSPACE')
@@ -75,13 +75,9 @@
             # On Linux/MacOS, all sh_binary rules generate an output file with
             # the same name as the rule, and this is a symlink to the file in
             # `srcs`. (Bazel allows only one file in `sh_binary.srcs`.)
-            # On Windows, if the rule's name and the srcs's name end with the
-            # same extension, and this extension is one of ".exe", ".cmd", or
-            # ".bat", then sh_binary makes a copy of the output file, with the
-            # same name as the rule. Otherwise (if the rule's name doesn't end
-            # with such an extension, or the extension of it doesn't match the
-            # main file's) then Bazel creates a %{rulename}.cmd output which is
-            # a similar launcher script to that generated by java_binary rules.
+            # On Windows, if the srcs's extension is one of ".exe", ".cmd", or
+            # ".bat", then Bazel requires the rule's name has the same
+            # extension, and the output file will be a copy of the source file.
             'sh_binary(',
             '  name = "bin1.sh",',
             '  srcs = ["foo.sh"],',
@@ -112,30 +108,37 @@
     self.AssertExitCode(exit_code, 0, stderr)
     bazel_bin = stdout[0]
 
-    exit_code, _, stderr = self.RunBazel(['build', '//foo:all'])
+    exit_code, _, stderr = self.RunBazel(['build', '//foo:bin1.sh'])
     self.AssertExitCode(exit_code, 0, stderr)
 
-    bin1 = os.path.join(bazel_bin, 'foo', 'bin1.sh.cmd'
+    bin1 = os.path.join(bazel_bin, 'foo', 'bin1.sh.exe'
                         if self.IsWindows() else 'bin1.sh')
     self.assertTrue(os.path.exists(bin1))
     self.assertTrue(
         os.path.isdir(os.path.join(bazel_bin, 'foo/bin1.sh.runfiles')))
 
+    exit_code, _, stderr = self.RunBazel(['build', '//foo:bin2.cmd'])
+    self.AssertExitCode(exit_code, 0, stderr)
+
     bin2 = os.path.join(bazel_bin, 'foo/bin2.cmd')
     self.assertTrue(os.path.exists(bin2))
     self.assertTrue(
         os.path.isdir(os.path.join(bazel_bin, 'foo/bin2.cmd.runfiles')))
 
-    bin3 = os.path.join(bazel_bin, 'foo', 'bin3.bat.cmd'
-                        if self.IsWindows() else 'bin3.bat')
-    self.assertTrue(os.path.exists(bin3))
-    self.assertTrue(
-        os.path.isdir(os.path.join(bazel_bin, 'foo/bin3.bat.runfiles')))
+    exit_code, _, stderr = self.RunBazel(['build', '//foo:bin3.bat'])
+    if self.IsWindows():
+      self.AssertExitCode(exit_code, 1, stderr)
+      self.assertIn('target name extension should match source file extension.',
+                    os.linesep.join(stderr))
+    else:
+      bin3 = os.path.join(bazel_bin, 'foo', 'bin3.bat')
+      self.assertTrue(os.path.exists(bin3))
+      self.assertTrue(
+          os.path.isdir(os.path.join(bazel_bin, 'foo/bin3.bat.runfiles')))
 
     if self.IsWindows():
       self.assertTrue(os.path.isfile(bin1))
       self.assertTrue(os.path.isfile(bin2))
-      self.assertTrue(os.path.isfile(bin3))
     else:
       self.assertTrue(os.path.islink(bin1))
       self.assertTrue(os.path.islink(bin2))
@@ -148,9 +151,6 @@
       self.AssertRunfilesManifestContains(
           os.path.join(bazel_bin, 'foo/bin2.cmd.runfiles/MANIFEST'),
           '__main__/bar/bar.txt')
-      self.AssertRunfilesManifestContains(
-          os.path.join(bazel_bin, 'foo/bin3.bat.runfiles/MANIFEST'),
-          '__main__/bar/bar.txt')
     else:
       self.assertTrue(
           os.path.islink(
@@ -174,9 +174,43 @@
       self.AssertExitCode(exit_code, 0, stderr)
       self.assertEqual(stdout[0], 'hello batch')
 
-      exit_code, stdout, stderr = self.RunProgram([bin3])
-      self.AssertExitCode(exit_code, 0, stderr)
-      self.assertEqual(stdout[0], 'hello batch')
+  def testShBinaryArgumentPassing(self):
+    self.ScratchFile('WORKSPACE')
+    self.ScratchFile('foo/BUILD', [
+        'sh_binary(',
+        '  name = "bin",',
+        '  srcs = ["bin.sh"],',
+        ')',
+    ])
+    foo_sh = self.ScratchFile('foo/bin.sh', [
+        '#!/bin/bash',
+        '# Store arguments in a array',
+        'args=("$@")',
+        '# Get the number of arguments',
+        'N=${#args[@]}',
+        '# Echo each argument',
+        'for (( i=0;i<$N;i++)); do',
+        ' echo ${args[${i}]}',
+        'done',
+    ])
+    os.chmod(foo_sh, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
+
+    exit_code, stdout, stderr = self.RunBazel(['info', 'bazel-bin'])
+    self.AssertExitCode(exit_code, 0, stderr)
+    bazel_bin = stdout[0]
+
+    exit_code, _, stderr = self.RunBazel(['build', '//foo:bin'])
+    self.AssertExitCode(exit_code, 0, stderr)
+
+    bin1 = os.path.join(bazel_bin, 'foo', 'bin.exe'
+                        if self.IsWindows() else 'bin')
+    self.assertTrue(os.path.exists(bin1))
+    self.assertTrue(os.path.isdir(os.path.join(bazel_bin, 'foo/bin.runfiles')))
+
+    arguments = ['a', 'a b', '"b"', 'C:\\a\\b\\', '"C:\\a b\\c\\"']
+    exit_code, stdout, stderr = self.RunProgram([bin1] + arguments)
+    self.AssertExitCode(exit_code, 0, stderr)
+    self.assertEqual(stdout, arguments)
 
   def testPyBinaryLauncher(self):
     self.ScratchFile('WORKSPACE')
@@ -220,7 +254,7 @@
         '  with open(sys.argv[1], "w") as f:',
         '    f.write("Hello World!")',
         'else:',
-        '  print "Hello World!"',
+        '  print("Hello World!")',
     ])
     self.ScratchFile('bar/BUILD', ['exports_files(["bar.txt"])'])
     self.ScratchFile('bar/bar.txt', ['hello'])
diff --git a/tools/BUILD b/tools/BUILD
index 9f9052e..49cd629 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -19,6 +19,7 @@
         "//tools/coverage:srcs",
         "//tools/ide:srcs",
         "//tools/jdk:srcs",
+        "//tools/launcher:srcs",
         "//tools/platforms:srcs",
         "//tools/genrule:srcs",
         "//tools/cpp:srcs",
@@ -50,6 +51,7 @@
         "//tools/j2objc:srcs",
         "//tools/jdk:package-srcs",
         "//tools/jdk:srcs",
+        "//tools/launcher:srcs",
         "//tools/platforms:package-srcs",
         "//tools/objc:srcs",
         "//tools/python:srcs",
diff --git a/tools/launcher/BUILD b/tools/launcher/BUILD
new file mode 100644
index 0000000..0158b25
--- /dev/null
+++ b/tools/launcher/BUILD
@@ -0,0 +1,11 @@
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
+
+alias(
+    name = "launcher",
+    actual = "//src/tools/launcher:launcher",
+)
diff --git a/tools/launcher/BUILD.tools b/tools/launcher/BUILD.tools
new file mode 100644
index 0000000..8679c1a
--- /dev/null
+++ b/tools/launcher/BUILD.tools
@@ -0,0 +1,13 @@
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "launcher",
+    srcs = select({
+      "//src:host_windows": ["launcher.exe"],
+      "//src:host_windows_msvc": ["launcher.exe"],
+      "//src:host_windows_msys": ["launcher.exe"],
+      "//conditions:default": [
+        "//src/tools/launcher:launcher",
+      ],
+    }),
+)