From 58c416f1bdb118f1bf17c7a24df46172358196c1 Mon Sep 17 00:00:00 2001 From: Reid 'arrdem' McKenzie Date: Tue, 6 Feb 2024 12:37:41 -0700 Subject: [PATCH] Bazelshim --- .envrc | 2 + bin/bazel | 2 + bin/bazelisk | 2 + projects/bazelshim/src/bazelshim/__main__.py | 163 +++++++++++++++++-- projects/bazelshim/test/test_normalizer.py | 74 +++++++++ projects/bazelshim/test/test_parser.py | 52 ++++++ 6 files changed, 277 insertions(+), 18 deletions(-) create mode 100644 .envrc create mode 100755 bin/bazel create mode 100755 bin/bazelisk mode change 100644 => 100755 projects/bazelshim/src/bazelshim/__main__.py create mode 100644 projects/bazelshim/test/test_normalizer.py create mode 100644 projects/bazelshim/test/test_parser.py diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..c589014 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +export SOURCE=$(dirname $(realpath $0)) +export PATH="${SOURCE}/bin:$PATH" diff --git a/bin/bazel b/bin/bazel new file mode 100755 index 0000000..40517be --- /dev/null +++ b/bin/bazel @@ -0,0 +1,2 @@ +#!/bin/sh +exec "${SOURCE}/projects/bazelshim/src/bazelshim/__main__.py" --bazelshim_exclude="$(realpath "${SOURCE}")/bin" "$@" diff --git a/bin/bazelisk b/bin/bazelisk new file mode 100755 index 0000000..40517be --- /dev/null +++ b/bin/bazelisk @@ -0,0 +1,2 @@ +#!/bin/sh +exec "${SOURCE}/projects/bazelshim/src/bazelshim/__main__.py" --bazelshim_exclude="$(realpath "${SOURCE}")/bin" "$@" diff --git a/projects/bazelshim/src/bazelshim/__main__.py b/projects/bazelshim/src/bazelshim/__main__.py old mode 100644 new mode 100755 index 9cc9f21..b953403 --- a/projects/bazelshim/src/bazelshim/__main__.py +++ b/projects/bazelshim/src/bazelshim/__main__.py @@ -2,15 +2,48 @@ # A Bazel wrapper # -# This script exists to allow for the setting of environment viariables and other context flags to Bazel on behalf of -# the user. Consequently it has some magical (partial) knowledge of Bazel's CLI options since it's really a CLI shim. +# This script exists to allow for the setting of environment viariables and other context flags to +# Bazel on behalf of the user. Consequently it has some magical (partial) knowledge of Bazel's CLI +# options since it's really a CLI shim. -import sys -from typing import List, Optional -from shlex import split as shlex from dataclasses import dataclass +from shlex import quote, split as shlex +import sys +import os +from pathlib import Path +from typing import List, Optional +from itertools import chain + + +VERBS = [ + "aquery", + "build", + "clean", + "coverage", + "cquery", + "dump", + "fetch", + "help", + "info", + "mod", + "query", + "run", + "sync", + "test", +] + + +def path(): + for it in os.getenv("PATH").split(":"): + yield Path(it) + + +def which(cmd): + for it in path(): + f: Path = (it / cmd).absolute() + if f.exists() and f.stat().st_mode & 0x700: + yield f -VERBS = ["sync", "build", "aquery", "query", "cquery", "run", "test", "coverage", "dump", "fetch", "help", "info", "mod",] def normalize_opts(args: List[str]) -> List[str]: acc = [] @@ -24,7 +57,7 @@ def normalize_opts(args: List[str]) -> List[str]: acc.extend(args) break - elif args[0].contains("="): + elif "=" in args[0]: # If it's a k/v form pass it through acc.append(args.pop(0)) @@ -32,7 +65,11 @@ def normalize_opts(args: List[str]) -> List[str]: # Convert --no args to --=no acc.append("--" + args.pop(0).lstrip("--no") + "=false") - elif args[0].startswith("--") and not args[1].startswith("--") and args[1] not in VERBS: + elif ( + args[0].startswith("--") + and not args[1].startswith("--") + and args[1] not in VERBS + ): # If the next thing isn't an opt, assume it's a '--a b' form acc.append(args[0] + "=" + args[1]) args.pop(0) @@ -42,27 +79,117 @@ def normalize_opts(args: List[str]) -> List[str]: # Assume it's a boolean true flag acc.append(args.pop(0) + "=true") - elif args[0] in VERBS: + else: acc.append(args.pop(0)) - else: - raise ValueError(repr(args)) + else: + if args: + acc.extend(args) return acc -assert normalize_opts(shlex("bazel clean")) == ["bazel", "clean"] -assert normalize_opts(shlex("bazel --client_debug clean")) == ["bazel", "--client_debug=true", "clean"] -assert normalize_opts(shlex("bazel build //foo:bar //baz:*")) == ["bazel", "build", "//foo:bar", "//baz:*"] -assert normalize_opts(shlex("bazel test //foo:bar //baz:* -- -vvv")) == ["bazel", "test", "//foo:bar", "//baz:*", "--", "-vvv"] -assert normalize_opts(shlex("bazel run //foo:bar -- --foo=bar --baz=qux")) == ["bazel", "run", "//foo:bar", "--", "--foo=bar", "--baz=qux"] @dataclass class BazelCli: + binary: str startup_opts: List[str] command: Optional[str] command_opts: List[str] subprocess_opts: List[str] @classmethod - def parse_cli(cls, args: List[str]) -> BazelCLI: - pass + def parse_cli(cls, args: List[str]) -> "BazelCli": + args = normalize_opts(args) + binary = args.pop(0) + + startup_opts = [] + while args and args[0].startswith("--"): + startup_opts.append(args.pop(0)) + + command = None + if args and args[0] in VERBS: + command = args.pop(0) + + command_opts = [] + while args and args[0] != "--": + command_opts.append(args.pop(0)) + + subprocess_opts = [] + if args: + if args[0] == "--": + args.pop(0) + subprocess_opts.extend(args) + + return cls( + binary=binary, + startup_opts=startup_opts, + command=command, + command_opts=command_opts, + subprocess_opts=subprocess_opts, + ) + + def render_cli(self): + acc = [ + self.binary, + *self.startup_opts, + ] + if self.command: + acc.append(self.command) + acc.extend(self.command_opts) + + if self.command == "test": + acc.extend(["--test_arg=" + it for it in self.subprocess_opts]) + elif self.command == "run": + acc.append("--") + acc.extend(self.subprocess_opts) + else: + print( + f"Warning: {self.command} does not support -- args!", + file=sys.stderr, + ) + + return acc + + def executable(self, exclude: List[Path]): + """Try to resolve as via which() an executable to delegate to.""" + + for p in chain(which("bazelisk"), which("bazel")): + if p.parent not in exclude: + return str(p) + + def render_text(self, next): + lines = [] + lines.append(" " + next) + for arg in self.startup_opts: + lines.append(" " + arg) + if self.command: + lines.append(" " + self.command) + for arg in self.command_opts: + lines.append(" " + arg) + if self.subprocess_opts: + lines.append(" --") + for arg in self.subprocess_opts: + lines.append(" " + arg) + return "\\\n".join(lines) + + +def middleware(cli): + return cli + + +if __name__ == "__main__": + exclude = [] + while len(sys.argv) > 1 and sys.argv[1].startswith("--bazelshim_exclude"): + exclude.append(Path(sys.argv.pop(1).split("=")[1]).absolute()) + + us = Path(sys.argv[0]).absolute() + exclude.append(us.parent) + + cli = BazelCli.parse_cli(["bazel"] + sys.argv[1:]) + cli = middleware(cli) + next = cli.executable(exclude=exclude) + print( + "Info: Executing\n" + cli.render_text(next), + file=sys.stderr, + ) + os.execv(next, cli.render_cli()) diff --git a/projects/bazelshim/test/test_normalizer.py b/projects/bazelshim/test/test_normalizer.py new file mode 100644 index 0000000..a2cf3ca --- /dev/null +++ b/projects/bazelshim/test/test_normalizer.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +from shlex import split as shlex + +from bazelshim.__main__ import normalize_opts + +import pytest + + +@pytest.mark.parametrize( + "a, b", + [ + ( + "bazel clean", + [ + "bazel", + "clean", + ], + ), + ( + "bazel --client_debug clean", + [ + "bazel", + "--client_debug=true", + "clean", + ], + ), + ( + "bazel build //foo:bar //baz:*", + [ + "bazel", + "build", + "//foo:bar", + "//baz:*", + ], + ), + ( + "bazel test //foo:bar //baz:* -- -vvv", + [ + "bazel", + "test", + "//foo:bar", + "//baz:*", + "--", + "-vvv", + ], + ), + ( + "bazel test --shell_executable /bin/bish //foo:bar //baz:* -- -vvv", + [ + "bazel", + "test", + "--shell_executable=/bin/bish", + "//foo:bar", + "//baz:*", + "--", + "-vvv", + ], + ), + ( + "bazel run //foo:bar -- --foo=bar --baz=qux", + [ + "bazel", + "run", + "//foo:bar", + "--", + "--foo=bar", + "--baz=qux", + ], + ), + ], +) +def test_normalize_opts(a, b): + assert normalize_opts(shlex(a)) == b diff --git a/projects/bazelshim/test/test_parser.py b/projects/bazelshim/test/test_parser.py new file mode 100644 index 0000000..c2cc3d0 --- /dev/null +++ b/projects/bazelshim/test/test_parser.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +from shlex import split as shlex + +from bazelshim.__main__ import BazelCli + +import pytest + + +@pytest.mark.parametrize( + "a, b", + [ + ( + "bazel clean", + BazelCli("bazel", [], "clean", [], []), + ), + ( + "bazel --client_debug clean", + BazelCli("bazel", ["--client_debug=true"], "clean", [], []), + ), + ( + "bazel build //foo:bar //baz:*", + BazelCli("bazel", [], "build", ["//foo:bar", "//baz:*"], []), + ), + ( + "bazel test //foo:bar //baz:* -- -vvv", + BazelCli("bazel", [], "test", ["//foo:bar", "//baz:*"], ["-vvv"]), + ), + ( + "bazel test --shell_executable /bin/bish //foo:bar //baz:* -- -vvv", + BazelCli( + "bazel", + [], + "test", + ["--shell_executable=/bin/bish", "//foo:bar", "//baz:*"], + ["-vvv"], + ), + ), + ( + "bazel run //foo:bar -- --foo=bar --baz=qux", + BazelCli( + "bazel", + [], + "run", + ["//foo:bar"], + ["--foo=bar", "--baz=qux"], + ), + ), + ], +) +def test_normalize_opts(a, b): + assert BazelCli.parse_cli(shlex(a)) == b