From 6a505ad9abf32a387ce3d8d0a1d40273a0e35af1 Mon Sep 17 00:00:00 2001 From: Jack Rosenthal Date: Tue, 1 Aug 2023 16:10:00 +0000 Subject: [PATCH] [ChromiumOS] Add a launcher for Bazel CrOS intends to provide a Bazel executable for our users in chromite/bin/bazel in our tree. We'd like the "bazel" command in depot_tools to call this executable. This adds a new launcher to depot_tools which searches for that bazel executable when located inside of a ChromiumOS checkout, and executes it. When located outside of a ChromiumOS checkout, this launcher "disappears", searching elsewhere in the PATH for another Bazel executable. Since other teams using depot_tools may want to start using Bazel in the future, this launcher is intended to have shared ownership: other teams are welcome to come add their search functions to the launcher if they require the same functionality as us. Bug: b:253268519 Change-Id: I61f6383d8b69b9eea622f37277678f898cc7fd6b Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/4718785 Reviewed-by: Shuhei Takahashi Reviewed-by: Josip Sokcevic Reviewed-by: Aaron Massey Commit-Queue: Jack Rosenthal Auto-Submit: Jack Rosenthal Commit-Queue: Josip Sokcevic --- OWNERS | 2 + bazel | 1 + bazel.py | 85 ++++++++++++++++++++++++++++++++++++++++ tests/OWNERS | 1 + tests/bazel_test.py | 94 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 183 insertions(+) create mode 120000 bazel create mode 100755 bazel.py create mode 100755 tests/bazel_test.py diff --git a/OWNERS b/OWNERS index 59958d775..9215c1e0b 100644 --- a/OWNERS +++ b/OWNERS @@ -28,6 +28,8 @@ per-file presubmit*.py=brucedawson@chromium.org per-file pylint*=vapier@chromium.org +per-file bazel=file://CROS_OWNERS +per-file bazel.py=file://CROS_OWNERS per-file cbuildbot=file://CROS_OWNERS per-file cros=file://CROS_OWNERS per-file *cros_python2*=file://CROS_OWNERS diff --git a/bazel b/bazel new file mode 120000 index 000000000..197489b39 --- /dev/null +++ b/bazel @@ -0,0 +1 @@ +bazel.py \ No newline at end of file diff --git a/bazel.py b/bazel.py new file mode 100755 index 000000000..8fbd77854 --- /dev/null +++ b/bazel.py @@ -0,0 +1,85 @@ +#!/usr/bin/env vpython3 +# Copyright 2023 The ChromiumOS Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# [VPYTHON:BEGIN] +# python_version: "3.8" +# [VPYTHON:END] +"""Bazel launcher wrapper. + +This script starts Bazel appropriate for the project you're working in. It's +currently used by ChromiumOS, but is intended for use and to be updated by any +depot_tools users who are using Bazel. + +In the case this script is not able to detect which project you're working in, +it will fall back to using the next "bazel" executable in your PATH. +""" + +import itertools +import os +from pathlib import Path +import shutil +import sys +from typing import List, Optional + + +def _find_bazel_cros() -> Optional[Path]: + """Find the bazel launcher for ChromiumOS.""" + cwd = Path.cwd() + for parent in itertools.chain([cwd], cwd.parents): + bazel_launcher = parent / "chromite" / "bin" / "bazel" + if bazel_launcher.exists(): + return bazel_launcher + return None + + +def _find_next_bazel_in_path() -> Optional[Path]: + """The fallback method: search the remainder of PATH for bazel.""" + # Remove depot_tools from PATH if present. + depot_tools = Path(__file__).resolve().parent + path_env = os.environ.get("PATH", os.defpath) + search_paths = [] + for path in path_env.split(os.pathsep): + if Path(path).resolve() != depot_tools: + search_paths.append(path) + new_path_env = os.pathsep.join(search_paths) + bazel = shutil.which("bazel", path=new_path_env) + if bazel: + return Path(bazel) + return None + + +# All functions used to search for Bazel (in order of search). +_SEARCH_FUNCTIONS = ( + _find_bazel_cros, + _find_next_bazel_in_path, +) + +_FIND_FAILURE_MSG = """\ +ERROR: The depot_tools bazel launcher was unable to find an appropriate bazel +executable to use. + +For ChromiumOS developers: + Make sure your current directory is inside a ChromiumOS checkout (e.g., + ~/chromiumos). If you're already in a ChromiumOS checkout, it may be because + you're working on a branch that's too old (i.e., prior to Bazel). + +If you're not working on any of the above listed projects, this launcher assumes +that you have Bazel installed on your system somewhere else in PATH. Check that +it's actually installed.""" + + +def main(argv: List[str]) -> int: + """Main.""" + for search_func in _SEARCH_FUNCTIONS: + bazel = search_func() + if bazel: + os.execv(bazel, [str(bazel), *argv]) + + print(_FIND_FAILURE_MSG, file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tests/OWNERS b/tests/OWNERS index 4c77ba566..6c194bbe1 100644 --- a/tests/OWNERS +++ b/tests/OWNERS @@ -1,5 +1,6 @@ per-file autoninja_test.py=brucedawson@chromium.org per-file autoninja_test.py=tikuta@chromium.org +per-file bazel_test.py=file://CROS_OWNERS per-file ninjalog_uploader_test.py=tikuta@chromium.org per-file ninja_reclient_test.py=file://BUILD_OWNERS per-file ninja_reclient_test.py=file://RECLIENT_OWNERS diff --git a/tests/bazel_test.py b/tests/bazel_test.py new file mode 100755 index 000000000..0d92314ae --- /dev/null +++ b/tests/bazel_test.py @@ -0,0 +1,94 @@ +#!/usr/bin/env vpython3 +# Copyright 2023 The ChromiumOS Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# [VPYTHON:BEGIN] +# python_version: "3.8" +# [VPYTHON:END] +"""Tests for Bazel launcher.""" + +import os +from pathlib import Path +import site +import sys +import unittest + +DEPOT_TOOLS_DIR = Path(__file__).resolve().parent.parent +site.addsitedir(DEPOT_TOOLS_DIR) + +import bazel +from testing_support import trial_dir + + +class FindCrosUnittest(trial_dir.TestCase): + """Test the _find_bazel_cros function.""" + def setUp(self): + """Create the checkout and chromite files.""" + super().setUp() + self.checkout_dir = Path(self.root_dir) / "chromiumos" + self.chromite_dir = self.checkout_dir / "chromite" + self.launcher = self.chromite_dir / "bin" / "bazel" + self.launcher.parent.mkdir(exist_ok=True, parents=True) + self.launcher.write_bytes(b"") + self.launcher.chmod(0o775) + self.orig_dir = Path.cwd() + + def tearDown(self): + os.chdir(self.orig_dir) + super().tearDown() + + def test_at_checkout_base(self): + """Test we find the launcher at the base of the checkout.""" + os.chdir(self.checkout_dir) + self.assertEqual(bazel._find_bazel_cros(), self.launcher) + + def test_in_checkout_subdir(self): + """Test we find the launcher in a subdir of the checkout.""" + os.chdir(self.chromite_dir) + self.assertEqual(bazel._find_bazel_cros(), self.launcher) + + def test_out_of_checkout(self): + """Test we don't find the launcher outside of the checkout.""" + os.chdir(self.root_dir) + self.assertIsNone(bazel._find_bazel_cros()) + + +class FindPathUnittest(trial_dir.TestCase): + """Test the _find_next_bazel_in_path function.""" + def setUp(self): + """Create the checkout and chromite files.""" + super().setUp() + + self.bin_dir = Path(self.root_dir) / "bin" + self.bin_dir.mkdir(exist_ok=True, parents=True) + self.orig_path = os.environ.get("PATH", os.defpath) + + # DEPOT_TOOLS_DIR is located twice in PATH for spice. + os.environ["PATH"] = os.pathsep.join([ + str(DEPOT_TOOLS_DIR), + str(self.bin_dir), + str(DEPOT_TOOLS_DIR), + ]) + + def tearDown(self): + """Restore actions from setUp().""" + os.environ["PATH"] = self.orig_path + + def test_not_in_path(self): + """Test we don't find anything in PATH when not present.""" + self.assertIsNone(bazel._find_next_bazel_in_path()) + + def test_in_path(self): + """Test we find the next Bazel in PATH when present.""" + if sys.platform == "win32": + launcher = self.bin_dir / "bazel.exe" + else: + launcher = self.bin_dir / "bazel" + launcher.write_bytes(b"") + launcher.chmod(0o755) + self.assertEqual(bazel._find_next_bazel_in_path(), launcher) + + +if __name__ == '__main__': + unittest.main()