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