emma_instr.py revision 3551c9c881056c480085172ff9840cab31610854
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
9This script corresponds to the 'emma_instr' action in the java build process.
10Depending on whether emma_instrument is set, the 'emma_instr' action will either
11call one of the instrument commands, or the copy command.
12
13Possible commands are:
14- instrument_jar: Accepts a jar and instruments it using emma.jar.
15- instrument_classes: Accepts a directory contains java classes and instruments
16      it using emma.jar.
17- copy: Triggered instead of an instrumentation command when we don't have EMMA
18      coverage enabled. This allows us to make this a required step without
19      necessarily instrumenting on every build.
20"""
21
22import collections
23import json
24import os
25import shutil
26import sys
27import tempfile
28
29sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
30from pylib.utils import command_option_parser
31
32from util import build_utils
33
34
35def _AddCommonOptions(option_parser):
36  """Adds common options to |option_parser|."""
37  option_parser.add_option('--input-path',
38                           help=('Path to input file(s). Either the classes '
39                                 'directory, or the path to a jar.'))
40  option_parser.add_option('--output-path',
41                           help=('Path to output final file(s) to. Either the '
42                                 'final classes directory, or the directory in '
43                                 'which to place the instrumented/copied jar.'))
44  option_parser.add_option('--stamp', help='Path to touch when done.')
45
46
47def _AddInstrumentOptions(option_parser):
48  """Adds options related to instrumentation to |option_parser|."""
49  _AddCommonOptions(option_parser)
50  option_parser.add_option('--coverage-file',
51                           help='File to create with coverage metadata.')
52  option_parser.add_option('--sources-file',
53                           help='File to create with the list of sources.')
54  option_parser.add_option('--sources',
55                           help='Space separated list of sources.')
56  option_parser.add_option('--src-root',
57                           help='Root of the src repository.')
58  option_parser.add_option('--emma-jar',
59                           help='Path to emma.jar.')
60
61
62def _RunCopyCommand(command, options, args, option_parser):
63  """Just copies the jar from input to output locations.
64
65  Args:
66    command: String indicating the command that was received to trigger
67        this function.
68    options: optparse options dictionary.
69    args: List of extra args from optparse.
70    option_parser: optparse.OptionParser object.
71
72  Returns:
73    An exit code.
74  """
75  if not (options.input_path and options.output_path):
76    option_parser.error('All arguments are required.')
77
78  if os.path.isdir(options.input_path):
79    shutil.rmtree(options.output_path, ignore_errors=True)
80    shutil.copytree(options.input_path, options.output_path)
81  else:
82    shutil.copy(options.input_path, options.output_path)
83
84  if options.stamp:
85    build_utils.Touch(options.stamp)
86
87
88def _CreateSourcesFile(sources_string, sources_file, src_root):
89  """Adds all normalized source directories to |sources_file|.
90
91  Args:
92    sources_string: String generated from gyp containing the list of sources.
93    sources_file: File into which to write the JSON list of sources.
94    src_root: Root which sources added to the file should be relative to.
95
96  Returns:
97    An exit code.
98  """
99  src_root = os.path.abspath(src_root)
100  sources = build_utils.ParseGypList(sources_string)
101  relative_sources = []
102  for s in sources:
103    abs_source = os.path.abspath(s)
104    if abs_source[:len(src_root)] != src_root:
105      print ('Error: found source directory not under repository root: %s %s'
106             % (abs_source, src_root))
107      return 1
108    rel_source = os.path.relpath(abs_source, src_root)
109
110    relative_sources.append(rel_source)
111
112  with open(sources_file, 'w') as f:
113    json.dump(relative_sources, f)
114
115
116def _RunInstrumentCommand(command, options, args, option_parser):
117  """Instruments the classes/jar files using EMMA.
118
119  Args:
120    command: 'instrument_jar' or 'instrument_classes'. This distinguishes
121        whether we copy the output from the created lib/ directory, or classes/
122        directory.
123    options: optparse options dictionary.
124    args: List of extra args from optparse.
125    option_parser: optparse.OptionParser object.
126
127  Returns:
128    An exit code.
129  """
130  if not (options.input_path and options.output_path and
131          options.coverage_file and options.sources_file and options.sources and
132          options.src_root and options.emma_jar):
133    option_parser.error('All arguments are required.')
134
135  coverage_file = os.path.join(os.path.dirname(options.output_path),
136                               options.coverage_file)
137  sources_file = os.path.join(os.path.dirname(options.output_path),
138                              options.sources_file)
139  temp_dir = tempfile.mkdtemp()
140  try:
141    # TODO(gkanwar): Add '-ix' option to filter out useless classes.
142    build_utils.CheckCallDie(['java', '-cp', options.emma_jar,
143                              'emma', 'instr',
144                              '-ip', options.input_path,
145                              '-d', temp_dir,
146                              '-out', coverage_file,
147                              '-m', 'fullcopy'], suppress_output=True)
148
149    if command == 'instrument_jar':
150      for jar in os.listdir(os.path.join(temp_dir, 'lib')):
151        shutil.copy(os.path.join(temp_dir, 'lib', jar),
152                    options.output_path)
153    else:  # 'instrument_classes'
154      if os.path.isdir(options.output_path):
155        shutil.rmtree(options.output_path, ignore_errors=True)
156      shutil.copytree(os.path.join(temp_dir, 'classes'),
157                      options.output_path)
158  finally:
159    shutil.rmtree(temp_dir)
160
161  _CreateSourcesFile(options.sources, sources_file, options.src_root)
162
163  if options.stamp:
164    build_utils.Touch(options.stamp)
165
166  return 0
167
168
169CommandFunctionTuple = collections.namedtuple(
170    'CommandFunctionTuple', ['add_options_func', 'run_command_func'])
171VALID_COMMANDS = {
172    'copy': CommandFunctionTuple(_AddCommonOptions,
173                                 _RunCopyCommand),
174    'instrument_jar': CommandFunctionTuple(_AddInstrumentOptions,
175                                           _RunInstrumentCommand),
176    'instrument_classes': CommandFunctionTuple(_AddInstrumentOptions,
177                                               _RunInstrumentCommand),
178}
179
180
181def main(argv):
182  option_parser = command_option_parser.CommandOptionParser(
183      commands_dict=VALID_COMMANDS)
184  command_option_parser.ParseAndExecute(option_parser)
185
186
187if __name__ == '__main__':
188  sys.exit(main(sys.argv))
189