| #!/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. |
| """ |
| 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 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 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) |
| if file_name in reachable_files: |
| 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(fromfile_prefix_chars='@') |
| 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) |