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