Bazelshim
This commit is contained in:
parent
27af0d2dff
commit
ab3357df3f
6 changed files with 277 additions and 18 deletions
2
.envrc
Normal file
2
.envrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
export SOURCE=$(dirname $(realpath $0))
|
||||
export PATH="${SOURCE}/bin:$PATH"
|
2
bin/bazel
Executable file
2
bin/bazel
Executable 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
2
bin/bazelisk
Executable 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
163
projects/bazelshim/src/bazelshim/__main__.py
Normal file → Executable file
|
@ -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<foo> args to --<foo>=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())
|
||||
|
|
74
projects/bazelshim/test/test_normalizer.py
Normal file
74
projects/bazelshim/test/test_normalizer.py
Normal 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
|
52
projects/bazelshim/test/test_parser.py
Normal file
52
projects/bazelshim/test/test_parser.py
Normal 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
|
Loading…
Reference in a new issue