Ben Murdoch | 097c5b2 | 2016-05-18 11:27:45 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright 2013 The Chromium Authors. All rights reserved. |
| 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
| 7 | """Instruments classes and jar files. |
| 8 | |
| 9 | This script corresponds to the 'emma_instr' action in the java build process. |
| 10 | Depending on whether emma_instrument is set, the 'emma_instr' action will either |
| 11 | call the instrument command or the copy command. |
| 12 | |
| 13 | Possible commands are: |
| 14 | - instrument_jar: Accepts a jar and instruments it using emma.jar. |
| 15 | - copy: Called when EMMA coverage is not enabled. This allows us to make |
| 16 | this a required step without necessarily instrumenting on every build. |
| 17 | Also removes any stale coverage files. |
| 18 | """ |
| 19 | |
| 20 | import collections |
| 21 | import json |
| 22 | import os |
| 23 | import shutil |
| 24 | import sys |
| 25 | import tempfile |
| 26 | |
| 27 | sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir)) |
| 28 | from pylib.utils import command_option_parser |
| 29 | |
| 30 | from util import build_utils |
| 31 | |
| 32 | |
| 33 | def _AddCommonOptions(option_parser): |
| 34 | """Adds common options to |option_parser|.""" |
| 35 | build_utils.AddDepfileOption(option_parser) |
| 36 | option_parser.add_option('--input-path', |
| 37 | help=('Path to input file(s). Either the classes ' |
| 38 | 'directory, or the path to a jar.')) |
| 39 | option_parser.add_option('--output-path', |
| 40 | help=('Path to output final file(s) to. Either the ' |
| 41 | 'final classes directory, or the directory in ' |
| 42 | 'which to place the instrumented/copied jar.')) |
| 43 | option_parser.add_option('--stamp', help='Path to touch when done.') |
| 44 | option_parser.add_option('--coverage-file', |
| 45 | help='File to create with coverage metadata.') |
| 46 | option_parser.add_option('--sources-list-file', |
| 47 | help='File to create with the list of sources.') |
| 48 | |
| 49 | |
| 50 | def _AddInstrumentOptions(option_parser): |
| 51 | """Adds options related to instrumentation to |option_parser|.""" |
| 52 | _AddCommonOptions(option_parser) |
| 53 | option_parser.add_option('--source-dirs', |
| 54 | help='Space separated list of source directories. ' |
| 55 | 'source-files should not be specified if ' |
| 56 | 'source-dirs is specified') |
| 57 | option_parser.add_option('--source-files', |
| 58 | help='Space separated list of source files. ' |
| 59 | 'source-dirs should not be specified if ' |
| 60 | 'source-files is specified') |
| 61 | option_parser.add_option('--src-root', |
| 62 | help='Root of the src repository.') |
| 63 | option_parser.add_option('--emma-jar', |
| 64 | help='Path to emma.jar.') |
| 65 | option_parser.add_option( |
| 66 | '--filter-string', default='', |
| 67 | help=('Filter string consisting of a list of inclusion/exclusion ' |
| 68 | 'patterns separated with whitespace and/or comma.')) |
| 69 | |
| 70 | |
| 71 | def _RunCopyCommand(_command, options, _, option_parser): |
| 72 | """Copies the jar from input to output locations. |
| 73 | |
| 74 | Also removes any old coverage/sources file. |
| 75 | |
| 76 | Args: |
| 77 | command: String indicating the command that was received to trigger |
| 78 | this function. |
| 79 | options: optparse options dictionary. |
| 80 | args: List of extra args from optparse. |
| 81 | option_parser: optparse.OptionParser object. |
| 82 | |
| 83 | Returns: |
| 84 | An exit code. |
| 85 | """ |
| 86 | if not (options.input_path and options.output_path and |
| 87 | options.coverage_file and options.sources_list_file): |
| 88 | option_parser.error('All arguments are required.') |
| 89 | |
| 90 | if os.path.exists(options.coverage_file): |
| 91 | os.remove(options.coverage_file) |
| 92 | if os.path.exists(options.sources_list_file): |
| 93 | os.remove(options.sources_list_file) |
| 94 | |
| 95 | shutil.copy(options.input_path, options.output_path) |
| 96 | |
| 97 | if options.stamp: |
| 98 | build_utils.Touch(options.stamp) |
| 99 | |
| 100 | if options.depfile: |
| 101 | build_utils.WriteDepfile(options.depfile, |
| 102 | build_utils.GetPythonDependencies()) |
| 103 | |
| 104 | |
| 105 | def _GetSourceDirsFromSourceFiles(source_files_string): |
| 106 | """Returns list of directories for the files in |source_files_string|. |
| 107 | |
| 108 | Args: |
| 109 | source_files_string: String generated from GN or GYP containing the list |
| 110 | of source files. |
| 111 | |
| 112 | Returns: |
| 113 | List of source directories. |
| 114 | """ |
| 115 | source_files = build_utils.ParseGypList(source_files_string) |
| 116 | return list(set(os.path.dirname(source_file) for source_file in source_files)) |
| 117 | |
| 118 | |
| 119 | def _CreateSourcesListFile(source_dirs, sources_list_file, src_root): |
| 120 | """Adds all normalized source directories to |sources_list_file|. |
| 121 | |
| 122 | Args: |
| 123 | source_dirs: List of source directories. |
| 124 | sources_list_file: File into which to write the JSON list of sources. |
| 125 | src_root: Root which sources added to the file should be relative to. |
| 126 | |
| 127 | Returns: |
| 128 | An exit code. |
| 129 | """ |
| 130 | src_root = os.path.abspath(src_root) |
| 131 | relative_sources = [] |
| 132 | for s in source_dirs: |
| 133 | abs_source = os.path.abspath(s) |
| 134 | if abs_source[:len(src_root)] != src_root: |
| 135 | print ('Error: found source directory not under repository root: %s %s' |
| 136 | % (abs_source, src_root)) |
| 137 | return 1 |
| 138 | rel_source = os.path.relpath(abs_source, src_root) |
| 139 | |
| 140 | relative_sources.append(rel_source) |
| 141 | |
| 142 | with open(sources_list_file, 'w') as f: |
| 143 | json.dump(relative_sources, f) |
| 144 | |
| 145 | |
| 146 | def _RunInstrumentCommand(_command, options, _, option_parser): |
| 147 | """Instruments jar files using EMMA. |
| 148 | |
| 149 | Args: |
| 150 | command: String indicating the command that was received to trigger |
| 151 | this function. |
| 152 | options: optparse options dictionary. |
| 153 | args: List of extra args from optparse. |
| 154 | option_parser: optparse.OptionParser object. |
| 155 | |
| 156 | Returns: |
| 157 | An exit code. |
| 158 | """ |
| 159 | if not (options.input_path and options.output_path and |
| 160 | options.coverage_file and options.sources_list_file and |
| 161 | (options.source_files or options.source_dirs) and |
| 162 | options.src_root and options.emma_jar): |
| 163 | option_parser.error('All arguments are required.') |
| 164 | |
| 165 | if os.path.exists(options.coverage_file): |
| 166 | os.remove(options.coverage_file) |
| 167 | temp_dir = tempfile.mkdtemp() |
| 168 | try: |
| 169 | cmd = ['java', '-cp', options.emma_jar, |
| 170 | 'emma', 'instr', |
| 171 | '-ip', options.input_path, |
| 172 | '-ix', options.filter_string, |
| 173 | '-d', temp_dir, |
| 174 | '-out', options.coverage_file, |
| 175 | '-m', 'fullcopy'] |
| 176 | build_utils.CheckOutput(cmd) |
| 177 | |
| 178 | # File is not generated when filter_string doesn't match any files. |
| 179 | if not os.path.exists(options.coverage_file): |
| 180 | build_utils.Touch(options.coverage_file) |
| 181 | |
| 182 | temp_jar_dir = os.path.join(temp_dir, 'lib') |
| 183 | jars = os.listdir(temp_jar_dir) |
| 184 | if len(jars) != 1: |
| 185 | print('Error: multiple output files in: %s' % (temp_jar_dir)) |
| 186 | return 1 |
| 187 | |
| 188 | # Delete output_path first to avoid modifying input_path in the case where |
| 189 | # input_path is a hardlink to output_path. http://crbug.com/571642 |
| 190 | if os.path.exists(options.output_path): |
| 191 | os.unlink(options.output_path) |
| 192 | shutil.move(os.path.join(temp_jar_dir, jars[0]), options.output_path) |
| 193 | finally: |
| 194 | shutil.rmtree(temp_dir) |
| 195 | |
| 196 | if options.source_dirs: |
| 197 | source_dirs = build_utils.ParseGypList(options.source_dirs) |
| 198 | else: |
| 199 | source_dirs = _GetSourceDirsFromSourceFiles(options.source_files) |
| 200 | _CreateSourcesListFile(source_dirs, options.sources_list_file, |
| 201 | options.src_root) |
| 202 | |
| 203 | if options.stamp: |
| 204 | build_utils.Touch(options.stamp) |
| 205 | |
| 206 | if options.depfile: |
| 207 | build_utils.WriteDepfile(options.depfile, |
| 208 | build_utils.GetPythonDependencies()) |
| 209 | |
| 210 | return 0 |
| 211 | |
| 212 | |
| 213 | CommandFunctionTuple = collections.namedtuple( |
| 214 | 'CommandFunctionTuple', ['add_options_func', 'run_command_func']) |
| 215 | VALID_COMMANDS = { |
| 216 | 'copy': CommandFunctionTuple(_AddCommonOptions, |
| 217 | _RunCopyCommand), |
| 218 | 'instrument_jar': CommandFunctionTuple(_AddInstrumentOptions, |
| 219 | _RunInstrumentCommand), |
| 220 | } |
| 221 | |
| 222 | |
| 223 | def main(): |
| 224 | option_parser = command_option_parser.CommandOptionParser( |
| 225 | commands_dict=VALID_COMMANDS) |
| 226 | command_option_parser.ParseAndExecute(option_parser) |
| 227 | |
| 228 | |
| 229 | if __name__ == '__main__': |
| 230 | sys.exit(main()) |