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