Jack Rosenthal | 110a83d | 2023-07-25 12:52:32 -0600 | [diff] [blame] | 1 | # 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 | |
| 7 | This wrapper sets up necessary symlinks for the workspace, ensures Bazelisk via |
| 8 | CIPD, and executes it. It's also the right home for gathering any telemetry on |
| 9 | users' Bazel commands. |
| 10 | """ |
Mike Frysinger | 93afe3e | 2023-09-05 14:18:52 -0400 | [diff] [blame^] | 11 | |
Jack Rosenthal | 110a83d | 2023-07-25 12:52:32 -0600 | [diff] [blame] | 12 | import argparse |
| 13 | import logging |
| 14 | import os |
| 15 | from pathlib import Path |
| 16 | from typing import List, Optional, Tuple |
| 17 | |
| 18 | from chromite.lib import cipd |
| 19 | from chromite.lib import commandline |
| 20 | from chromite.lib import constants |
| 21 | from 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 Rangel | ced5c15 | 2023-08-04 14:41:41 -0600 | [diff] [blame] | 36 | "@portage", |
Jack Rosenthal | 110a83d | 2023-07-25 12:52:32 -0600 | [diff] [blame] | 37 | ] |
| 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 | |
| 44 | def _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 Hashimoto | 2208f6e | 2023-08-16 15:30:05 +0900 | [diff] [blame] | 59 | path.relative_to(constants.BAZEL_WORKSPACE_ROOT), |
| 60 | constants.BAZEL_WORKSPACE_ROOT / path.name, |
Jack Rosenthal | 110a83d | 2023-07-25 12:52:32 -0600 | [diff] [blame] | 61 | ) |
| 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 | |
| 70 | def _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 | |
| 101 | def _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 Stark | fa0b6e8 | 2023-08-08 10:42:21 +1000 | [diff] [blame] | 109 | cipd_path, |
| 110 | _BAZELISK_PACKAGE, |
| 111 | _BAZELISK_VERSION, |
| 112 | print_cmd=False, |
Jack Rosenthal | 110a83d | 2023-07-25 12:52:32 -0600 | [diff] [blame] | 113 | ) |
| 114 | return package_path / "bazelisk" |
| 115 | |
| 116 | |
| 117 | def _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 | |
| 136 | def 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 | |
| 153 | def 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]) |