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