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