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 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())
    231