blob: 817a8dd933a1925464e1e8502585d20680492e15 [file] [log] [blame]
Jack Rosenthal110a83d2023-07-25 12:52:32 -06001# Copyright 2023 The ChromiumOS Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Bazel command wrapper.
6
7This wrapper sets up necessary symlinks for the workspace, ensures Bazelisk via
8CIPD, and executes it. It's also the right home for gathering any telemetry on
9users' Bazel commands.
10"""
11import argparse
12import logging
13import os
14from pathlib import Path
15from typing import List, Optional, Tuple
16
17from chromite.lib import cipd
18from chromite.lib import commandline
19from chromite.lib import constants
20from chromite.lib import osutils
21
22
23# TODO(jrosenth): We likely want to publish our own Bazelisk at some point
24# instead of relying upon Skia's.
25_BAZELISK_PACKAGE = "skia/bots/bazelisk_${os}_${arch}"
26_BAZELISK_VERSION = "version:0"
27
28# Symlinks which may exist in the workspace root without an underlying file in
29# src/bazel/workspace_root. These are symlinks generated by bazel itself.
30_KNOWN_SYMLINKS = [
31 "bazel-bin",
32 "bazel-out",
33 "bazel-src",
34 "bazel-testlogs",
35]
36
37# Workspaces for each project are defined here.
38_PROJECTS = ["alchemy", "metallurgy", "fwsdk"]
39_WORKSPACES_DIR = constants.BAZEL_WORKSPACE_ROOT / "bazel" / "workspace_root"
40
41
42def _setup_workspace(project: str):
43 """Setup the Bazel workspace root.
44
45 Args:
46 project: The temporary project type (e.g., metallurgy, alchemy, or
47 fwsdk). This argument will eventually be removed when all Bazel
48 projects share a unified workspace.
49 """
50 known_symlinks = set(_KNOWN_SYMLINKS)
51 for workspace in (
52 _WORKSPACES_DIR / "general",
53 _WORKSPACES_DIR / project,
54 ):
55 for path in workspace.iterdir():
56 osutils.SafeSymlink(
57 path, constants.BAZEL_WORKSPACE_ROOT / path.name
58 )
59 known_symlinks.add(path.name)
60
61 # Remove any stale symlinks from the workspace root.
62 for path in constants.BAZEL_WORKSPACE_ROOT.iterdir():
63 if path.is_symlink() and not path.name in known_symlinks:
64 osutils.SafeUnlink(path)
65
66
67def _get_default_project() -> str:
68 """Get the default value for --project.
69
70 It's inconvenient to pass --project for each Bazel invocation. We assume if
71 the user has run with --project before, we can use the value from their last
72 invocation.
73
74 If no other default project can be found, the assumed project is "alchemy".
75
76 This function will be removed once all projects unify into a single Bazel
77 workspace.
78 """
79 workspace_file = constants.BAZEL_WORKSPACE_ROOT / "WORKSPACE.bazel"
80 if workspace_file.is_symlink():
81 project = Path(os.readlink(workspace_file)).parent.name
82 if project in _PROJECTS:
83 return project
84 else:
85 logging.warning(
86 "Your checkout contains a WORKSPACE.bazel symlink which points "
87 "to an unknown project (%s).",
88 project,
89 )
90
91 logging.notice(
92 "Assuming a default project of alchemy. Pass --project if you want a "
93 "different one."
94 )
95 return "alchemy"
96
97
98def _get_bazelisk() -> Path:
99 """Ensure Bazelisk from CIPD.
100
101 Returns:
102 The path to the Bazel executable.
103 """
104 cipd_path = cipd.GetCIPDFromCache()
105 package_path = cipd.InstallPackage(
106 cipd_path, _BAZELISK_PACKAGE, _BAZELISK_VERSION
107 )
108 return package_path / "bazelisk"
109
110
111def _get_parser() -> commandline.ArgumentParser:
112 """Build the argument parser."""
113
114 # We don't create a help message, as we want --help to go to the Bazel help.
115 parser = commandline.ArgumentParser(add_help=False)
116
117 parser.add_argument(
118 "--project",
119 choices=_PROJECTS,
120 default=_get_default_project(),
121 help=(
122 "The temporary project type. This argument will be removed once "
123 "all projects unify into a single Bazel workspace."
124 ),
125 )
126
127 return parser
128
129
130def parse_arguments(
131 argv: Optional[List[str]],
132) -> Tuple[argparse.Namespace, List[str]]:
133 """Parse and validate arguments.
134
135 Args:
136 argv: The command line to parse.
137
138 Returns:
139 A two tuple, the parsed arguments, and the remaining arguments that
140 should be passed to Bazel.
141 """
142 parser = _get_parser()
143 opts, bazel_args = parser.parse_known_args(argv)
144 return opts, bazel_args
145
146
147def main(argv: Optional[List[str]]) -> Optional[int]:
148 """Main."""
149 opts, bazel_args = parse_arguments(argv)
150
151 _setup_workspace(opts.project)
152
153 bazelisk = _get_bazelisk()
154 os.execv(bazelisk, [bazelisk, *bazel_args])