Update j2objc scripts to be `py_binary` targets.

PiperOrigin-RevId: 436904987
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java
index 14f60fd..a0d1771 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationSupport.java
@@ -864,7 +864,6 @@
       J2ObjcMappingFileProvider j2ObjcMappingFileProvider,
       J2ObjcEntryClassProvider j2ObjcEntryClassProvider) {
     NestedSet<String> entryClasses = j2ObjcEntryClassProvider.getEntryClasses();
-    Artifact pruner = ruleContext.getPrerequisiteArtifact("$j2objc_dead_code_pruner");
     NestedSet<Artifact> j2ObjcDependencyMappingFiles =
         j2ObjcMappingFileProvider.getDependencyMappingFiles();
     NestedSet<Artifact> j2ObjcHeaderMappingFiles =
@@ -903,9 +902,8 @@
                   XcodeConfigInfo.fromRuleContext(ruleContext),
                   appleConfiguration.getSingleArchPlatform())
               .setMnemonic("DummyPruner")
-              .setExecutable(pruner)
+              .setExecutable(ruleContext.getExecutablePrerequisite("$j2objc_dead_code_pruner"))
               .addInput(dummyArchive)
-              .addInput(pruner)
               .addInput(j2objcArchive)
               .addInput(xcrunwrapper(ruleContext).getExecutable())
               .addTransitiveInputs(j2ObjcDependencyMappingFiles)
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcAspect.java b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcAspect.java
index 7621ae9..c85f3a5 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcAspect.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/J2ObjcAspect.java
@@ -81,7 +81,6 @@
 import com.google.devtools.build.lib.skyframe.ConfiguredTargetAndData;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
-import com.google.devtools.build.lib.util.FileType;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -172,22 +171,20 @@
                         toolsRepository + "//tools/j2objc:j2objc_deploy.jar")))
         .add(
             attr("$j2objc_wrapper", LABEL)
-                .allowedFileTypes(FileType.of(".py"))
                 .cfg(ExecutionTransitionFactory.create())
                 .exec()
-                .singleArtifact()
+                .legacyAllowAnyFileType()
                 .value(
                     Label.parseAbsoluteUnchecked(
-                        toolsRepository + "//tools/j2objc:j2objc_wrapper")))
+                        toolsRepository + "//tools/j2objc:j2objc_wrapper_binary")))
         .add(
             attr("$j2objc_header_map", LABEL)
-                .allowedFileTypes(FileType.of(".py"))
                 .cfg(ExecutionTransitionFactory.create())
                 .exec()
-                .singleArtifact()
+                .legacyAllowAnyFileType()
                 .value(
                     Label.parseAbsoluteUnchecked(
-                        toolsRepository + "//tools/j2objc:j2objc_header_map")))
+                        toolsRepository + "//tools/j2objc:j2objc_header_map_binary")))
         .add(
             attr("$jre_emul_jar", LABEL)
                 .cfg(ExecutionTransitionFactory.create())
@@ -584,8 +581,7 @@
     SpawnAction.Builder transpilationAction =
         new SpawnAction.Builder()
             .setMnemonic("TranspilingJ2objc")
-            .setExecutable(ruleContext.getPrerequisiteArtifact("$j2objc_wrapper"))
-            .addInput(ruleContext.getPrerequisiteArtifact("$j2objc_wrapper"))
+            .setExecutable(ruleContext.getExecutablePrerequisite("$j2objc_wrapper"))
             .addInput(j2ObjcDeployJar)
             .addInput(bootclasspathJar)
             .addInputs(moduleFiles)
@@ -627,8 +623,7 @@
       ruleContext.registerAction(
           new SpawnAction.Builder()
               .setMnemonic("GenerateJ2objcHeaderMap")
-              .setExecutable(ruleContext.getPrerequisiteArtifact("$j2objc_header_map"))
-              .addInput(ruleContext.getPrerequisiteArtifact("$j2objc_header_map"))
+              .setExecutable(ruleContext.getExecutablePrerequisite("$j2objc_header_map"))
               .addInputs(sources)
               .addInputs(sourceJars)
               .addCommandLine(
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcRuleClasses.java
index 2d56a2e..acb5f26 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcRuleClasses.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcRuleClasses.java
@@ -664,12 +664,15 @@
                   .direct_compile_time_input()
                   .allowedFileTypes(FileTypeSet.ANY_FILE))
           .add(
+              // This attribute definition must be kept in sync with
+              // third_party/bazel_rules/rules_apple/apple/internal/rule_factory.bzl
               attr("$j2objc_dead_code_pruner", LABEL)
-                  .allowedFileTypes(FileType.of(".py"))
                   .cfg(ExecutionTransitionFactory.create())
                   .exec()
-                  .singleArtifact()
-                  .value(env.getToolsLabel("//tools/objc:j2objc_dead_code_pruner")))
+                  // Allow arbitrary executable files; this gives more flexibility for the
+                  // implementation of the underlying tool.
+                  .legacyAllowAnyFileType()
+                  .value(env.getToolsLabel("//tools/objc:j2objc_dead_code_pruner_binary")))
           .add(attr("$dummy_lib", LABEL).value(env.getToolsLabel("//tools/objc:dummy_lib")))
           .build();
     }
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/MockJ2ObjcSupport.java b/src/test/java/com/google/devtools/build/lib/packages/util/MockJ2ObjcSupport.java
index 7c10aba..91d2807 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/MockJ2ObjcSupport.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/MockJ2ObjcSupport.java
@@ -17,13 +17,9 @@
 import com.google.devtools.build.lib.testutil.TestConstants;
 import java.io.IOException;
 
-/**
- * Creates mock BUILD files required for J2Objc.
- */
+/** Creates mock BUILD files required for J2Objc. */
 public final class MockJ2ObjcSupport {
-  /**
-   * Setup the support for building with J2ObjC.
-   */
+  /** Setup the support for building with J2ObjC. */
   public static void setup(MockToolsConfig config) throws IOException {
     config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "third_party/java/j2objc/jre_emul.jar");
     config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "third_party/java/j2objc/mod/release");
@@ -79,12 +75,14 @@
         TestConstants.LOAD_PROTO_LANG_TOOLCHAIN,
         "package(default_visibility=['//visibility:public'])",
         "licenses(['notice'])",
-        "filegroup(",
-        "    name = 'j2objc_wrapper',",
-        "    srcs = ['j2objc_wrapper.py'])",
-        "filegroup(",
-        "    name = 'j2objc_header_map',",
-        "    srcs = ['j2objc_header_map.py'])",
+        "py_binary(",
+        "    name = 'j2objc_wrapper_binary',",
+        "    srcs = ['j2objc_wrapper_binary.py'],",
+        ")",
+        "py_binary(",
+        "    name = 'j2objc_header_map_binary',",
+        "    srcs = ['j2objc_header_map_binary.py'],",
+        ")",
         "proto_lang_toolchain(",
         "    name = 'j2objc_proto_toolchain',",
         "    blacklisted_protos = [':j2objc_proto_blacklist'],",
@@ -94,7 +92,9 @@
         "    plugin = '//third_party/java/j2objc:proto_plugin',",
         "    runtime = '//third_party/java/j2objc:proto_runtime',",
         ")",
