Home | History | Annotate | Download | only in gyp
      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 one of the instrument commands, or the copy command.
     12 
     13 Possible commands are:
     14 - instrument_jar: Accepts a jar and instruments it using emma.jar.
     15 - instrument_classes: Accepts a directory containing java classes and
     16       instruments it using emma.jar.
     17 - copy: Called when EMMA coverage is not enabled. This allows us to make
     18       this a required step without necessarily instrumenting on every build.
     19       Also removes any stale coverage files.
     20 """
     21 
     22 import collections
     23 import json
     24 import os
     25 import shutil
     26 import sys
     27 import tempfile
     28 
     29 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
     30 from pylib.utils import command_option_parser
     31 
     32 from util import build_utils
     33 
     34 
     35 def _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   option_parser.add_option('--coverage-file',
     46                            help='File to create with coverage metadata.')
     47   option_parser.add_option('--sources-file',
     48                            help='File to create with the list of sources.')
     49 
     50 
     51 def _AddInstrumentOptions(option_parser):
     52   """Adds options related to instrumentation to |option_parser|."""
     53   _AddCommonOptions(option_parser)
     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   option_parser.add_option(
     61       '--filter-string', default='',
     62       help=('Filter string consisting of a list of inclusion/exclusion '
     63             'patterns separated with whitespace and/or comma.'))
     64 
     65 
     66 def _RunCopyCommand(command, options, args, option_parser):
     67   """Copies the jar from input to output locations.
     68 
     69   Also removes any old coverage/sources file.
     70 
     71   Args:
     72     command: String indicating the command that was received to trigger
     73         this function.
     74     options: optparse options dictionary.
     75     args: List of extra args from optparse.
     76     option_parser: optparse.OptionParser object.
     77 
     78   Returns:
     79     An exit code.
     80   """
     81   if not (options.input_path and options.output_path and
     82           options.coverage_file and options.sources_file):
     83     option_parser.error('All arguments are required.')
     84 
     85   coverage_file = os.path.join(os.path.dirname(options.output_path),
     86                                options.coverage_file)
     87   sources_file = os.path.join(os.path.dirname(options.output_path),
     88                               options.sources_file)
     89   if os.path.exists(coverage_file):
     90     os.remove(coverage_file)
     91   if os.path.exists(sources_file):
     92     os.remove(sources_file)
     93 
     94   if os.path.isdir(options.input_path):
     95     shutil.rmtree(options.output_path, ignore_errors=True)
     96     shutil.copytree(options.input_path, options.output_path)
     97   else:
     98     shutil.copy(options.input_path, options.output_path)
     99 
    100   if options.stamp:
    101     build_utils.Touch(options.stamp)
    102 
    103 
    104 def _CreateSourcesFile(sources_string, sources_file, src_root):
    105   """Adds all normalized source directories to |sources_file|.
    106 
    107   Args:
    108     sources_string: String generated from gyp containing the list of sources.
    109     sources_file: File into which to write the JSON list of sources.
    110     src_root: Root which sources added to the file should be relative to.
    111 
    112   Returns:
    113     An exit code.
    114   """
    115   src_root = os.path.abspath(src_root)
    116   sources = build_utils.ParseGypList(sources_string)
    117   relative_sources = []
    118   for s in sources:
    119     abs_source = os.path.abspath(s)
    120     if abs_source[:len(src_root)] != src_root:
    121       print ('Error: found source directory not under repository root: %s %s'
    122              % (abs_source, src_root))
    123       return 1
    124     rel_source = os.path.relpath(abs_source, src_root)
    125 
    126     relative_sources.append(rel_source)
    127 
    128   with open(sources_file, 'w') as f:
    129     json.dump(relative_sources, f)
    130 
    131 
    132 def _RunInstrumentCommand(command, options, args, option_parser):
    133   """Instruments the classes/jar files using EMMA.
    134 
    135   Args:
    136     command: 'instrument_jar' or 'instrument_classes'. This distinguishes
    137         whether we copy the output from the created lib/ directory, or classes/
    138         directory.
    139     options: optparse options dictionary.
    140     args: List of extra args from optparse.
    141     option_parser: optparse.OptionParser object.
    142 
    143   Returns:
    144     An exit code.
    145   """
    146   if not (options.input_path and options.output_path and
    147           options.coverage_file and options.sources_file and options.sources and
    148           options.src_root and options.emma_jar):
    149     option_parser.error('All arguments are required.')
    150 
    151   coverage_file = os.path.join(os.path.dirname(options.output_path),
    152                                options.coverage_file)
    153   sources_file = os.path.join(os.path.dirname(options.output_path),
    154                               options.sources_file)
    155   temp_dir = tempfile.mkdtemp()
    156   try:
    157     cmd = ['java', '-cp', options.emma_jar,
    158            'emma', 'instr',
    159            '-ip', options.input_path,
    160            '-ix', options.filter_string,
    161            '-d', temp_dir,
    162            '-out', coverage_file,
    163            '-m', 'fullcopy']
    164     build_utils.CheckOutput(cmd)
    165 
    166     if command == 'instrument_jar':
    167       for jar in os.listdir(os.path.join(temp_dir, 'lib')):
    168         shutil.copy(os.path.join(temp_dir, 'lib', jar),
    169                     options.output_path)
    170     else:  # 'instrument_classes'
    171       if os.path.isdir(options.output_path):
    172         shutil.rmtree(options.output_path, ignore_errors=True)
    173       shutil.copytree(os.path.join(temp_dir, 'classes'),
    174                       options.output_path)
    175   finally:
    176     shutil.rmtree(temp_dir)
    177 
    178   _CreateSourcesFile(options.sources, sources_file, options.src_root)
    179 
    180   if options.stamp:
    181     build_utils.Touch(options.stamp)
    182 
    183   return 0
    184 
    185 
    186 CommandFunctionTuple = collections.namedtuple(
    187     'CommandFunctionTuple', ['add_options_func', 'run_command_func'])
    188 VALID_COMMANDS = {
    189     'copy': CommandFunctionTuple(_AddCommonOptions,
    190                                  _RunCopyCommand),
    191     'instrument_jar': CommandFunctionTuple(_AddInstrumentOptions,
    192                                            _RunInstrumentCommand),
    193     'instrument_classes': CommandFunctionTuple(_AddInstrumentOptions,
    194                                                _RunInstrumentCommand),
    195 }
    196 
    197 
    198 def main(argv):
    199   option_parser = command_option_parser.CommandOptionParser(
    200       commands_dict=VALID_COMMANDS)
    201   command_option_parser.ParseAndExecute(option_parser)
    202 
    203 
    204 if __name__ == '__main__':
    205   sys.exit(main(sys.argv))
    206