Update j2objc scripts to be `py_binary` targets.

PiperOrigin-RevId: 436904987
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)