Skip to content

Commit 196c947

Browse files
committed
Avoid arg limits for internal uses of Bazel
1 parent 72f87db commit 196c947

File tree

8 files changed

+153
-14
lines changed

8 files changed

+153
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1010

1111
***Changed:***
1212

13-
- Rename the `[git.user]` section to `[tools.git.author]`
13+
- Rename the `[git.user]` config section to `[tools.git.author]`
1414
- The `Tool` interface now uses a single execution context instead of the dedicated methods `format_command` and `env_vars`
15+
- Rename `build bazel` command to `bzl`
1516

1617
***Added:***
1718

1819
- Add `git` tool
1920
- Add `[user]` section to the configuration
2021
- Add abstract context manager `execution_context` method to the `Tool` interface
22+
- Methods on the `app.tools.bazel` tool now prevent exceeding platform-specific command line length limits
2123

2224
## 0.26.0 - 2025-09-09
2325

docs/reference/api/tools.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,28 @@
55
::: dda.tools.Tools
66
options:
77
members:
8+
- bazel
89
- docker
10+
- git
911
- go
1012
- uv
11-
- git
13+
14+
::: dda.tools.bazel.Bazel
15+
options:
16+
members: []
1217

1318
::: dda.tools.docker.Docker
1419
options:
1520
members: []
1621

17-
::: dda.tools.go.Go
22+
::: dda.tools.git.Git
1823
options:
1924
members: []
2025

21-
::: dda.tools.uv.UV
26+
::: dda.tools.go.Go
2227
options:
2328
members: []
2429

25-
::: dda.tools.git.Git
30+
::: dda.tools.uv.UV
2631
options:
2732
members: []

src/dda/cli/build/bazel/__init__.py renamed to src/dda/cli/bzl/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@
2020
@click.argument("args", nargs=-1)
2121
@pass_app
2222
def cmd(app: Application, *, args: tuple[str, ...]) -> None:
23-
process = app.tools.bazel.attach(list(args), check=False)
23+
with app.tools.bazel.ignore_arg_limits():
24+
process = app.tools.bazel.attach(list(args), check=False)
25+
2426
app.abort(code=process.returncode)

src/dda/tools/bazel.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
from __future__ import annotations
55

66
from contextlib import contextmanager
7-
from functools import cached_property
8-
from typing import TYPE_CHECKING
7+
from functools import cache, cached_property
8+
from typing import TYPE_CHECKING, Any
99

