1# Copyright 2024 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from __future__ import annotations 6 7import abc 8import logging 9from typing import TYPE_CHECKING, Iterator, Optional, Tuple 10 11from crossbench import path as pth 12 13if TYPE_CHECKING: 14 from crossbench.plt.base import Platform 15 16 17class BaseToolFinder(abc.ABC): 18 19 def __init__( 20 self, platform: Platform, candidates: Tuple[pth.AnyPath, 21 ...] = tuple()) -> None: 22 self._platform = platform 23 self._candidates = candidates + self.default_candidates() 24 self._path: Optional[pth.AnyPath] = self._find_path() 25 if self._path: 26 assert self.is_valid_path(self._path) 27 28 @property 29 def platform(self) -> Platform: 30 return self._platform 31 32 @property 33 def path(self) -> Optional[pth.AnyPath]: 34 return self._path 35 36 @property 37 def candidates(self) -> Tuple[pth.AnyPath, ...]: 38 return self._candidates 39 40 def default_candidates(self) -> Tuple[pth.AnyPath, ...]: 41 return tuple() 42 43 def _find_path(self) -> Optional[pth.AnyPath]: 44 # Try potential build location 45 for candidate_path in self._candidates: 46 if self.is_valid_path(candidate_path): 47 return candidate_path 48 return None 49 50 @abc.abstractmethod 51 def is_valid_path(self, candidate: pth.AnyPath) -> bool: 52 pass 53 54 55def default_chromium_candidates(platform: Platform) -> Tuple[pth.AnyPath, ...]: 56 """Returns a generous list of potential locations of a chromium checkout.""" 57 candidates = [] 58 if chromium_src := platform.environ.get("CHROMIUM_SRC"): 59 candidates.append(platform.path(chromium_src)) 60 if platform.is_local: 61 candidates.append(chromium_src_relative_local_path()) 62 if platform.is_android: 63 return tuple(candidates) 64 home_dir = platform.home() 65 candidates += [ 66 # Guessing default locations 67 home_dir / "Documents/chromium/src", 68 home_dir / "chromium/src", 69 platform.path("C:/src/chromium/src"), 70 home_dir / "Documents/chrome/src", 71 home_dir / "chrome/src", 72 platform.path("C:/src/chrome/src"), 73 ] 74 return tuple(candidates) 75 76 77def chromium_src_relative_local_path(): 78 """Gets the local relative path of `chromium/src`. 79 80 Assuming the cli.py path is `third_party/crossbench/crossbench/cli/cli.py`. 81 """ 82 return pth.LocalPath(__file__).parents[4] 83 84 85def is_chromium_checkout_dir(platform: Platform, dir_path: pth.AnyPath) -> bool: 86 return (platform.is_dir(dir_path / "v8") and 87 platform.is_dir(dir_path / "chrome") and 88 platform.is_dir(dir_path / ".git")) 89 90 91class ChromiumCheckoutFinder(BaseToolFinder): 92 """Finds a chromium src checkout at either given locations or at 93 some preset known checkout locations.""" 94 95 def default_candidates(self) -> Tuple[pth.AnyPath, ...]: 96 return default_chromium_candidates(self.platform) 97 98 def is_valid_path(self, candidate: pth.AnyPath) -> bool: 99 return is_chromium_checkout_dir(self.platform, candidate) 100 101 102class ChromiumBuildBinaryFinder(BaseToolFinder): 103 """Finds a custom-built binary in either a given out/BUILD dir or 104 tries to find it in build dirs in common known chromium checkout locations.""" 105 106 def __init__( 107 self, 108 platform: Platform, 109 binary_name: str, 110 candidates: Tuple[pth.AnyPath, ...] = tuple()) -> None: 111 self._binary_name = binary_name 112 super().__init__(platform, candidates) 113 114 @property 115 def binary_name(self) -> str: 116 return self._binary_name 117 118 def _iterate_candidate_bin_paths(self) -> Iterator[pth.AnyPath]: 119 for candidate_dir in self._candidates: 120 yield candidate_dir / self._binary_name 121 122 for candidate in default_chromium_candidates(self.platform): 123 candidate_out = candidate / "out" 124 if not self.platform.is_dir(candidate_out): 125 continue 126 # TODO: support remote glob 127 for build in ("Release", "release", "rel", "Optdebug", "optdebug", "opt"): 128 yield candidate_out / build / self._binary_name 129 130 def _find_path(self) -> Optional[pth.AnyPath]: 131 for candidate in self._iterate_candidate_bin_paths(): 132 if self.is_valid_path(candidate): 133 return candidate 134 return None 135 136 def is_valid_path(self, candidate: pth.AnyPath) -> bool: 137 assert candidate.name == self._binary_name 138 if not self.platform.is_file(candidate): 139 return False 140 # .../chromium/src/out/Release/BINARY => .../chromium/src/ 141 # Don't use parents[] access to stop at the root. 142 maybe_checkout_dir = candidate.parent.parent.parent 143 return is_chromium_checkout_dir(self._platform, maybe_checkout_dir) 144 145 146class V8CheckoutFinder(BaseToolFinder): 147 148 def default_candidates(self) -> Tuple[pth.AnyPath, ...]: 149 if self.platform.is_android: 150 return () 151 home_dir = self._platform.home() 152 return ( 153 # V8 Checkouts 154 home_dir / "Documents/v8/v8", 155 home_dir / "v8/v8", 156 self._platform.path("C:/src/v8/v8"), 157 # Raw V8 checkouts 158 home_dir / "Documents/v8", 159 home_dir / "v8", 160 self._platform.path("C:/src/v8/"), 161 ) 162 163 def _find_path(self) -> Optional[pth.AnyPath]: 164 if v8_checkout := super()._find_path(): 165 return v8_checkout 166 if chromium_checkout := ChromiumCheckoutFinder(self._platform).path: 167 return chromium_checkout / "v8" 168 maybe_d8_path = self.platform.environ.get("D8_PATH") 169 if not maybe_d8_path: 170 return None 171 for candidate_dir in self.platform.path(maybe_d8_path).parents: 172 if self.is_valid_path(candidate_dir): 173 return candidate_dir 174 return None 175 176 def is_valid_path(self, candidate: pth.AnyPath) -> bool: 177 v8_header_file = candidate / "include/v8.h" 178 return (self.platform.is_file(v8_header_file) and 179 (self.platform.is_dir(candidate / ".git"))) 180 181 182class V8ToolsFinder: 183 """Helper class to find d8 binaries and the tick-processor. 184 If no explicit d8 and checkout path are given, $D8_PATH and common v8 and 185 chromium installation directories are checked.""" 186 187 def __init__(self, 188 platform: Platform, 189 d8_binary: Optional[pth.AnyPath] = None, 190 v8_checkout: Optional[pth.AnyPath] = None) -> None: 191 self.platform = platform 192 self.d8_binary: Optional[pth.AnyPath] = d8_binary 193 self.v8_checkout: Optional[pth.AnyPath] = None 194 if v8_checkout: 195 self.v8_checkout = v8_checkout 196 else: 197 self.v8_checkout = V8CheckoutFinder(self.platform).path 198 self.tick_processor: Optional[pth.AnyPath] = None 199 self.d8_binary = self._find_d8() 200 if self.d8_binary: 201 self.tick_processor = self._find_v8_tick_processor() 202 logging.debug("V8ToolsFinder found d8_binary='%s' tick_processor='%s'", 203 self.d8_binary, self.tick_processor) 204 205 def _find_d8(self) -> Optional[pth.AnyPath]: 206 if self.d8_binary and self.platform.is_file(self.d8_binary): 207 return self.d8_binary 208 environ = self.platform.environ 209 if "D8_PATH" in environ: 210 candidate = self.platform.path(environ["D8_PATH"]) / "d8" 211 if self.platform.is_file(candidate): 212 return candidate 213 candidate = self.platform.path(environ["D8_PATH"]) 214 if self.platform.is_file(candidate): 215 return candidate 216 # Try potential build location 217 for candidate_dir in V8CheckoutFinder(self.platform).candidates: 218 for build_type in ("release", "optdebug", "Default", "Release"): 219 candidates = list( 220 self.platform.glob(candidate_dir, f"out/*{build_type}/d8")) 221 if not candidates: 222 continue 223 d8_candidate = candidates[0] 224 if self.platform.is_file(d8_candidate): 225 return d8_candidate 226 return None 227 228 def _find_v8_tick_processor(self) -> Optional[pth.AnyPath]: 229 if self.platform.is_linux: 230 tick_processor = "tools/linux-tick-processor" 231 elif self.platform.is_macos: 232 tick_processor = "tools/mac-tick-processor" 233 elif self.platform.is_win: 234 tick_processor = "tools/windows-tick-processor.bat" 235 else: 236 logging.debug( 237 "Not looking for the v8 tick-processor on unsupported platform: %s", 238 self.platform) 239 return None 240 if self.v8_checkout and self.platform.is_dir(self.v8_checkout): 241 candidate = self.v8_checkout / tick_processor 242 assert self.platform.is_file(candidate), ( 243 f"Provided v8_checkout has no '{tick_processor}' at {candidate}") 244 assert self.d8_binary 245 # Try inferring the V8 checkout from a built d8: 246 # .../foo/v8/v8/out/x64.release/d8 247 candidate = self.d8_binary.parents[2] / tick_processor 248 if self.platform.is_file(candidate): 249 return candidate 250 if self.v8_checkout: 251 candidate = self.v8_checkout / tick_processor 252 if self.platform.is_file(candidate): 253 return candidate 254 return None 255 256 257class BaseChromiumBinaryToolFinder(BaseToolFinder): 258 259 def is_valid_path(self, candidate: pth.AnyPath) -> bool: 260 return self._platform.is_file(candidate) 261 262 @classmethod 263 def chrome_path(cls) -> pth.AnyPath: 264 raise NotImplementedError() 265 266 def default_candidates(self) -> Tuple[pth.AnyPath, ...]: 267 relative_path = chromium_src_relative_local_path() / self.chrome_path() 268 if maybe_chrome := ChromiumCheckoutFinder(self._platform).path: 269 return (relative_path, maybe_chrome / self.chrome_path(),) 270 return (relative_path,) 271 272 273class TraceconvFinder(BaseChromiumBinaryToolFinder): 274 275 @classmethod 276 def chrome_path(cls) -> pth.AnyPath: 277 return pth.AnyPath("third_party/perfetto/tools/traceconv") 278 279 280class TraceProcessorFinder(BaseChromiumBinaryToolFinder): 281 282 @classmethod 283 def chrome_path(cls) -> pth.AnyPath: 284 return pth.AnyPath("third_party/perfetto/tools/trace_processor") 285 286 287CROSSBENCH_DIR = pth.LocalPath(__file__).parents[2] 288 289 290class BaseCrossbenchBinaryToolFinder(BaseChromiumBinaryToolFinder): 291 292 def default_candidates(self) -> Tuple[pth.AnyPath, ...]: 293 candidates = super().default_candidates() 294 return (CROSSBENCH_DIR / self.crossbench_path(),) + candidates 295 296 @classmethod 297 @abc.abstractmethod 298 def crossbench_path(cls) -> pth.AnyPath: 299 pass 300 301 302class WprGoToolFinder(BaseCrossbenchBinaryToolFinder): 303 304 @classmethod 305 def chrome_path(cls) -> pth.AnyPath: 306 return pth.AnyPath("third_party/catapult/web_page_replay_go/src/wpr.go") 307 308 @classmethod 309 def crossbench_path(cls) -> pth.AnyPath: 310 return pth.AnyPath("third_party/webpagereplay/src/wpr.go") 311 312 313class TsProxyFinder(BaseCrossbenchBinaryToolFinder): 314 315 @classmethod 316 def chrome_path(cls) -> pth.AnyPath: 317 return pth.AnyPath("third_party/catapult/third_party/tsproxy/tsproxy.py") 318 319 @classmethod 320 def crossbench_path(cls) -> pth.AnyPath: 321 return pth.AnyPath("third_party/tsproxy/tsproxy.py") 322