• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright 2013 The Chromium Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""Instruments classes and jar files.
7
8This script corresponds to the 'jacoco_instr' action in the Java build process.
9Depending on whether jacoco_instrument is set, the 'jacoco_instr' action will
10call the instrument command which accepts a jar and instruments it using
11jacococli.jar.
12
13"""
14
15import argparse
16import json
17import os
18import shutil
19import sys
20import zipfile
21
22from util import build_utils
23import action_helpers
24import zip_helpers
25
26
27# This should be same as recipe side token. See bit.ly/3STSPcE.
28INSTRUMENT_ALL_JACOCO_OVERRIDE_TOKEN = 'INSTRUMENT_ALL_JACOCO'
29
30
31def _AddArguments(parser):
32  """Adds arguments related to instrumentation to parser.
33
34  Args:
35    parser: ArgumentParser object.
36  """
37  parser.add_argument(
38      '--input-path',
39      required=True,
40      help='Path to input file(s). Either the classes '
41      'directory, or the path to a jar.')
42  parser.add_argument(
43      '--output-path',
44      required=True,
45      help='Path to output final file(s) to. Either the '
46      'final classes directory, or the directory in '
47      'which to place the instrumented/copied jar.')
48  parser.add_argument(
49      '--sources-json-file',
50      required=True,
51      help='File to create with the list of source directories '
52      'and input path.')
53  parser.add_argument(
54      '--target-sources-file',
55      required=True,
56      help='File containing newline-separated .java and .kt paths')
57  parser.add_argument(
58      '--jacococli-jar', required=True, help='Path to jacococli.jar.')
59  parser.add_argument(
60      '--files-to-instrument',
61      help='Path to a file containing which source files are affected.')
62
63
64def _GetSourceDirsFromSourceFiles(source_files):
65  """Returns list of directories for the files in |source_files|.
66
67  Args:
68    source_files: List of source files.
69
70  Returns:
71    List of source directories.
72  """
73  return list(set(os.path.dirname(source_file) for source_file in source_files))
74
75
76def _CreateSourcesJsonFile(source_dirs, input_path, sources_json_file,
77                           src_root):
78  """Adds all normalized source directories and input path to
79  |sources_json_file|.
80
81  Args:
82    source_dirs: List of source directories.
83    input_path: The input path to non-instrumented class files.
84    sources_json_file: File into which to write the list of source directories
85    and input path.
86    src_root: Root which sources added to the file should be relative to.
87
88  Returns:
89    An exit code.
90  """
91  src_root = os.path.abspath(src_root)
92  relative_sources = []
93  for s in source_dirs:
94    abs_source = os.path.abspath(s)
95    if abs_source[:len(src_root)] != src_root:
96      print('Error: found source directory not under repository root: %s %s' %
97            (abs_source, src_root))
98      return 1
99    rel_source = os.path.relpath(abs_source, src_root)
100
101    relative_sources.append(rel_source)
102
103  data = {}
104  data['source_dirs'] = relative_sources
105  data['input_path'] = []
106  data['output_dir'] = src_root
107  if input_path:
108    data['input_path'].append(os.path.abspath(input_path))
109  with open(sources_json_file, 'w') as f:
110    json.dump(data, f)
111  return 0
112
113
114def _GetAffectedClasses(jar_file, source_files):
115  """Gets affected classes by affected source files to a jar.
116
117  Args:
118    jar_file: The jar file to get all members.
119    source_files: The list of affected source files.
120
121  Returns:
122    A tuple of affected classes and unaffected members.
123  """
124  with zipfile.ZipFile(jar_file) as f:
125    members = f.namelist()
126
127  affected_classes = []
128  unaffected_members = []
129
130  for member in members:
131    if not member.endswith('.class'):
132      unaffected_members.append(member)
133      continue
134
135    is_affected = False
136    index = member.find('$')
137    if index == -1:
138      index = member.find('.class')
139    for source_file in source_files:
140      if source_file.endswith(
141          (member[:index] + '.java', member[:index] + '.kt')):
142        affected_classes.append(member)
143        is_affected = True
144        break
145    if not is_affected:
146      unaffected_members.append(member)
147
148  return affected_classes, unaffected_members
149
150
151def _InstrumentClassFiles(instrument_cmd,
152                          input_path,
153                          output_path,
154                          temp_dir,
155                          affected_source_files=None):
156  """Instruments class files from input jar.
157
158  Args:
159    instrument_cmd: JaCoCo instrument command.
160    input_path: The input path to non-instrumented jar.
161    output_path: The output path to instrumented jar.
162    temp_dir: The temporary directory.
163    affected_source_files: The affected source file paths to input jar.
164      Default is None, which means instrumenting all class files in jar.
165  """
166  affected_classes = None
167  unaffected_members = None
168  if affected_source_files:
169    affected_classes, unaffected_members = _GetAffectedClasses(
170        input_path, affected_source_files)
171
172  # Extract affected class files.
173  with zipfile.ZipFile(input_path) as f:
174    f.extractall(temp_dir, affected_classes)
175
176  instrumented_dir = os.path.join(temp_dir, 'instrumented')
177
178  # Instrument extracted class files.
179  instrument_cmd.extend([temp_dir, '--dest', instrumented_dir])
180  build_utils.CheckOutput(instrument_cmd)
181
182  if affected_source_files and unaffected_members:
183    # Extract unaffected members to instrumented_dir.
184    with zipfile.ZipFile(input_path) as f:
185      f.extractall(instrumented_dir, unaffected_members)
186
187  # Zip all files to output_path
188  with action_helpers.atomic_output(output_path) as f:
189    zip_helpers.zip_directory(f, instrumented_dir)
190
191
192def _RunInstrumentCommand(parser):
193  """Instruments class or Jar files using JaCoCo.
194
195  Args:
196    parser: ArgumentParser object.
197
198  Returns:
199    An exit code.
200  """
201  args = parser.parse_args()
202
203  source_files = []
204  if args.target_sources_file:
205    source_files.extend(build_utils.ReadSourcesList(args.target_sources_file))
206
207  with build_utils.TempDir() as temp_dir:
208    instrument_cmd = build_utils.JavaCmd() + [
209        '-jar', args.jacococli_jar, 'instrument'
210    ]
211
212    if not args.files_to_instrument:
213      affected_source_files = None
214    else:
215      affected_files = build_utils.ReadSourcesList(args.files_to_instrument)
216      # Check if coverage recipe decided to instrument everything by overriding
217      # the try builder default setting(selective instrumentation). This can
218      # happen in cases like a DEPS roll of jacoco library
219
220      # Note: This token is preceded by ../../ because the paths to be
221      # instrumented are expected to be relative to the build directory.
222      # See _rebase_paths() at https://bit.ly/40oiixX
223      token = '../../' + INSTRUMENT_ALL_JACOCO_OVERRIDE_TOKEN
224      if token in affected_files:
225        affected_source_files = None
226      else:
227        source_set = set(source_files)
228        affected_source_files = [f for f in affected_files if f in source_set]
229
230        # Copy input_path to output_path and return if no source file affected.
231        if not affected_source_files:
232          shutil.copyfile(args.input_path, args.output_path)
233          # Create a dummy sources_json_file.
234          _CreateSourcesJsonFile([], None, args.sources_json_file,
235                                 build_utils.DIR_SOURCE_ROOT)
236          return 0
237    _InstrumentClassFiles(instrument_cmd, args.input_path, args.output_path,
238                          temp_dir, affected_source_files)
239
240  source_dirs = _GetSourceDirsFromSourceFiles(source_files)
241  # TODO(GYP): In GN, we are passed the list of sources, detecting source
242  # directories, then walking them to re-establish the list of sources.
243  # This can obviously be simplified!
244  _CreateSourcesJsonFile(source_dirs, args.input_path, args.sources_json_file,
245                         build_utils.DIR_SOURCE_ROOT)
246
247  return 0
248
249
250def main():
251  parser = argparse.ArgumentParser()
252  _AddArguments(parser)
253  _RunInstrumentCommand(parser)
254
255
256if __name__ == '__main__':
257  sys.exit(main())
258