1010
from dda.tools.base import ExecutionContext, Tool
1111
from dda.utils.platform import PLATFORM_ID, which
@@ -28,9 +28,52 @@ class Bazel(Tool):
2828
to an internal location if `bazel` nor `bazelisk` are already on PATH.
2929
"""
3030

31+
def __init__(self, *args: Any, **kwargs: Any) -> None:
32+
super().__init__(*args, **kwargs)
33+
34+
# Avoid platform-specific command line length limits by default
35+
self.__ignore_arg_limits = False
36+
3137
@contextmanager
3238
def execution_context(self, command: list[str]) -> Generator[ExecutionContext, None, None]:
33-
yield ExecutionContext(command=[self.path, *command], env_vars={})
39+
first_arg: str | None = None
40+
for arg in command:
41+
if not arg.startswith("-"):
42+
first_arg = arg
43+
break
44+
45+
if first_arg is None:
46+
yield ExecutionContext(command=[self.path, *command], env_vars={})
47+
return
48+
49+
try:
50+
sep_index = command.index("--")
51+
except ValueError:
52+
if self.__ignore_arg_limits:
53+
yield ExecutionContext(command=[self.path, *command], env_vars={})
54+
return
55+
56+
msg = "Bazel arguments must come after the `--` separator"
57+
raise ValueError(msg) from None
58+
59+
if first_arg in self.target_accepting_commands:
60+
arg_file_flag = "--target_pattern_file"
61+
elif first_arg in self.query_accepting_commands:
62+
arg_file_flag = "--query_file"
63+
else:
64+
yield ExecutionContext(command=[self.path, *command], env_vars={})
65+
return
66+
67+
from tempfile import NamedTemporaryFile
68+
69+
with NamedTemporaryFile(mode="w", encoding="utf-8") as f:
70+
f.write("\n".join(command[sep_index + 1 :]))
71+
f.flush()
72+
73+
yield ExecutionContext(
74+
command=[self.path, *command[:sep_index], arg_file_flag, f.name],
75+
env_vars={},
76+
)
3477

3578
@property
3679
def managed(self) -> bool:
@@ -67,6 +110,22 @@ def __external_path(self) -> str | None:
67110

68111
return None
69112

113+
@property
114+
def target_accepting_commands(self) -> frozenset[str]:
115+
return target_accepting_commands()
116+
117+
@property
118+
def query_accepting_commands(self) -> frozenset[str]:
119+
return query_accepting_commands()
120+
121+
@contextmanager
122+
def ignore_arg_limits(self) -> Generator[None, None, None]:
123+
self.__ignore_arg_limits = True
124+
try:
125+
yield
126+
finally:
127+
self.__ignore_arg_limits = False
128+
70129

71130
def get_download_url() -> str:
72131
import platform
@@ -80,3 +139,13 @@ def get_download_url() -> str:
80139

81140
url = f"https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-{system}-{arch}"
82141
return f"{url}.exe" if PLATFORM_ID == "windows" else url
142+
143+
144+
@cache
145+
def target_accepting_commands() -> frozenset[str]:
146+
return frozenset({"build", "coverage", "fetch", "run", "test"})
147+
148+
149+
@cache
150+
def query_accepting_commands() -> frozenset[str]:
151+
return frozenset({"aquery", "cquery", "query"})
File renamed without changes.

tests/cli/build/test_bazel.py renamed to tests/cli/bzl/test_bzl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def test_default_download(dda, helpers, isolation, mocker):
1414
subprocess_runner = mocker.patch("subprocess.run", return_value=subprocess.CompletedProcess(args=[], returncode=0))
1515

1616
with EnvVars(exclude=["PATH"]):
17-
result = dda("build", "bazel", "build", "//...")
17+
result = dda("bzl", "build", "//...")
1818

1919
assert result.exit_code == 0, result.output
2020
assert result.output == helpers.dedent(
@@ -39,7 +39,7 @@ def test_default_exists(dda, helpers, temp_dir, mocker):
3939
subprocess_runner = mocker.patch("subprocess.run", return_value=subprocess.CompletedProcess(args=[], returncode=0))
4040

4141
with EnvVars({"PATH": str(temp_dir)}):
42-
result = dda("build", "bazel", "build", "//...")
42+
result = dda("bzl", "build", "//...")
4343

4444
assert result.exit_code == 0, result.output
4545
assert not result.output
@@ -59,7 +59,7 @@ def test_config_force_managed(dda, helpers, isolation, config_file, temp_dir, mo
5959
subprocess_runner = mocker.patch("subprocess.run", return_value=subprocess.CompletedProcess(args=[], returncode=0))
6060

6161
with EnvVars({"PATH": str(temp_dir)}):
62-
result = dda("build", "bazel", "build", "//...")
62+
result = dda("bzl", "build", "//...")
6363

6464
assert result.exit_code == 0, result.output
6565
assert result.output == helpers.dedent(
@@ -83,7 +83,7 @@ def test_config_force_unmanaged(dda, helpers, config_file, mocker):
8383
downloader = mocker.patch("dda.utils.network.http.manager.HTTPClientManager.download")
8484

8585
with EnvVars(exclude=["PATH"]):
86-
result = dda("build", "bazel", "build", "//...")
86+
result = dda("bzl", "build", "//...")
8787

8888
assert result.exit_code == 1, result.output
8989
assert result.output == helpers.dedent(

tests/conftest.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from dda.utils.ci import running_in_ci
2121
from dda.utils.fs import Path, temp_directory
2222
from dda.utils.git.constants import GitEnvVars
23-
from dda.utils.platform import PLATFORM_ID
23+
from dda.utils.platform import PLATFORM_ID, which
2424
from dda.utils.process import EnvVars
2525

2626
if TYPE_CHECKING:
@@ -160,6 +160,11 @@ def uv_on_path() -> Path:
160160
return Path(shutil.which("uv"))
161161

162162

163+
@pytest.fixture(scope="session")
164+
def bazel_on_path() -> Path:
165+
return Path(which("bazel"))
166+
167+
163168
def pytest_runtest_setup(item):
164169
for marker in item.iter_markers():
165170
if marker.name == "requires_ci" and not running_in_ci(): # no cov

tests/tools/test_bzl.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# SPDX-FileCopyrightText: 2025-present Datadog, Inc. <dev@datadoghq.com>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
from __future__ import annotations
5+
6+
from typing import TYPE_CHECKING
7+
8+
import pytest
9+
10+
from dda.tools.bazel import query_accepting_commands, target_accepting_commands
11+
12+
if TYPE_CHECKING:
13+
from pytest_mock import MockerFixture
14+
15+
from dda.cli.application import Application
16+
from dda.utils.fs import Path
17+
18+
19+
class TestArgLengthLimits:
20+
@pytest.mark.parametrize("command", sorted(target_accepting_commands()))
21+
def test_ambiguous_targets(self, app: Application, command: str) -> None:
22+
with (
23+
pytest.raises(ValueError, match="Bazel arguments must come after the `--` separator"),
24+
app.tools.bazel.execution_context([command, "//..."]),
25+
):
26+
pass
27+
28+
@pytest.mark.parametrize("command", sorted(query_accepting_commands()))
29+
def test_ambiguous_query(self, app: Application, command: str) -> None:
30+
with (
31+
pytest.raises(ValueError, match="Bazel arguments must come after the `--` separator"),
32+
app.tools.bazel.execution_context([command, "deps(//foo)"]),
33+
):
34+
pass
35+
36+
def test_targets_arg_file(self, app: Application, bazel_on_path: Path, mocker: MockerFixture) -> None:
37+
writer = mocker.MagicMock()
38+
writer.name = "targets.txt"
39+
mocker.patch(
40+
"tempfile.NamedTemporaryFile",
41+
return_value=mocker.MagicMock(__enter__=lambda *_, **__: writer, __exit__=lambda *_, **__: None),
42+
)
43+
with app.tools.bazel.execution_context(["build", "--foo", "--", "//foo", "//bar"]) as context:
44+
assert context.command == [str(bazel_on_path), "build", "--foo", "--target_pattern_file", "targets.txt"]
45+
writer.write.assert_called_once_with("//foo\n//bar")
46+
47+
def test_query_arg_file(self, app: Application, bazel_on_path: Path, mocker: MockerFixture) -> None:
48+
writer = mocker.MagicMock()
49+
writer.name = "query.txt"
50+
mocker.patch(
51+
"tempfile.NamedTemporaryFile",
52+
return_value=mocker.MagicMock(__enter__=lambda *_, **__: writer, __exit__=lambda *_, **__: None),
53+
)
54+
with app.tools.bazel.execution_context(["query", "--foo", "--", "deps(//foo)"]) as context:
55+
assert context.command == [str(bazel_on_path), "query", "--foo", "--query_file", "query.txt"]
56+
writer.write.assert_called_once_with("deps(//foo)")

0 commit comments

Comments
 (0)