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" "$@"
|
161
projects/bazelshim/src/bazelshim/__main__.py
Normal file → Executable file
161
projects/bazelshim/src/bazelshim/__main__.py
Normal file → Executable 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())
|
||||||
|
|
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