xref: /aosp_15_r20/external/cronet/components/cronet/tools/utils.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env python3
2#
3# Copyright 2024 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"""
7Contains general-purpose methods that can be used to execute shell,
8GN and Ninja commands.
9"""
10
11import subprocess
12import os
13import re
14import pathlib
15import difflib
16
17REPOSITORY_ROOT = os.path.abspath(
18    os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))
19
20_MB_PATH = os.path.join(REPOSITORY_ROOT, 'tools/mb/mb.py')
21_GN_PATH = os.path.join(REPOSITORY_ROOT, 'buildtools/linux64/gn')
22_GN_ARG_MATCHER = re.compile("^.*=.*$")
23
24
25def run(command, **kwargs):
26  """See the official documentation for subprocess.call.
27
28  Args:
29    command (list[str]): command to be executed
30
31  Returns:
32    int: the return value of subprocess.call
33  """
34  print(command, kwargs)
35  return subprocess.call(command, **kwargs)
36
37
38def run_shell(command, extra_options=''):
39  """Runs a shell command.
40
41  Runs a shell command with no escaping. It is recommended
42  to use `run` instead.
43  """
44  command = command + ' ' + extra_options
45  print(command)
46  return os.system(command)
47
48
49def gn(out_dir, gn_args, gn_extra=None, **kwargs):
50  """ Executes `gn gen`.
51
52  Runs `gn gen |out_dir| |gn_args + gn_extra|` which will generate
53  a GN configuration that lives under |out_dir|. This is done
54  locally on the same chromium checkout.
55
56  Args:
57    out_dir (str): Path to delegate to `gn gen`.
58    gn_args (str): Args as a string delimited by space.
59    gn_extra (str): extra args as a string delimited by space.
60
61  Returns:
62    Exit code of running `gn gen` command with argument provided.
63  """
64  cmd = [_GN_PATH, 'gen', out_dir, '--args=%s' % gn_args]
65  if gn_extra:
66    cmd += gn_extra
67  return run(cmd, **kwargs)
68
69
70def compare_text_and_generate_diff(generated_text, golden_text,
71                                   golden_file_path):
72  """
73  Compares the generated text with the golden text.
74
75  returns a diff that can be applied with `patch` if exists.
76  """
77  golden_lines = [line.rstrip() for line in golden_text.splitlines()]
78  generated_lines = [line.rstrip() for line in generated_text.splitlines()]
79  if golden_lines == generated_lines:
80    return None
81
82  expected_path = os.path.relpath(golden_file_path, REPOSITORY_ROOT)
83
84  diff = difflib.unified_diff(
85      golden_lines,
86      generated_lines,
87      fromfile=os.path.join('before', expected_path),
88      tofile=os.path.join('after', expected_path),
89      n=0,
90      lineterm='',
91  )
92
93  return '\n'.join(diff)
94
95
96def read_file(path):
97  """Reads a file as a string"""
98  return pathlib.Path(path).read_text()
99
100
101def build(out_dir, build_target, extra_options=None):
102  """Runs `autoninja build`.
103
104  Runs `autoninja -C |out_dir| |build_target| |extra_options|` which will build
105  the target |build_target| for the GN configuration living under |out_dir|.
106  This is done locally on the same chromium checkout.
107
108  Returns:
109    Exit code of running `autoninja ..` command with the argument provided.
110  """
111  cmd = ['autoninja', '-C', out_dir, build_target]
112  if extra_options:
113    cmd += extra_options
114  return run(cmd)
115
116
117def android_gn_gen(is_release, target_cpu, out_dir):
118  """Runs `gn gen` using Cronet's android gn_args.
119
120  Creates a local GN configuration under |out_dir| with the provided argument
121  as input to `get_android_gn_args`, see the documentation of
122  `get_android_gn_args` for more information.
123  """
124  return gn(out_dir, ' '.join(get_android_gn_args(is_release, target_cpu)))
125
126
127def get_android_gn_args(is_release, target_cpu):
128  """Fetches the gn args for a specific builder.
129
130  Returns a list of gn args used by the builders whose target cpu
131  is |target_cpu| and (dev or rel) depending on is_release.
132
133  See https://ci.chromium.org/p/chromium/g/chromium.android/console for
134  a list of the builders
135
136  Example:
137
138  get_android_gn_args(true, 'x86') -> GN Args for `android-cronet-x86-rel`
139  get_android_gn_args(false, 'x86') -> GN Args for `android-cronet-x86-dev`
140  """
141  group_name = 'chromium.android'
142  builder_name = _map_config_to_android_builder(is_release, target_cpu)
143  # Ideally we would call `mb_py gen` directly, but we need to filter out the
144  # use_remoteexec arg, as that cannot be used in a local environment.
145  gn_args = subprocess.check_output(
146      ['python3', _MB_PATH, 'lookup', '-m', group_name, '-b',
147       builder_name]).decode('utf-8').strip()
148  return filter_gn_args(gn_args.split("\n"), [])
149
150
151def get_path_from_gn_label(gn_label: str) -> str:
152  """Returns the path part from a GN Label
153
154  GN label consist of two parts, path and target_name, this will
155  remove the target name and return the path or throw an error
156  if it can't remove the target_name or if it doesn't exist.
157  """
158  if ":" not in gn_label:
159    raise ValueError(f"Provided gn label {gn_label} is not a proper label")
160  return gn_label[:gn_label.find(":")]
161
162
163def _map_config_to_android_builder(is_release, target_cpu):
164  target_cpu_to_base_builder = {
165      'x86': 'android-cronet-x86',
166      'x64': 'android-cronet-x64',
167      'arm': 'android-cronet-arm',
168      'arm64': 'android-cronet-arm64',
169      'riscv64': 'android-cronet-riscv64',
170  }
171  if target_cpu not in target_cpu_to_base_builder:
172    raise ValueError('Unsupported target CPU')
173
174  builder_name = target_cpu_to_base_builder[target_cpu]
175  if is_release:
176    builder_name += '-rel'
177  else:
178    builder_name += '-dbg'
179  return builder_name
180
181
182def _should_remove_arg(arg, keys):
183  """An arg is removed if its key appear in the list of |keys|"""
184  return arg.split("=")[0].strip() in keys
185
186
187def filter_gn_args(gn_args, keys_to_remove):
188  """Returns a list of filtered GN args.
189
190  (1) GN arg's returned must match the regex |_GN_ARG_MATCHER|.
191  (2) GN arg's key must not be in |keys_to_remove|.
192
193  Args:
194    gn_args: list of GN args.
195    keys_to_remove: List of string that will be removed from gn_args.
196  """
197  filtered_args = []
198  for arg in gn_args:
199    if _GN_ARG_MATCHER.match(arg) and not _should_remove_arg(
200        arg, keys_to_remove):
201      filtered_args.append(arg)
202  return filtered_args
203