Browse Source

[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 <nya@chromium.org>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
Reviewed-by: Aaron Massey <aaronmassey@google.com>
Commit-Queue: Jack Rosenthal <jrosenth@chromium.org>
Auto-Submit: Jack Rosenthal <jrosenth@chromium.org>
Commit-Queue: Josip Sokcevic <sokcevic@chromium.org>
Jack Rosenthal 2 years ago
parent
commit
6a505ad9ab
5 changed files with 183 additions and 0 deletions
  1. 2 0
      OWNERS
  2. 1 0
      bazel
  3. 85 0
      bazel.py
  4. 1 0
      tests/OWNERS
  5. 94 0
      tests/bazel_test.py

+ 2 - 0
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

+ 1 - 0
bazel

@@ -0,0 +1 @@
+bazel.py

+ 85 - 0
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:]))

+ 1 - 0
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

+ 94 - 0
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()