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