#!/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())