-        "exports_files(['j2objc_deploy.jar'])",
+        "exports_files([",
+        "    'j2objc_deploy.jar',",
+        "])",
         "proto_library(",
         "    name = 'j2objc_proto_blacklist',",
         "    deps = [",
@@ -112,12 +112,15 @@
 
     if (config.isRealFileSystem()) {
       config.linkTool(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_deploy.jar");
-      config.linkTool(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_wrapper.py");
-      config.linkTool(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_header_map.py");
+      config.linkTool(
+          TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_wrapper_binary");
+      config.linkTool(
+          TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_header_map_binary");
     } else {
       config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_deploy.jar");
-      config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_wrapper.py");
-      config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_header_map.py");
+      config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_wrapper_binary");
+      config.create(
+          TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/j2objc/j2objc_header_map_binary");
     }
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/packages/util/MockObjcSupport.java b/src/test/java/com/google/devtools/build/lib/packages/util/MockObjcSupport.java
index 003f0d6..dc715b2 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/util/MockObjcSupport.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/util/MockObjcSupport.java
@@ -168,7 +168,10 @@
         "filegroup(name = 'default_provisioning_profile', srcs = ['foo.mobileprovision'])",
         "sh_binary(name = 'xcrunwrapper', srcs = ['xcrunwrapper.sh'])",
         "filegroup(name = 'xctest_infoplist', srcs = ['xctest.plist'])",
-        "filegroup(name = 'j2objc_dead_code_pruner', srcs = ['j2objc_dead_code_pruner.py'])",
+        "py_binary(",
+        "  name = 'j2objc_dead_code_pruner_binary',",
+        "  srcs = ['j2objc_dead_code_pruner_binary.py']",
+        ")",
         "xcode_config(name = 'host_xcodes',",
         "  default = ':version7_3_1',",
         "  versions = [':version7_3_1', ':version5_0', ':version7_3', ':version5_8', ':version5'])",
@@ -208,7 +211,6 @@
     config.create(
         TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/foo.mobileprovision", "No such luck");
     config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/xctest.plist");
-    config.create(TestConstants.TOOLS_REPOSITORY_SCRATCH + "tools/objc/j2objc_dead_code_pruner.py");
     setupCcToolchainConfig(config);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/BazelJ2ObjcLibraryTest.java b/src/test/java/com/google/devtools/build/lib/rules/objc/BazelJ2ObjcLibraryTest.java
index d977ee3..1b9f403 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/BazelJ2ObjcLibraryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/BazelJ2ObjcLibraryTest.java
@@ -529,16 +529,17 @@
         getConfiguration(target).getBinDirectory(RepositoryName.MAIN).getExecPath() + "/";
     assertThat(baseArtifactNames(headerMappingAction.getInputs()))
         .containsAtLeast("libOne.java", "jar.srcjar");
-    assertThat(headerMappingAction.getArguments())
-        .containsExactly(
-            TestConstants.TOOLS_REPOSITORY_PATH_PREFIX + "tools/j2objc/j2objc_header_map.py",
-            "--source_files",
-            "java/com/google/transpile/libOne.java",
-            "--source_jars",
-            "java/com/google/transpile/jar.srcjar",
-            "--output_mapping_file",
-            execPath + "java/com/google/transpile/lib1.mapping.j2objc")
-        .inOrder();
+    assertThat(headerMappingAction.getArguments().get(0))
+        .contains("tools/j2objc/j2objc_header_map_binary");
+    assertThat(headerMappingAction.getArguments().get(1)).isEqualTo("--source_files");
+    assertThat(headerMappingAction.getArguments().get(2))
+        .isEqualTo("java/com/google/transpile/libOne.java");
+    assertThat(headerMappingAction.getArguments().get(3)).isEqualTo("--source_jars");
+    assertThat(headerMappingAction.getArguments().get(4))
+        .isEqualTo("java/com/google/transpile/jar.srcjar");
+    assertThat(headerMappingAction.getArguments().get(5)).isEqualTo("--output_mapping_file");
+    assertThat(headerMappingAction.getArguments().get(6))
+        .isEqualTo(execPath + "java/com/google/transpile/lib1.mapping.j2objc");
   }
 
   protected void checkObjcCompileActions(
@@ -1045,17 +1046,17 @@
         TestConstants.LOAD_PROTO_LANG_TOOLCHAIN,
         "package(default_visibility=['//visibility:public'])",
         "exports_files(['j2objc_deploy.jar'])",
-        "filegroup(",
-        "    name = 'j2objc_wrapper',",
-        "    srcs = ['j2objc_wrapper.py'],",
+        "py_binary(",
+        "    name = 'j2objc_wrapper_binary',",
+        "    srcs = ['j2objc_wrapper_binary.py'],",
         ")",
         "proto_library(",
         "    name = 'excluded_protos',",
         "    srcs = ['proto_to_exclude.proto'],",
         ")",
-        "filegroup(",
-        "    name = 'j2objc_header_map',",
-        "    srcs = ['j2objc_header_map.py'],",
+        "py_binary(",
+        "    name = 'j2objc_header_map_binary',",
+        "    srcs = ['j2objc_header_map_binary.py'],",
         ")",
         "proto_lang_toolchain(",
         "    name = 'alt_j2objc_proto_toolchain',",
@@ -1162,13 +1163,8 @@
             "com.google.app.test.test"));
 
     SpawnAction deadCodeRemovalAction = (SpawnAction) getGeneratingAction(prunedArchive);
-    assertContainsSublist(
-        deadCodeRemovalAction.getArguments(),
-        new ImmutableList.Builder<String>()
-            .add(
-                TestConstants.TOOLS_REPOSITORY_PATH_PREFIX
-                    + "tools/objc/j2objc_dead_code_pruner.py")
-            .build());
+    assertThat(deadCodeRemovalAction.getArguments().get(0))
+        .contains("tools/objc/j2objc_dead_code_pruner_binary");
     assertThat(deadCodeRemovalAction.getOutputs()).containsExactly(prunedArchive);
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java
index 8a8c8a8..cca510e 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java
+++ b/src/test/java/com/google/devtools/build/lib/rules/objc/ObjcRuleTestCase.java
@@ -585,7 +585,10 @@
         "            default = Label('" + toolsRepo + "//tools/cpp:grep-includes'),",
         "        ),",
         "        '_j2objc_dead_code_pruner': attr.label(",
-        "            default = Label('" + toolsLoc + ":j2objc_dead_code_pruner'),),",
+        "            executable = True,",
+        "            allow_files=True,",
+        "            cfg = 'exec',",
+        "            default = Label('" + toolsLoc + ":j2objc_dead_code_pruner_binary'),),",
         "        '_xcode_config': attr.label(",
         "            default=configuration_field(",
         "                fragment='apple', name='xcode_config_label'),),",
diff --git a/tools/j2objc/BUILD.tools b/tools/j2objc/BUILD.tools
index 83f43b8..da87d38 100644
--- a/tools/j2objc/BUILD.tools
+++ b/tools/j2objc/BUILD.tools
@@ -11,6 +11,7 @@
     runtime_deps = ["@bazel_j2objc//:j2objc"],
 )
 
+# TODO(b/225174999): Remove filegroups once blaze update is rolled out.
 filegroup(
     name = "j2objc_wrapper",
     srcs = ["j2objc_wrapper.py"],
@@ -21,6 +22,18 @@
     srcs = ["j2objc_header_map.py"],
 )
 
+py_binary(
+    name = "j2objc_wrapper_binary",
+    srcs = ["j2objc_wrapper_binary.py"],
+    python_version = "PY3",
+)
+
+py_binary(
+    name = "j2objc_header_map_binary",
+    srcs = ["j2objc_header_map_binary.py"],
+    python_version = "PY3",
+)
+
 proto_lang_toolchain(
     name = "j2objc_proto_toolchain",
     blacklisted_protos = [],
diff --git a/tools/j2objc/j2objc_header_map_binary.py b/tools/j2objc/j2objc_header_map_binary.py
new file mode 100755
index 0000000..ecdf1a5
--- /dev/null
+++ b/tools/j2objc/j2objc_header_map_binary.py
@@ -0,0 +1,134 @@
+#!/usr/bin/python3
+
+# Copyright 2017 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A script to generate Java class to ObjC header mapping for J2ObjC.
+
+This script generates a text file containing mapping between top-level Java
+classes to associated ObjC headers, generated by J2ObjC.
+
+The mapping file is used by dependent J2ObjC transpilation actions to locate
+the correct header import paths for dependent Java classes.
+
+Inside the script, we read the Java source files and source jars of a single
+Java rule, and parse out the package names from the package statements, using
+regular expression matching.
+
+Note that we cannot guarantee 100% correctness by using just regular expression,
+but it should be good enough. This allows us to avoid doing any further complex
+parsing of the source files and keep the script light-weight without other
+dependencies. In the future, we can consider implementing a simple Java lexer
+here that correctly parses the package statements out of Java files.
+"""
+
+import argparse
+import os
+import re
+import zipfile
+
+_PACKAGE_RE = re.compile(r'(package)\s+([\w\.]+);')
+
+
+def _get_file_map_entry(java_file_path, java_file):
+  """Returns the top-level Java class and header file path tuple.
+
+  Args:
+    java_file_path: The file path of the source Java file.
+    java_file: The actual file of the source java file.
+  Returns:
+    A tuple containing top-level Java class and associated header file path. Or
+    None if no package statement exists in the source file.
+  """
+  for line in java_file:
+    stripped_line = line.strip()
+    stripped_line_str = stripped_line
+    if isinstance(stripped_line, bytes):
+      stripped_line_str = stripped_line.decode('utf-8', 'strict')
+    elif not isinstance(stripped_line, (str, bytes)):
+      raise TypeError("not expecting type '%s'" % type(stripped_line_str))
+    package_statement = _PACKAGE_RE.search(stripped_line_str)
+
+    # We identified a potential package statement.
+    if package_statement:
+      preceding_characters = stripped_line[0:package_statement.start(1)]
+      # We have preceding characters before the package statement. We need to
+      # look further into them.
+      if preceding_characters:
+        # Skip comment line.
+        if preceding_characters.startswith('//'):
+          continue
+
+        # Preceding characters also must end with a space, represent an end
+        # of comment, or end of a statement.
+        # Otherwise, we skip the current line.
+        if not (preceding_characters[len(preceding_characters) - 1].isspace() or
+                preceding_characters.endswith(';') or
+                preceding_characters.endswith('*/')):
+          continue
+      package_name = package_statement.group(2)
+      class_name = os.path.splitext(os.path.basename(java_file_path))[0]
+      header_file = os.path.splitext(java_file_path)[0] + '.h'
+      return (package_name + '.' + class_name, header_file)
+  return None
+
+
+def main():
+  parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
+  parser.add_argument(
+      '--source_files',
+      required=False,
+      help='The source files')
+  parser.add_argument(
+      '--source_jars',
+      required=False,
+      help='The source jars.')
+  parser.add_argument(
+      '--output_mapping_file',
+      required=False,
+      help='The output mapping file')
+
+  args, _ = parser.parse_known_args()
+  class_to_header_map = dict()
+
+  # Process the source files.
+  if args.source_files:
+    source_files = args.source_files.split(',')
+    for source_file in source_files:
+      with open(source_file, 'r') as f:
+        entry = _get_file_map_entry(source_file, f)
+        if entry:
+          class_to_header_map[entry[0]] = entry[1]
+
+  # Process the source jars.
+  if args.source_jars:
+    source_jars = args.source_jars.split(',')
+    for source_jar in source_jars:
+      with zipfile.ZipFile(source_jar, 'r') as jar:
+        for jar_entry in jar.namelist():
+          if jar_entry.endswith('.java'):
+            with jar.open(jar_entry) as jar_entry_file:
+              entry = _get_file_map_entry(jar_entry, jar_entry_file)
+              if entry:
+                class_to_header_map[entry[0]] = entry[1]
+
+  # Generate the output header mapping file.
+  if args.output_mapping_file:
+    with open(args.output_mapping_file, 'w') as output_mapping_file:
+      for class_name in sorted(class_to_header_map):
+        header_path = class_to_header_map[class_name]
+        output_mapping_file.write(class_name + '=' + header_path + '\n')
+
+if __name__ == '__main__':
+  main()
diff --git a/tools/j2objc/j2objc_wrapper_binary.py b/tools/j2objc/j2objc_wrapper_binary.py
new file mode 100755
index 0000000..e6f4c75
--- /dev/null
+++ b/tools/j2objc/j2objc_wrapper_binary.py
@@ -0,0 +1,538 @@
+#!/usr/bin/python3
+
+# Copyright 2015 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A wrapper script for J2ObjC transpiler.
+
+This script wraps around J2ObjC transpiler to also output a dependency mapping
+file by scanning the import and include directives of the J2ObjC-translated
+files.
+"""
+
+import argparse
+import errno
+import multiprocessing
+import os
+import queue
+import re
+import shutil
+import subprocess
+import tempfile
+import threading
+import zipfile
+
+_INCLUDE_RE = re.compile('#(include|import) "([^"]+)"')
+_CONST_DATE_TIME = [1980, 1, 1, 0, 0, 0]
+_ADD_EXPORTS = [
+    '--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED',
+    '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
+    '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
+    '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
+    '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
+]
+
+
+def RunJ2ObjC(java, jvm_flags, j2objc, main_class, output_file_path,
+              j2objc_args, source_paths, files_to_translate):
+  """Runs J2ObjC transpiler to translate Java source files to ObjC.
+
+  Args:
+    java: The path of the Java executable.
+    jvm_flags: A comma-separated list of flags to pass to JVM.
+    j2objc: The deploy jar of J2ObjC.
+    main_class: The J2ObjC main class to invoke.
+    output_file_path: The output file directory.
+    j2objc_args: A list of args to pass to J2ObjC transpiler.
+    source_paths: A list of directories that contain sources to translate.
+    files_to_translate: A list of relative paths (relative to source_paths) that
+        point to sources to translate.
+  Returns:
+    None.
+  """
+  j2objc_args.extend(['-sourcepath', ':'.join(source_paths)])
+  j2objc_args.extend(['-d', output_file_path])
+  j2objc_args.extend(files_to_translate)
+  param_file_content = ' '.join(j2objc_args).encode('utf-8')
+  fd = None
+  param_filename = None
+  try:
+    fd, param_filename = tempfile.mkstemp(text=True)
+    os.write(fd, param_file_content)
+  finally:
+    if fd:
+      os.close(fd)
+  try:
+    j2objc_cmd = [java]
+    j2objc_cmd.extend([f_ for f_ in jvm_flags.split(',') if f_])
+    j2objc_cmd.extend(_ADD_EXPORTS)
+    j2objc_cmd.extend(['-cp', j2objc, main_class])
+    j2objc_cmd.append('@%s' % param_filename)
+    subprocess.check_call(j2objc_cmd, stderr=subprocess.STDOUT)
+  finally:
+    if param_filename:
+      os.remove(param_filename)
+
+
+def WriteDepMappingFile(objc_files,
+                        objc_file_root,
+                        output_dependency_mapping_file,
+                        file_open=open):
+  """Scans J2ObjC-translated files and outputs a dependency mapping file.
+
+  The mapping file contains mappings between translated source files and their
+  imported source files scanned from the import and include directives.
+
+  Args:
+    objc_files: A list of ObjC files translated by J2ObjC.
+    objc_file_root: The file path which represents a directory where the
+        generated ObjC files reside.
+    output_dependency_mapping_file: The path of the dependency mapping file to
+        write to.
+    file_open: Reference to the builtin open function so it may be
+        overridden for testing.
+  Raises:
+    RuntimeError: If spawned threads throw errors during processing.
+  Returns:
+    None.
+  """
+  dep_mapping = dict()
+  input_file_queue = queue.Queue()
+  output_dep_mapping_queue = queue.Queue()
+  error_message_queue = queue.Queue()
+  for objc_file in objc_files:
+    input_file_queue.put(os.path.join(objc_file_root, objc_file))
+
+  for _ in range(multiprocessing.cpu_count()):
+    t = threading.Thread(target=_ReadDepMapping, args=(input_file_queue,
+                                                       output_dep_mapping_queue,
+                                                       error_message_queue,
+                                                       objc_file_root,
+                                                       file_open))
+    t.start()
+
+  input_file_queue.join()
+
+  if not error_message_queue.empty():
+    error_messages = list(error_message_queue.queue)
+    raise RuntimeError('\n'.join(error_messages))
+
+  while not output_dep_mapping_queue.empty():
+    entry_file, deps = output_dep_mapping_queue.get()
+    dep_mapping[entry_file] = deps
+
+  with file_open(output_dependency_mapping_file, 'w') as f:
+    for entry in sorted(dep_mapping):
+      for dep in dep_mapping[entry]:
+        f.write(entry + ':' + dep + '\n')
+
+
+def _ReadDepMapping(input_file_queue, output_dep_mapping_queue,
+                    error_message_queue, output_root, file_open=open):
+  while True:
+    try:
+      input_file = input_file_queue.get_nowait()
+    except queue.Empty:
+      # No more work left in the queue.
+      return
+
+    try:
+      deps = set()
+      input_file_name = os.path.splitext(input_file)[0]
+      entry = os.path.relpath(input_file_name, output_root)
+      for file_ext in ['.m', '.h']:
+        with file_open(input_file_name + file_ext, 'r') as f:
+          for line in f:
+            include = _INCLUDE_RE.match(line)
+            if include:
+              include_path = include.group(2)
+              dep = os.path.splitext(include_path)[0]
+              if dep != entry:
+                deps.add(dep)
+
+      output_dep_mapping_queue.put((entry, sorted(deps)))
+    except Exception as e:  # pylint: disable=broad-except
+      error_message_queue.put(str(e))
+    finally:
+      # We need to mark the task done to prevent blocking the main process
+      # indefinitely.
+      input_file_queue.task_done()
+
+
+def WriteArchiveSourceMappingFile(compiled_archive_file_path,
+                                  output_archive_source_mapping_file,
+                                  objc_files,
+                                  file_open=open):
+  """Writes a mapping file between archive file to associated ObjC source files.
+
+  Args:
+    compiled_archive_file_path: The path of the archive file.
+    output_archive_source_mapping_file: A path of the mapping file to write to.
+    objc_files: A list of ObjC files translated by J2ObjC.
+    file_open: Reference to the builtin open function so it may be
+        overridden for testing.
+  Returns:
+    None.
+  """
+  with file_open(output_archive_source_mapping_file, 'w') as f:
+    for objc_file in objc_files:
+      f.write(compiled_archive_file_path + ':' + objc_file + '\n')
+
+
+def _ParseArgs(j2objc_args):
+  """Separate arguments passed to J2ObjC into source files and J2ObjC flags.
+
+  Args:
+    j2objc_args: A list of args to pass to J2ObjC transpiler.
+  Returns:
+    A tuple containing source files and J2ObjC flags
+  """
+  source_files = []
+  flags = []
+  is_next_flag_value = False
+  for j2objc_arg in j2objc_args:
+    if j2objc_arg.startswith('-'):
+      flags.append(j2objc_arg)
+      is_next_flag_value = True
+    elif is_next_flag_value:
+      flags.append(j2objc_arg)
+      is_next_flag_value = False
+    else:
+      source_files.append(j2objc_arg)
+  return (source_files, flags)
+
+
+def _J2ObjcOutputObjcFiles(java_files):
+  """Returns the relative paths of the associated output ObjC source files.
+
+  Args:
+    java_files: The list of Java files to translate.
+  Returns:
+    A list of associated output ObjC source files.
+  """
+  return [os.path.splitext(java_file)[0] + '.m' for java_file in java_files]
+
+
+def UnzipSourceJarSources(source_jars):
+  """Unzips the source jars containing Java source files.
+
+  Args:
+    source_jars: The list of input Java source jars.
+  Returns:
+    A tuple of the temporary output root and a list of root-relative paths of
+    unzipped Java files
+  """
+  srcjar_java_files = []
+  if source_jars:
+    tmp_input_root = tempfile.mkdtemp()
+    for source_jar in source_jars:
+      zip_ref = zipfile.ZipFile(source_jar, 'r')
+      zip_entries = []
+
+      for file_entry in zip_ref.namelist():
+        # We only care about Java source files.
+        if file_entry.endswith('.java'):
+          zip_entries.append(file_entry)
+
+      zip_ref.extractall(tmp_input_root, zip_entries)
+      zip_ref.close()
+      srcjar_java_files.extend(zip_entries)
+
+    return (tmp_input_root, srcjar_java_files)
+  else:
+    return None
+
+
+def RenameGenJarObjcFileRootInFileContent(tmp_objc_file_root,
+                                          j2objc_source_paths,
+                                          gen_src_jar, genjar_objc_files,
+                                          execute=subprocess.check_call):
+  """Renames references to temporary root inside ObjC sources from gen srcjar.
+
+  Args:
+    tmp_objc_file_root: The temporary output root containing ObjC sources.
+    j2objc_source_paths: The source paths used by J2ObjC.
+    gen_src_jar: The path of the gen srcjar.
+    genjar_objc_files: The list of ObjC sources translated from the gen srcjar.
+    execute: The function used to execute shell commands.
+  Returns:
+    None.
+  """
+  if genjar_objc_files:
+    abs_genjar_objc_source_files = [
+        os.path.join(tmp_objc_file_root, genjar_objc_f)
+        for genjar_objc_f in genjar_objc_files
+    ]
+    abs_genjar_objc_header_files = [
+        os.path.join(tmp_objc_file_root,
+                     os.path.splitext(genjar_objc_f)[0] + '.h')
+        for genjar_objc_f in genjar_objc_files
+    ]
+
+    # We execute a command to change all references of the temporary Java root
+    # where we unzipped the gen srcjar sources, to the actual gen srcjar that
+    # contains the original Java sources.
+    cmd = [
+        'sed',
+        '-i',
+        '-e',
+        's|%s/|%s::|g' % (j2objc_source_paths[1], gen_src_jar)
+    ]
+    cmd.extend(abs_genjar_objc_source_files)
+    cmd.extend(abs_genjar_objc_header_files)
+    execute(cmd, stderr=subprocess.STDOUT)
+
+
+def MoveObjcFileToFinalOutputRoot(objc_files,
+                                  tmp_objc_file_root,
+                                  final_objc_file_root,
+                                  suffix,
+                                  os_module=os,
+                                  shutil_module=shutil):
+  """Moves ObjC files from temporary location to the final output location.
+
+  Args:
+    objc_files: The list of objc files to move.
+    tmp_objc_file_root: The temporary output root containing ObjC sources.
+    final_objc_file_root: The final output root.
+    suffix: The suffix of the files to move.
+    os_module: The os python module.
+    shutil_module: The shutil python module.
+  Returns:
+    None.
+  """
+  for objc_file in objc_files:
+    file_with_suffix = os_module.path.splitext(objc_file)[0] + suffix
+    dest_path = os_module.path.join(
+        final_objc_file_root, file_with_suffix)
+    dest_path_dir = os_module.path.dirname(dest_path)
+
+    if not os_module.path.isdir(dest_path_dir):
+      try:
+        os_module.makedirs(dest_path_dir)
+      except OSError as e:
+        if e.errno != errno.EEXIST or not os_module.path.isdir(dest_path_dir):
+          raise
+
+    shutil_module.move(
+        os_module.path.join(tmp_objc_file_root, file_with_suffix),
+        dest_path)
+
+
+def PostJ2ObjcFileProcessing(normal_objc_files, genjar_objc_files,
+                             tmp_objc_file_root, final_objc_file_root,
+                             j2objc_source_paths, gen_src_jar,
+                             output_gen_source_dir, output_gen_header_dir):
+  """Performs cleanups on ObjC files and moves them to final output location.
+
+  Args:
+    normal_objc_files: The list of objc files translated from normal Java files.
+    genjar_objc_files: The list of ObjC sources translated from the gen srcjar.
+    tmp_objc_file_root: The temporary output root containing ObjC sources.
+    final_objc_file_root: The final output root.
+    j2objc_source_paths: The source paths used by J2ObjC.
+    gen_src_jar: The path of the gen srcjar.
+    output_gen_source_dir: The final output directory of ObjC source files
+        translated from gen srcjar. Maybe null.
+    output_gen_header_dir: The final output directory of ObjC header files
+        translated from gen srcjar. Maybe null.
+  Returns:
+    None.
+  """
+  RenameGenJarObjcFileRootInFileContent(tmp_objc_file_root,
+                                        j2objc_source_paths,
+                                        gen_src_jar,
+                                        genjar_objc_files)
+  MoveObjcFileToFinalOutputRoot(normal_objc_files,
+                                tmp_objc_file_root,
+                                final_objc_file_root,
+                                '.m')
+  MoveObjcFileToFinalOutputRoot(normal_objc_files,
+                                tmp_objc_file_root,
+                                final_objc_file_root,
+                                '.h')
+
+  if output_gen_source_dir:
+    MoveObjcFileToFinalOutputRoot(
+        genjar_objc_files,
+        tmp_objc_file_root,
+        output_gen_source_dir,
+        '.m')
+
+  if output_gen_header_dir:
+    MoveObjcFileToFinalOutputRoot(
+        genjar_objc_files,
+        tmp_objc_file_root,
+        output_gen_header_dir,
+        '.h')
+
+
+def GenerateJ2objcMappingFiles(normal_objc_files,
+                               genjar_objc_files,
+                               tmp_objc_file_root,
+                               output_dependency_mapping_file,
+                               output_archive_source_mapping_file,
+                               compiled_archive_file_path):
+  """Generates J2ObjC mapping files.
+
+  Args:
+    normal_objc_files: The list of objc files translated from normal Java files.
+    genjar_objc_files: The list of ObjC sources translated from the gen srcjar.
+    tmp_objc_file_root: The temporary output root containing ObjC sources.
+    output_dependency_mapping_file: The path of the dependency mapping file to
+        write to.
+    output_archive_source_mapping_file: A path of the mapping file to write to.
+    compiled_archive_file_path: The path of the archive file.
+  Returns:
+    None.
+  """
+  WriteDepMappingFile(normal_objc_files + genjar_objc_files,
+                      tmp_objc_file_root,
+                      output_dependency_mapping_file)
+
+  if output_archive_source_mapping_file:
+    WriteArchiveSourceMappingFile(compiled_archive_file_path,
+                                  output_archive_source_mapping_file,
+                                  normal_objc_files + genjar_objc_files)
+
+
+def main():
+  parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
+  parser.add_argument(
+      '--java',
+      required=True,
+      help='The path to the Java executable.')
+  parser.add_argument(
+      '--jvm_flags',
+      default='-Xss4m,-XX:+UseParallelGC',
+      help='A comma-separated list of flags to pass to the JVM.')
+  parser.add_argument(
+      '--j2objc',
+      required=True,
+      help='The path to the J2ObjC deploy jar.')
+  parser.add_argument(
+      '--main_class',
+      required=True,
+      help='The main class of the J2ObjC deploy jar to execute.')
+  # TODO(rduan): Remove, no longer needed.
+  parser.add_argument(
+      '--translated_source_files',
+      required=False,
+      help=('A comma-separated list of file paths where J2ObjC will write the '
+            'translated files to.'))
+  parser.add_argument(
+      '--output_dependency_mapping_file',
+      required=True,
+      help='The file path of the dependency mapping file to write to.')
+  parser.add_argument(
+      '--objc_file_path', '-d',
+      required=True,
+      help=('The file path which represents a directory where the generated '
+            'ObjC files reside.'))
+  parser.add_argument(
+      '--output_archive_source_mapping_file',
+      help='The file path of the mapping file containing mappings between the '
+           'translated source files and the to-be-generated archive file '
+           'compiled from those source files. --compile_archive_file_path must '
+           'be specified if this option is specified.')
+  parser.add_argument(
+      '--compiled_archive_file_path',
+      required=False,
+      help=('The archive file path that will be produced by ObjC compile action'
+            ' later'))
+  # TODO(rduan): Remove this flag once it is fully replaced by flag --src_jars.
+  parser.add_argument(
+      '--gen_src_jar',
+      required=False,
+      help='The jar containing Java sources generated by annotation processor.')
+  parser.add_argument(
+      '--src_jars',
+      required=False,
+      help='The list of Java source jars containing Java sources to translate.')
+  parser.add_argument(
+      '--output_gen_source_dir',
+      required=False,
+      help='The output directory of ObjC source files translated from the gen'
+           ' srcjar')
+  parser.add_argument(
+      '--output_gen_header_dir',
+      required=False,
+      help='The output directory of ObjC header files translated from the gen'
+           ' srcjar')
+
+  args, pass_through_args = parser.parse_known_args()
+  normal_java_files, j2objc_flags = _ParseArgs(pass_through_args)
+  srcjar_java_files = []
+  j2objc_source_paths = [os.getcwd()]
+
+  # Unzip the source jars, so J2ObjC can translate the contained sources.
+  # Also add the temporary directory containing the unzipped sources as a source
+  # path for J2ObjC, so it can find these sources.
+  source_jars = []
+  if args.gen_src_jar:
+    source_jars.append(args.gen_src_jar)
+  if args.src_jars:
+    source_jars.extend(args.src_jars.split(','))
+
+  srcjar_source_tuple = UnzipSourceJarSources(source_jars)
+  if srcjar_source_tuple:
+    j2objc_source_paths.append(srcjar_source_tuple[0])
+    srcjar_java_files = srcjar_source_tuple[1]
+
+  # Run J2ObjC over the normal input Java files and unzipped gen jar Java files.
+  # The output is stored in a temporary directory.
+  tmp_objc_file_root = tempfile.mkdtemp()
+
+  # If we do not generate the header mapping from J2ObjC, we still
+  # need to specify --output-header-mapping, as it signals to J2ObjC that we
+  # are using source paths as import paths, not package paths.
+  # TODO(rduan): Make another flag in J2ObjC to specify using source paths.
+  if '--output-header-mapping' not in j2objc_flags:
+    j2objc_flags.extend(['--output-header-mapping', '/dev/null'])
+
+  RunJ2ObjC(args.java,
+            args.jvm_flags,
+            args.j2objc,
+            args.main_class,
+            tmp_objc_file_root,
+            j2objc_flags,
+            j2objc_source_paths,
+            normal_java_files + srcjar_java_files)
+
+  # Calculate the relative paths of generated objc files.
+  normal_objc_files = _J2ObjcOutputObjcFiles(normal_java_files)
+  genjar_objc_files = _J2ObjcOutputObjcFiles(srcjar_java_files)
+
+  # Generate J2ObjC mapping files needed for distributed builds.
+  GenerateJ2objcMappingFiles(normal_objc_files,
+                             genjar_objc_files,
+                             tmp_objc_file_root,
+                             args.output_dependency_mapping_file,
+                             args.output_archive_source_mapping_file,
+                             args.compiled_archive_file_path)
+
+  # Post J2ObjC-run processing, involving file editing, zipping and moving
+  # files to their final output locations.
+  PostJ2ObjcFileProcessing(
+      normal_objc_files,
+      genjar_objc_files,
+      tmp_objc_file_root,
+      args.objc_file_path,
+      j2objc_source_paths,
+      args.gen_src_jar,
+      args.output_gen_source_dir,
+      args.output_gen_header_dir)
+
+if __name__ == '__main__':
+  main()
diff --git a/tools/objc/BUILD b/tools/objc/BUILD
index 32557d4..b50136d 100644
--- a/tools/objc/BUILD
+++ b/tools/objc/BUILD
@@ -32,6 +32,11 @@
     srcs = ["j2objc_dead_code_pruner.py"],
 )
 
+py_binary(
+    name = "j2objc_dead_code_pruner_binary",
+    srcs = ["j2objc_dead_code_pruner_binary.py"],
+)
+
 objc_library(
     name = "dummy_lib",
     srcs = [
diff --git a/tools/objc/j2objc_dead_code_pruner_binary.py b/tools/objc/j2objc_dead_code_pruner_binary.py
new file mode 100755
index 0000000..00524128
--- /dev/null
+++ b/tools/objc/j2objc_dead_code_pruner_binary.py
@@ -0,0 +1,499 @@
+#!/usr/bin/python3
+
+# Copyright 2015 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A script for J2ObjC dead code removal in Blaze.
+
+This script removes unused J2ObjC-translated classes from compilation and
+linking by:
+  1. Build a class dependency tree among translated source files.
+  2. Use user-provided Java class entry points to get a list of reachable
+     classes.
+  3. Go through all translated source files and rewrite unreachable ones with
+     dummy content.
+"""
+
+import argparse
+import collections
+import multiprocessing
+import os
+import queue
+import re
+import shlex
+import shutil
+import subprocess
+import threading
+
+
+PRUNED_SRC_CONTENT = 'static int DUMMY_unused __attribute__((unused,used)) = 0;'
+
+
+def BuildReachabilityTree(dependency_mapping_files, file_open=open):
+  """Builds a reachability tree using entries from dependency mapping files.
+
+  Args:
+    dependency_mapping_files: A comma separated list of J2ObjC-generated
+        dependency mapping files.
+    file_open: Reference to the builtin open function so it may be
+        overridden for testing.
+  Returns:
+    A dict mapping J2ObjC-generated source files to the corresponding direct
+    dependent source files.
+  """
+  return BuildArtifactSourceTree(dependency_mapping_files, file_open)
+
+
+def BuildHeaderMapping(header_mapping_files, file_open=open):
+  """Builds a mapping between Java classes and J2ObjC-translated header files.
+
+  Args:
+    header_mapping_files: A comma separated list of J2ObjC-generated
+        header mapping files.
+    file_open: Reference to the builtin open function so it may be
+        overridden for testing.
+  Returns:
+    An ordered dict mapping Java class names to corresponding J2ObjC-translated
+    source files.
+  """
+  header_mapping = collections.OrderedDict()
+  for header_mapping_file in header_mapping_files.split(','):
+    with file_open(header_mapping_file, 'r') as f:
+      for line in f:
+        java_class_name = line.strip().split('=')[0]
+        transpiled_file_name = os.path.splitext(line.strip().split('=')[1])[0]
+        header_mapping[java_class_name] = transpiled_file_name
+  return header_mapping
+
+
+def BuildReachableFileSet(entry_classes, reachability_tree, header_mapping,
+                          archive_source_file_mapping=None):
+  """Builds a set of reachable translated files from entry Java classes.
+
+  Args:
+    entry_classes: A comma separated list of Java entry classes.
+    reachability_tree: A dict mapping translated files to their direct
+        dependencies.
+    header_mapping: A dict mapping Java class names to translated source files.
+    archive_source_file_mapping: A dict mapping source files to the associated
+        archive file that contains them.
+  Returns:
+    A set of reachable translated files from the given list of entry classes.
+  Raises:
+    Exception: If there is an entry class that is not being transpiled in this
+        j2objc_library.
+  """
+  transpiled_entry_files = []
+  for entry_class in entry_classes.split(','):
+    if entry_class not in header_mapping:
+      raise Exception(
+          entry_class +
+          ' is not in the transitive Java deps of included ' +
+          'j2objc_library rules.')
+    transpiled_entry_files.append(header_mapping[entry_class])
+
+  # Translated files going into the same static library archive with duplicated
+  # base names also need to be added to the set of entry files.
+  #
+  # This edge case is ignored because we currently cannot correctly perform
+  # dead code removal in this case. The object file entries in static library
+  # archives are named by the base names of the original source files. If two
+  # source files (e.g., foo/bar.m, bar/bar.m) go into the same archive and
+  # share the same base name (bar.m), their object file entries inside the
+  # archive will have the same name (bar.o). We cannot correctly handle this
+  # case because current archive tools (ar, ranlib, etc.) do not handle this
+  # case very well.
+  if archive_source_file_mapping:
+    transpiled_entry_files.extend(_DuplicatedFiles(archive_source_file_mapping))
+
+  # Translated files from package-info.java are also added to the entry files
+  # because they are needed to resolve ObjC class names with prefixes and these
+  # files may also have dependencies.
+  for transpiled_file in reachability_tree:
+    if transpiled_file.endswith('package-info'):
+      transpiled_entry_files.append(transpiled_file)
+
+  reachable_files = set()
+  for transpiled_entry_file in transpiled_entry_files:
+    reachable_files.add(transpiled_entry_file)
+    current_level_deps = []
+    # We need to check if the transpiled file is in the reachability tree
+    # because J2ObjC protos are not analyzed for dead code stripping and
+    # therefore are not in the reachability tree at all.
+    if transpiled_entry_file in reachability_tree:
+      current_level_deps = reachability_tree[transpiled_entry_file]
+    while current_level_deps:
+      next_level_deps = []
+      for dep in current_level_deps:
+        if dep not in reachable_files:
+          reachable_files.add(dep)
+          if dep in reachability_tree:
+            next_level_deps.extend(reachability_tree[dep])
+      current_level_deps = next_level_deps
+  return reachable_files
+
+
+def PruneFiles(input_files, output_files, objc_file_path, reachable_files,
+               file_open=open, file_shutil=shutil):
+  """Copies over translated files and remove the contents of unreachable files.
+
+  Args:
+    input_files: A comma separated list of input source files to prune. It has
+        a one-on-one pair mapping with the output_file list.
+    output_files: A comma separated list of output source files to write pruned
+        source files to. It has a one-on-one pair mapping with the input_file
+        list.
+    objc_file_path: The file path which represents a directory where the
+        generated ObjC files reside.
+    reachable_files: A set of reachable source files.
+    file_open: Reference to the builtin open function so it may be
+        overridden for testing.
+    file_shutil: Reference to the builtin shutil module so it may be
+        overridden for testing.
+  Returns:
+    None.
+  """
+  file_queue = queue.queue()
+  for input_file, output_file in zip(
+      input_files.split(','),
+      output_files.split(',')):
+    file_queue.put((input_file, output_file))
+
+  for _ in range(multiprocessing.cpu_count()):
+    t = threading.Thread(target=_PruneFile, args=(file_queue,
+                                                  reachable_files,
+                                                  objc_file_path,
+                                                  file_open,
+                                                  file_shutil))
+    t.start()
+
+  file_queue.join()
+
+
+def _PruneFile(file_queue, reachable_files, objc_file_path, file_open=open,
+               file_shutil=shutil):
+  while True:
+    try:
+      input_file, output_file = file_queue.get_nowait()
+    except queue.Empty:
+      return
+    file_name = os.path.relpath(os.path.splitext(input_file)[0],
+                                objc_file_path)
+    if file_name in reachable_files:
+      file_shutil.copy(input_file, output_file)
+    else:
+      with file_open(output_file, 'w') as f:
+        # Use a static variable scoped to the source file to suppress
+        # the "has no symbols" linker warning for empty object files.
+        f.write(PRUNED_SRC_CONTENT)
+    file_queue.task_done()
+
+
+def _DuplicatedFiles(archive_source_file_mapping):
+  """Returns a list of file with duplicated base names in each archive file.
+
+  Args:
+    archive_source_file_mapping: A dict mapping source files to the associated
+        archive file that contains them.
+  Returns:
+    A list containing files with duplicated base names.
+  """
+  duplicated_files = []
+  dict_with_duplicates = dict()
+
+  for source_files in archive_source_file_mapping.values():
+    for source_file in source_files:
+      file_basename = os.path.basename(source_file)
+      file_without_ext = os.path.splitext(source_file)[0]
+      if file_basename in dict_with_duplicates:
+        dict_with_duplicates[file_basename].append(file_without_ext)
+      else:
+        dict_with_duplicates[file_basename] = [file_without_ext]
+    for basename in dict_with_duplicates:
+      if len(dict_with_duplicates[basename]) > 1:
+        duplicated_files.extend(dict_with_duplicates[basename])
+    dict_with_duplicates = dict()
+
+  return duplicated_files
+
+
+def BuildArchiveSourceFileMapping(archive_source_mapping_files, file_open):
+  """Builds a mapping between archive files and their associated source files.
+
+  Args:
+    archive_source_mapping_files: A comma separated list of J2ObjC-generated
+        mapping between archive files and their associated source files.
+    file_open: Reference to the builtin open function so it may be
+        overridden for testing.
+  Returns:
+    A dict mapping between archive files and their associated source files.
+  """
+  return BuildArtifactSourceTree(archive_source_mapping_files, file_open)
+
+
+def PruneSourceFiles(input_files, output_files, dependency_mapping_files,
+                     header_mapping_files, entry_classes, objc_file_path,
+                     file_open=open, file_shutil=shutil):
+  """Copies over translated files and remove the contents of unreachable files.
+
+  Args:
+    input_files: A comma separated list of input source files to prune. It has
+        a one-on-one pair mapping with the output_file list.
+    output_files: A comma separated list of output source files to write pruned
+        source files to. It has a one-on-one pair mapping with the input_file
+        list.
+    dependency_mapping_files: A comma separated list of J2ObjC-generated
+        dependency mapping files.
+    header_mapping_files: A comma separated list of J2ObjC-generated
+        header mapping files.
+    entry_classes: A comma separated list of Java entry classes.
+    objc_file_path: The file path which represents a directory where the
+        generated ObjC files reside.
+    file_open: Reference to the builtin open function so it may be
+        overridden for testing.
+    file_shutil: Reference to the builtin shutil module so it may be
+        overridden for testing.
+  """
+  reachability_file_mapping = BuildReachabilityTree(
+      dependency_mapping_files, file_open)
+  header_map = BuildHeaderMapping(header_mapping_files, file_open)
+  reachable_files_set = BuildReachableFileSet(entry_classes,
+                                              reachability_file_mapping,
+                                              header_map)
+  PruneFiles(input_files,
+             output_files,
+             objc_file_path,
+             reachable_files_set,
+             file_open,
+             file_shutil)
+
+
+def MatchObjectNamesInArchive(xcrunwrapper, archive, object_names):
+  """Returns object names matching their identity in an archive file.
+
+  The linker that blaze uses appends an md5 hash to object file
+  names prior to inclusion in the archive file. Thus, object names
+  such as 'foo.o' need to be matched to their appropriate name in
+  the archive file, such as 'foo_<hash>.o'.
+
+  Args:
+    xcrunwrapper: A wrapper script over xcrun.
+    archive: The location of the archive file.
+    object_names: The expected basenames of object files to match,
+        sans extension. For example 'foo' (not 'foo.o').
+  Returns:
+    A list of basenames of matching members of the given archive
+  """
+  ar_contents_cmd = [xcrunwrapper, 'ar', '-t', archive]
+  real_object_names_output = subprocess.check_output(ar_contents_cmd)
+  real_object_names = real_object_names_output.decode('utf-8')
+  expected_object_name_regex = r'^(?:%s)(?:_[0-9a-f]{32}(?:-[0-9]+)?)?\.o$' % (
+      '|'.join([re.escape(name) for name in object_names]))
+  return re.findall(
+      expected_object_name_regex,
+      real_object_names,
+      flags=re.MULTILINE)
+
+
+def PruneArchiveFile(input_archive, output_archive, dummy_archive,
+                     dependency_mapping_files, header_mapping_files,
+                     archive_source_mapping_files, entry_classes, xcrunwrapper,
+                     file_open=open):
+  """Remove unreachable objects from archive file.
+
+  Args:
+    input_archive: The source archive file to prune.
+    output_archive: The location of the pruned archive file.
+    dummy_archive: A dummy archive file that contains no object.
+    dependency_mapping_files: A comma separated list of J2ObjC-generated
+        dependency mapping files.
+    header_mapping_files: A comma separated list of J2ObjC-generated
+        header mapping files.
+    archive_source_mapping_files: A comma separated list of J2ObjC-generated
+        mapping between archive files and their associated source files.
+    entry_classes: A comma separated list of Java entry classes.
+    xcrunwrapper: A wrapper script over xcrun.
+    file_open: Reference to the builtin open function so it may be
+        overridden for testing.
+  """
+  reachability_file_mapping = BuildReachabilityTree(
+      dependency_mapping_files, file_open)
+  header_map = BuildHeaderMapping(header_mapping_files, file_open)
+  archive_source_file_mapping = BuildArchiveSourceFileMapping(
+      archive_source_mapping_files, file_open)
+  reachable_files_set = BuildReachableFileSet(entry_classes,
+                                              reachability_file_mapping,
+                                              header_map,
+                                              archive_source_file_mapping)
+
+  # Copy the current processes' environment, as xcrunwrapper depends on these
+  # variables.
+  cmd_env = dict(os.environ)
+  j2objc_cmd = ''
+  if input_archive in archive_source_file_mapping:
+    source_files = archive_source_file_mapping[input_archive]
+    unreachable_object_names = []
+
+    for source_file in source_files:
+      if os.path.splitext(source_file)[0] not in reachable_files_set:
+        unreachable_object_names.append(
+            os.path.basename(os.path.splitext(source_file)[0]))
+
+    # There are unreachable objects in the archive to prune
+    if unreachable_object_names:
+      # If all objects in the archive are unreachable, just copy over a dummy
+      # archive that contains no object
+      if len(unreachable_object_names) == len(source_files):
+        j2objc_cmd = 'cp %s %s' % (shlex.quote(dummy_archive),
+                                   shlex.quote(output_archive))
+      # Else we need to prune the archive of unreachable objects
+      else:
+        cmd_env['ZERO_AR_DATE'] = '1'
+        # Copy the input archive to the output location
+        j2objc_cmd += 'cp %s %s && ' % (shlex.quote(input_archive),
+                                        shlex.quote(output_archive))
+        # Make the output archive editable
+        j2objc_cmd += 'chmod +w %s && ' % (shlex.quote(output_archive))
+        # Remove the unreachable objects from the archive
+        unreachable_object_names = MatchObjectNamesInArchive(
+            xcrunwrapper, input_archive, unreachable_object_names)
+        j2objc_cmd += '%s ar -d -s %s %s && ' % (
+            shlex.quote(xcrunwrapper),
+            shlex.quote(output_archive),
+            ' '.join(shlex.quote(uon) for uon in unreachable_object_names))
+        # Update the table of content of the archive file
+        j2objc_cmd += '%s ranlib %s' % (shlex.quote(xcrunwrapper),
+                                        shlex.quote(output_archive))
+    # There are no unreachable objects, we just copy over the original archive
+    else:
+      j2objc_cmd = 'cp %s %s' % (shlex.quote(input_archive),
+                                 shlex.quote(output_archive))
+  # The archive cannot be pruned by J2ObjC dead code removal, just copy over
+  # the original archive
+  else:
+    j2objc_cmd = 'cp %s %s' % (shlex.quote(input_archive),
+                               shlex.quote(output_archive))
+
+  try:
+    subprocess.check_output(
+        j2objc_cmd, stderr=subprocess.STDOUT, shell=True, env=cmd_env)
+  except OSError as e:
+    raise Exception(
+        'executing command failed: %s (%s)' % (j2objc_cmd, e.strerror))
+
+  # "Touch" the output file.
+  # Prevents a pre-Xcode-8 bug in which passing zero-date archive files to ld
+  # would cause ld to error.
+  os.utime(output_archive, None)
+
+
+def BuildArtifactSourceTree(files, file_open=open):
+  """Builds a dependency tree using from dependency mapping files.
+
+  Args:
+   files: A comma separated list of dependency mapping files.
+   file_open: Reference to the builtin open function so it may be overridden for
+     testing.
+
+  Returns:
+   A dict mapping build artifacts (possibly generated source files) to the
+   corresponding direct dependent source files.
+  """
+  tree = dict()
+  if not files:
+    return tree
+  for filename in files.split(','):
+    with file_open(filename, 'r') as f:
+      for line in f:
+        entry = line.strip().split(':')[0]
+        dep = line.strip().split(':')[1]
+        if entry in tree:
+          tree[entry].append(dep)
+        else:
+          tree[entry] = [dep]
+  return tree
+
+
+if __name__ == '__main__':
+  parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
+
+  # TODO(rduan): Remove these three flags once J2ObjC compile actions are fully
+  # moved to the edges.
+  parser.add_argument(
+      '--input_files',
+      help=('The comma-separated file paths of translated source files to '
+            'prune.'))
+  parser.add_argument(
+      '--output_files',
+      help='The comma-separated file paths of pruned source files to write to.')
+  parser.add_argument(
+      '--objc_file_path',
+      help='The file path which represents a directory where the generated ObjC'
+      ' files reside')
+
+  parser.add_argument(
+      '--input_archive',
+      help=('The path of the translated archive to prune.'))
+  parser.add_argument(
+      '--output_archive',
+      help='The path of the pruned archive file to write to.')
+  parser.add_argument(
+      '--dummy_archive',
+      help='The dummy archive file that contains no symbol.')
+  parser.add_argument(
+      '--dependency_mapping_files',
+      help='The comma-separated file paths of dependency mapping files.')
+  parser.add_argument(
+      '--header_mapping_files',
+      help='The comma-separated file paths of header mapping files.')
+  parser.add_argument(
+      '--archive_source_mapping_files',
+      help='The comma-separated file paths of archive to source mapping files.'
+           'These mapping files should contain mappings between the '
+           'translated source files and the archive file compiled from those '
+           'source files.')
+  parser.add_argument(
+      '--entry_classes',
+      help=('The comma-separated list of Java entry classes to be used as entry'
+            ' point of the dead code analysis.'))
+  parser.add_argument(
+      '--xcrunwrapper',
+      help=('The xcrun wrapper script.'))
+
+  args = parser.parse_args()
+
+  if not args.entry_classes:
+    raise Exception('J2objC dead code removal is on but no entry class is ',
+                    'specified in any j2objc_library targets in the transitive',
+                    ' closure')
+  if args.input_archive and args.output_archive:
+    PruneArchiveFile(
+        args.input_archive,
+        args.output_archive,
+        args.dummy_archive,
+        args.dependency_mapping_files,
+        args.header_mapping_files,
+        args.archive_source_mapping_files,
+        args.entry_classes,
+        args.xcrunwrapper)
+  else:
+    # TODO(rduan): Remove once J2ObjC compile actions are fully moved to the
+    # edges.
+    PruneSourceFiles(
+        args.input_files,
+        args.output_files,
+        args.dependency_mapping_files,
+        args.header_mapping_files,
+        args.entry_classes,
+        args.objc_file_path)