• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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