Move j2objc helper scripts into open-source Bazel. -- MOS_MIGRATED_REVID=100490916
diff --git a/tools/BUILD b/tools/BUILD index 4aa82c9..8e1ea84 100644 --- a/tools/BUILD +++ b/tools/BUILD
@@ -12,6 +12,7 @@ "//tools/jdk:srcs", "//tools/genrule:srcs", "//tools/cpp:srcs", + "//tools/j2objc:srcs", "//tools/objc:srcs", "//tools/test:srcs", "//tools/python:srcs", @@ -26,6 +27,7 @@ "//tools/jdk:package-srcs", "//tools/genrule:srcs", "//tools/cpp:srcs", + "//tools/j2objc:srcs", "//tools/objc:srcs", "//tools/test:srcs", "//tools/python:srcs",
diff --git a/tools/j2objc/BUILD b/tools/j2objc/BUILD new file mode 100644 index 0000000..b0f68cc --- /dev/null +++ b/tools/j2objc/BUILD
@@ -0,0 +1,13 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files(glob(["**"])) + +filegroup( + name = "srcs", + srcs = glob(["**"]), +) + +py_library( + name = "j2objc_wrapper", + srcs = ["j2objc_wrapper.py"], +)
diff --git a/tools/j2objc/j2objc_wrapper.py b/tools/j2objc/j2objc_wrapper.py new file mode 100755 index 0000000..c3e5778 --- /dev/null +++ b/tools/j2objc/j2objc_wrapper.py
@@ -0,0 +1,163 @@ +#!/usr/bin/python2.7 + +# Copyright 2015 Google Inc. 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 multiprocessing +import os +import Queue +import re +import subprocess +import threading + +INCLUDE_RE = re.compile('#(include|import) "([^"]+)"') + + +def RunJ2ObjC(java, jvm_flags, j2objc, main_class, flags): + """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. + flags: A list of flags to pass to J2ObjC transpiler. + Returns: + None. + """ + j2objc_args = [java] + j2objc_args.extend(filter(None, jvm_flags.split(','))) + j2objc_args.extend(['-cp', j2objc, main_class]) + j2objc_args.extend(flags) + + subprocess.check_call(j2objc_args, stderr=subprocess.STDOUT) + + +def WriteDepMappingFile(translated_source_files, + objc_file_path, + 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: + translated_source_files: A comma-separated list of files translated by + J2ObjC. + objc_file_path: 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. + Returns: + None. + """ + dep_mapping = dict() + input_file_queue = Queue.Queue() + output_dep_mapping_queue = Queue.Queue() + for output_file in translated_source_files.split(','): + input_file_queue.put(output_file) + + for _ in xrange(multiprocessing.cpu_count()): + t = threading.Thread(target=_ReadDepMapping, args=(input_file_queue, + output_dep_mapping_queue, + objc_file_path, + file_open)) + t.start() + + input_file_queue.join() + + while not output_dep_mapping_queue.empty(): + entry_file, deps = output_dep_mapping_queue.get() + dep_mapping[entry_file] = deps + + f = file_open(output_dependency_mapping_file, 'w') + for entry in sorted(dep_mapping): + for dep in dep_mapping[entry]: + f.write(entry + ':' + dep + '\n') + f.close() + + +def _ReadDepMapping(input_file_queue, output_dep_mapping_queue, objc_file_path, + file_open=open): + while True: + try: + input_file = input_file_queue.get_nowait() + except Queue.Empty: + return + deps = [] + entry = os.path.relpath(os.path.splitext(input_file)[0], objc_file_path) + with file_open(input_file, '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.append(dep) + output_dep_mapping_queue.put((entry, deps)) + input_file_queue.task_done() + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + '--java', + required=True, + help='The path to the Java executable.') + parser.add_argument( + '--jvm_flags', + default='', + 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.') + parser.add_argument( + '--translated_source_files', + required=True, + 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', + required=True, + help=('The file path which represents a directory where the generated ' + 'ObjC files reside.')) + args, pass_through_flags = parser.parse_known_args() + + RunJ2ObjC(args.java, + args.jvm_flags, + args.j2objc, + args.main_class, + pass_through_flags) + WriteDepMappingFile(args.translated_source_files, + args.objc_file_path, + args.output_dependency_mapping_file)
diff --git a/tools/objc/BUILD b/tools/objc/BUILD index 2033ce7..7f87137 100644 --- a/tools/objc/BUILD +++ b/tools/objc/BUILD
@@ -79,9 +79,7 @@ ) # TODO(bazel-team): Open-source the script once J2ObjC support is open-sourced. -genrule( +py_library( name = "j2objc_dead_code_pruner", - srcs = [], - outs = ["dummy_dead_code_pruner.py"], - cmd = "touch $(OUTS)", + srcs = ["j2objc_dead_code_pruner.py"], )
diff --git a/tools/objc/j2objc_dead_code_pruner.py b/tools/objc/j2objc_dead_code_pruner.py new file mode 100755 index 0000000..546a9ef --- /dev/null +++ b/tools/objc/j2objc_dead_code_pruner.py
@@ -0,0 +1,258 @@ +#!/usr/bin/python2.7 + +# Copyright 2015 Google Inc. 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 +from collections import OrderedDict +import multiprocessing +import os +import Queue +import shutil +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. + """ + tree = dict() + for dependency_mapping_file in dependency_mapping_files.split(','): + with file_open(dependency_mapping_file, '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 + + +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 = 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): + """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. + 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. + """ + reachable_files = set() + 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_file_name = header_mapping[entry_class] + reachable_files.add(transpiled_file_name) + current_level_deps = reachability_tree[transpiled_file_name] + 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 xrange(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) + # Translated files from package-info.java are also preserved because + # they are needed to resolve ObjC class names with prefixes. + if file_name in reachable_files or file_name.endswith('package-info'): + file_shutil.copy(input_file, output_file) + else: + f = file_open(output_file, 'w') + # Use a static variable scoped to the source file to supress + # the "has no symbols" linker warning for empty object files. + f.write(PRUNED_SRC_CONTENT) + f.close() + file_queue.task_done() + + +def PruneDeadCode(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. + Returns: + None. + """ + 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) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument( + '--input_files', + required=True, + help=('The comma-separated file paths of translated source files to ' + 'prune.')) + parser.add_argument( + '--output_files', + required=True, + help='The comma-separated file paths of pruned source files to write to.') + parser.add_argument( + '--dependency_mapping_files', + required=True, + help='The comma-separated file paths of dependency mapping files.') + parser.add_argument( + '--header_mapping_files', + required=True, + help='The comma-separated file paths of header mapping files.') + parser.add_argument( + '--entry_classes', + required=True, + help=('The comma-separated list of Java entry classes to be used as entry' + ' point of the dead code anlysis.')) + parser.add_argument( + '--objc_file_path', + required=True, + help='The file path which represents a directory where the generated ObjC' + ' files reside') + 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') + PruneDeadCode( + args.input_files, + args.output_files, + args.dependency_mapping_files, + args.header_mapping_files, + args.entry_classes, + args.objc_file_path)