Bazelshim

This commit is contained in:
Reid 'arrdem' McKenzie 2024-02-06 12:37:41 -07:00
parent 27af0d2dff
commit ab3357df3f
6 changed files with 277 additions and 18 deletions

2
.envrc Normal file
View file

@ -0,0 +1,2 @@
export SOURCE=$(dirname $(realpath $0))
export PATH="${SOURCE}/bin:$PATH"

2
bin/bazel Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
exec "${SOURCE}/projects/bazelshim/src/bazelshim/__main__.py" --bazelshim_exclude="$(realpath "${SOURCE}")/bin" "$@"

2
bin/bazelisk Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
exec "${SOURCE}/projects/bazelshim/src/bazelshim/__main__.py" --bazelshim_exclude="$(realpath "${SOURCE}")/bin" "$@"

163
projects/bazelshim/src/bazelshim/__main__.py Normal file → Executable file
View file

@ -2,15 +2,48 @@
# A Bazel wrapper # A Bazel wrapper
# #
# This script exists to allow for the setting of environment viariables and other context flags to Bazel on behalf of # This script exists to allow for the setting of environment viariables and other context flags to
# the user. Consequently it has some magical (partial) knowledge of Bazel's CLI options since it's really a CLI shim. # 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 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]: def normalize_opts(args: List[str]) -> List[str]:
acc = [] acc = []
@ -24,7 +57,7 @@ def normalize_opts(args: List[str]) -> List[str]:
acc.extend(args) acc.extend(args)
break break
elif args[0].contains("="): elif "=" in args[0]:
# If it's a k/v form pass it through # If it's a k/v form pass it through
acc.append(args.pop(0)) acc.append(args.pop(0))
@ -32,7 +65,11 @@ def normalize_opts(args: List[str]) -> List[str]:
# Convert --no<foo> args to --<foo>=no # Convert --no<foo> args to --<foo>=no
acc.append("--" + args.pop(0).lstrip("--no") + "=false") 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 # If the next thing isn't an opt, assume it's a '--a b' form
acc.append(args[0] + "=" + args[1]) acc.append(args[0] + "=" + args[1])
args.pop(0) args.pop(0)
@ -42,27 +79,117 @@ def normalize_opts(args: List[str]) -> List[str]:
# Assume it's a boolean true flag # Assume it's a boolean true flag
acc.append(args.pop(0) + "=true") acc.append(args.pop(0) + "=true")
elif args[0] in VERBS: else:
acc.append(args.pop(0)) acc.append(args.pop(0))
else: else:
raise ValueError(repr(args)) if args:
acc.extend(args)
return acc 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 @dataclass
class BazelCli: class BazelCli:
binary: str
startup_opts: List[str] startup_opts: List[str]
command: Optional[str] command: Optional[str]
command_opts: List[str] command_opts: List[str]
subprocess_opts: List[str] subprocess_opts: List[str]
@classmethod @classmethod
def parse_cli(cls, args: List[str]) -> BazelCLI: def parse_cli(cls, args: List[str]) -> "BazelCli":
pass 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())

View file

@ -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

View file

@ -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