| #!/usr/bin/env python3 | |
| # | |
| # Copyright 2013 The Chromium Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Instruments classes and jar files. | |
| This script corresponds to the 'jacoco_instr' action in the Java build process. | |
| Depending on whether jacoco_instrument is set, the 'jacoco_instr' action will | |
| call the instrument command which accepts a jar and instruments it using | |
| jacococli.jar. | |
| """ | |
| from __future__ import print_function | |
| import argparse | |
| import json | |
| import os | |
| import shutil | |
| import sys | |
| import tempfile | |
| import zipfile | |
| from util import build_utils | |
| def _AddArguments(parser): | |
| """Adds arguments related to instrumentation to parser. | |
| Args: | |
| parser: ArgumentParser object. | |
| """ | |
| parser.add_argument( | |
| '--input-path', | |
| required=True, | |
| help='Path to input file(s). Either the classes ' | |
| 'directory, or the path to a jar.') | |
| parser.add_argument( | |
| '--output-path', | |
| required=True, | |
| help='Path to output final file(s) to. Either the ' | |
| 'final classes directory, or the directory in ' | |
| 'which to place the instrumented/copied jar.') | |
| parser.add_argument( | |
| '--sources-json-file', | |
| required=True, | |
| help='File to create with the list of source directories ' | |
| 'and input path.') | |
| parser.add_argument( | |
| '--java-sources-file', | |
| required=True, | |
| help='File containing newline-separated .java paths') | |
| parser.add_argument( | |
| '--jacococli-jar', required=True, help='Path to jacococli.jar.') | |
| parser.add_argument( | |
| '--files-to-instrument', | |
| help='Path to a file containing which source files are affected.') | |
| def _GetSourceDirsFromSourceFiles(source_files): | |
| """Returns list of directories for the files in |source_files|. | |
| Args: | |
| source_files: List of source files. | |
| Returns: | |
| List of source directories. | |
| """ | |
| return list(set(os.path.dirname(source_file) for source_file in source_files)) | |
| def _CreateSourcesJsonFile(source_dirs, input_path, sources_json_file, | |
| src_root): | |
| """Adds all normalized source directories and input path to | |
| |sources_json_file|. | |
| Args: | |
| source_dirs: List of source directories. | |
| input_path: The input path to non-instrumented class files. | |
| sources_json_file: File into which to write the list of source directories | |
| and input path. | |
| src_root: Root which sources added to the file should be relative to. | |
| Returns: | |
| An exit code. | |
| """ | |
| src_root = os.path.abspath(src_root) | |
| relative_sources = [] | |
| for s in source_dirs: | |
| abs_source = os.path.abspath(s) | |
| if abs_source[:len(src_root)] != src_root: | |
| print('Error: found source directory not under repository root: %s %s' % | |
| (abs_source, src_root)) | |
| return 1 | |
| rel_source = os.path.relpath(abs_source, src_root) | |
| relative_sources.append(rel_source) | |
| data = {} | |
| data['source_dirs'] = relative_sources | |
| data['input_path'] = [] | |
| if input_path: | |
| data['input_path'].append(os.path.abspath(input_path)) | |
| with open(sources_json_file, 'w') as f: | |
| json.dump(data, f) | |
| def _GetAffectedClasses(jar_file, source_files): | |
| """Gets affected classes by affected source files to a jar. | |
| Args: | |
| jar_file: The jar file to get all members. | |
| source_files: The list of affected source files. | |
| Returns: | |
| A tuple of affected classes and unaffected members. | |
| """ | |
| with zipfile.ZipFile(jar_file) as f: | |
| members = f.namelist() | |
| affected_classes = [] | |
| unaffected_members = [] | |
| for member in members: | |
| if not member.endswith('.class'): | |
| unaffected_members.append(member) | |
| continue | |
| is_affected = False | |
| index = member.find('$') | |
| if index == -1: | |
| index = member.find('.class') | |
| for source_file in source_files: | |
| if source_file.endswith(member[:index] + '.java'): | |
| affected_classes.append(member) | |
| is_affected = True | |
| break | |
| if not is_affected: | |
| unaffected_members.append(member) | |
| return affected_classes, unaffected_members | |
| def _InstrumentClassFiles(instrument_cmd, | |
| input_path, | |
| output_path, | |
| temp_dir, | |
| affected_source_files=None): | |
| """Instruments class files from input jar. | |
| Args: | |
| instrument_cmd: JaCoCo instrument command. | |
| input_path: The input path to non-instrumented jar. | |
| output_path: The output path to instrumented jar. | |
| temp_dir: The temporary directory. | |
| affected_source_files: The affected source file paths to input jar. | |
| Default is None, which means instrumenting all class files in jar. | |
| """ | |
| affected_classes = None | |
| unaffected_members = None | |
| if affected_source_files: | |
| affected_classes, unaffected_members = _GetAffectedClasses( | |
| input_path, affected_source_files) | |
| # Extract affected class files. | |
| with zipfile.ZipFile(input_path) as f: | |
| f.extractall(temp_dir, affected_classes) | |
| instrumented_dir = os.path.join(temp_dir, 'instrumented') | |
| # Instrument extracted class files. | |
| instrument_cmd.extend([temp_dir, '--dest', instrumented_dir]) | |
| build_utils.CheckOutput(instrument_cmd) | |
| if affected_source_files and unaffected_members: | |
| # Extract unaffected members to instrumented_dir. | |
| with zipfile.ZipFile(input_path) as f: | |
| f.extractall(instrumented_dir, unaffected_members) | |
| # Zip all files to output_path | |
| build_utils.ZipDir(output_path, instrumented_dir) | |
| def _RunInstrumentCommand(parser): | |
| """Instruments class or Jar files using JaCoCo. | |
| Args: | |
| parser: ArgumentParser object. | |
| Returns: | |
| An exit code. | |
| """ | |
| args = parser.parse_args() | |
| source_files = [] | |
| if args.java_sources_file: | |
| source_files.extend(build_utils.ReadSourcesList(args.java_sources_file)) | |
| with build_utils.TempDir() as temp_dir: | |
| instrument_cmd = build_utils.JavaCmd() + [ | |
| '-jar', args.jacococli_jar, 'instrument' | |
| ] | |
| if not args.files_to_instrument: | |
| _InstrumentClassFiles(instrument_cmd, args.input_path, args.output_path, | |
| temp_dir) | |
| else: | |
| affected_files = build_utils.ReadSourcesList(args.files_to_instrument) | |
| source_set = set(source_files) | |
| affected_source_files = [f for f in affected_files if f in source_set] | |
| # Copy input_path to output_path and return if no source file affected. | |
| if not affected_source_files: | |
| shutil.copyfile(args.input_path, args.output_path) | |
| # Create a dummy sources_json_file. | |
| _CreateSourcesJsonFile([], None, args.sources_json_file, | |
| build_utils.DIR_SOURCE_ROOT) | |
| return 0 | |
| else: | |
| _InstrumentClassFiles(instrument_cmd, args.input_path, args.output_path, | |
| temp_dir, affected_source_files) | |
| source_dirs = _GetSourceDirsFromSourceFiles(source_files) | |
| # TODO(GYP): In GN, we are passed the list of sources, detecting source | |
| # directories, then walking them to re-establish the list of sources. | |
| # This can obviously be simplified! | |
| _CreateSourcesJsonFile(source_dirs, args.input_path, args.sources_json_file, | |
| build_utils.DIR_SOURCE_ROOT) | |
| return 0 | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| _AddArguments(parser) | |
| _RunInstrumentCommand(parser) | |
| if __name__ == '__main__': | |
| sys.exit(main()) |