blob: 958aedf9300f2291be809458907091d2c68695bc [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(
107 cipd_path, _BAZELISK_PACKAGE, _BAZELISK_VERSION
108 )
109 return package_path / "bazelisk"
110
111
112def _get_parser() -> commandline.ArgumentParser:
113 """Build the argument parser."""
114
115 # We don't create a help message, as we want --help to go to the Bazel help.
116 parser = commandline.ArgumentParser(add_help=False)
117
118 parser.add_argument(
119 "--project",
120 choices=_PROJECTS,
121 default=_get_default_project(),
122 help=(
123 "The temporary project type. This argument will be removed once "
124 "all projects unify into a single Bazel workspace."
125 ),
126 )
127
128 return parser
129
130
131def parse_arguments(
132 argv: Optional[List[str]],
133) -> Tuple[argparse.Namespace, List[str]]:
134 """Parse and validate arguments.
135
136 Args:
137 argv: The command line to parse.
138
139 Returns:
140 A two tuple, the parsed arguments, and the remaining arguments that
141 should be passed to Bazel.
142 """
143 parser = _get_parser()
144 opts, bazel_args = parser.parse_known_args(argv)
145 return opts, bazel_args
146
147
148def main(argv: Optional[List[str]]) -> Optional[int]:
149 """Main."""
150 opts, bazel_args = parse_arguments(argv)
151
152 _setup_workspace(opts.project)
153
154 bazelisk = _get_bazelisk()
155 os.execv(bazelisk, [bazelisk, *bazel_args])