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, _, 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, _, 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   if os.path.exists(coverage_file):
    156     os.remove(coverage_file)
    157   temp_dir = tempfile.mkdtemp()
    158   try:
    159     cmd = ['java', '-cp', options.emma_jar,
    160            'emma', 'instr',
    161            '-ip', options.input_path,
    162            '-ix', options.filter_string,
    163            '-d', temp_dir,
    164            '-out', coverage_file,
    165            '-m', 'fullcopy']
    166     build_utils.CheckOutput(cmd)
    167 
    168     if command == 'instrument_jar':
    169       for jar in os.listdir(os.path.join(temp_dir, 'lib')):
    170         shutil.copy(os.path.join(temp_dir, 'lib', jar),
    171                     options.output_path)
    172     else:  # 'instrument_classes'
    173       if os.path.isdir(options.output_path):
    174         shutil.rmtree(options.output_path, ignore_errors=True)
    175       shutil.copytree(os.path.join(temp_dir, 'classes'),
    176                       options.output_path)
    177   finally:
    178     shutil.rmtree(temp_dir)
    179 
    180   _CreateSourcesFile(options.sources, sources_file, options.src_root)
    181 
    182   if options.stamp:
    183     build_utils.Touch(options.stamp)
    184 
    185   return 0
    186 
    187 
    188 CommandFunctionTuple = collections.namedtuple(
    189     'CommandFunctionTuple', ['add_options_func', 'run_command_func'])
    190 VALID_COMMANDS = {
    191     'copy': CommandFunctionTuple(_AddCommonOptions,
    192                                  _RunCopyCommand),
    193     'instrument_jar': CommandFunctionTuple(_AddInstrumentOptions,
    194                                            _RunInstrumentCommand),
    195     'instrument_classes': CommandFunctionTuple(_AddInstrumentOptions,
    196                                                _RunInstrumentCommand),
    197 }
    198 
    199 
    200 def main():
    201   option_parser = command_option_parser.CommandOptionParser(
    202       commands_dict=VALID_COMMANDS)
    203   command_option_parser.ParseAndExecute(option_parser)
    204 
    205 
    206 if __name__ == '__main__':
    207   sys.exit(main())
    208