From 6118df48433026459183fae861e104ae79fbe956 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Oct 2026 17:11:45 +0200 Subject: [PATCH 1/6] tine: honor BUCK_ISOLATION_DIR when parsing Buck commands Buck takes the isolation dir from `--isolation-dir` or `$BUCK_ISOLATION_DIR`, but the wrapper's parser did not check the latter. Signed-off-by: Martin Pitt --- bin/tine.py | 6 +++++- bin/tine_test.py | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bin/tine.py b/bin/tine.py index 984e2515..6604d0d3 100644 --- a/bin/tine.py +++ b/bin/tine.py @@ -1687,7 +1687,11 @@ def parse_buck_command(argv: list[str]) -> BuckCommand: elif not word.startswith("-") and subcommand is None: subcommand = word rest = rest[2:] if word in VALUE_FLAGS else rest[1:] - return BuckCommand(subcommand=subcommand, isolation=isolation) + return BuckCommand( + subcommand=subcommand, + # The flag's own fallback, which Buck honors wherever the flag may appear. + isolation=isolation or os.environ.get("BUCK_ISOLATION_DIR"), + ) def glob_literal(value: str) -> str: diff --git a/bin/tine_test.py b/bin/tine_test.py index 78f02a1a..5f81358a 100644 --- a/bin/tine_test.py +++ b/bin/tine_test.py @@ -3183,6 +3183,11 @@ def test_no_subcommand(self) -> None: self.assertIsNone(tine.parse_buck_command([]).subcommand) self.assertIsNone(tine.parse_buck_command(["--", "build"]).subcommand) + def test_isolation_falls_back_to_bucks_environment_variable(self) -> None: + with unittest.mock.patch.dict(os.environ, {"BUCK_ISOLATION_DIR": "elsewhere"}): + self.assertEqual(tine.parse_buck_command(["build", "//x"]).isolation, "elsewhere") + self.assertEqual(tine.parse_buck_command(["--isolation-dir", "x", "build"]).isolation, "x") + def test_option_separator(self) -> None: self.assertIsNone( tine.parse_buck_command(["run", "//x", "--", "--isolation-dir", "other"]).isolation From 2c28b1dfa93f9c2fbd460aacdaae172d786c2e03 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Fri, 9 Oct 2026 18:07:58 +0200 Subject: [PATCH 2/6] tools: drop the unused --buck options Nothing uses them, and by now our buck2 is modified so heavily that using a different/non-tested buck2 is "here be dragons" territorry. Signed-off-by: Martin Pitt --- tools/catalog.py | 14 +++++-------- tools/dev.py | 53 +++++++++++++++++++++++------------------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/tools/catalog.py b/tools/catalog.py index 84dc01ce..e9b594d9 100644 --- a/tools/catalog.py +++ b/tools/catalog.py @@ -621,11 +621,6 @@ def main(argv: list[str] | None = None) -> None: default=DEFAULT_CATALOG, help=f"catalog package to refresh (default: {DEFAULT_CATALOG})", ) - p.add_argument( - "--buck", - default=nested_buck(), - help="buck binary to nest (default: $BUCK2_BINARY, else PATH)", - ) p.add_argument( "--box", action="append", @@ -647,6 +642,7 @@ def main(argv: list[str] | None = None) -> None: help="commit the refreshed catalog", ) args = p.parse_args(argv) + buck = nested_buck() if args.commit and args.verify: p.error("--verify leaves the checkout as it found it, so there is nothing to commit") if args.advance and args.verify: @@ -654,15 +650,15 @@ def main(argv: list[str] | None = None) -> None: catalog = _catalog_pattern(args.catalog) # Run nested commands from the project root so wrappers resolve consistently. - with contextlib.chdir(buck_output(args.buck, "root", "--kind", "project")) as _: - catalog_dir, snapshots, resolves = _plan(args.buck, catalog, args.box) + with contextlib.chdir(buck_output(buck, "root", "--kind", "project")) as _: + catalog_dir, snapshots, resolves = _plan(buck, catalog, args.box) # Lazy: nothing is snapshotted until the loop below asks, after the pins have moved. - regenerated = _regenerate(args.buck, catalog_dir, snapshots, resolves) + regenerated = _regenerate(buck, catalog_dir, snapshots, resolves) if not args.verify: checkout = _Checkout() try: if args.advance: - _advance_snapshots(checkout, args.buck, catalog, catalog_dir, snapshots) + _advance_snapshots(checkout, buck, catalog, catalog_dir, snapshots) for path, content in regenerated: checkout.write(path, content) except BaseException: diff --git a/tools/dev.py b/tools/dev.py index deaa7069..a16c1222 100644 --- a/tools/dev.py +++ b/tools/dev.py @@ -127,10 +127,11 @@ def _fmt_diff(args: argparse.Namespace, src: Path) -> str: def _lint(args: argparse.Namespace) -> None: - cell = _cell_root(args.buck, "tine") - srcs = _starlark_srcs(args.buck) + buck = nested_buck() + cell = _cell_root(buck, "tine") + srcs = _starlark_srcs(buck) _bold("test targets") - if orphans := _orphan_tests(args.buck, cell): + if orphans := _orphan_tests(buck, cell): listing = "\n".join(f" {p.relative_to(cell)}" for p in orphans) fail(f"no box_python_test lists these, so they never run:\n{listing}") _bold("ruff") @@ -139,15 +140,13 @@ def _lint(args: argparse.Namespace) -> None: _bold("reuse") # Scoped by --root rather than by the cwd Buck runs this in, so a consuming project checks the # cell it declares and not whatever else sits beside it. - _run([args.buck, "-v", "0", "run", "tine//tools:reuse", "--", "--root", cell, "lint", "--lines"]) + _run([buck, "-v", "0", "run", "tine//tools:reuse", "--", "--root", cell, "lint", "--lines"]) _bold("ty") - targets = buck_output( - args.buck, "uquery", f"attrfilter(labels, 'python-typecheck', {_universe()})" - ).split() + targets = buck_output(buck, "uquery", f"attrfilter(labels, 'python-typecheck', {_universe()})").split() if not targets: fail("ty: no generated type-check targets found") _require_manifests(cell, _ty_packages(targets)) - _run([args.buck, "build", *targets]) + _run([buck, "build", *targets]) _bold("starlark_fmt") # starlark_fmt has no check mode, so diff each file and fail on the first rewrite it would make. if diffs := [diff for src in srcs if (diff := _fmt_diff(args, src))]: @@ -156,21 +155,21 @@ def _lint(args: argparse.Namespace) -> None: # Pass the files rather than the cell directory: standalone, the tine cell is the project root, # which Buck normalizes to an empty path and rejects. _bold("starlark lint") - _run([args.buck, "-v", "0", "starlark", "lint", "--console", "none", *srcs]) + _run([buck, "-v", "0", "starlark", "lint", "--console", "none", *srcs]) _bold("starlark typecheck") # Typecheck errors use stdout; stderr is only the per-file event log. Unlike lint, typecheck # follows load() into data files and parses them as Starlark, which no TOML survives (a JSON # object happens to be a valid Starlark expression), so files with a TOML load stay out; the # probe also matches the `?format=toml` spelling. checkable = [f for f in srcs if 'toml"' not in f.read_text(encoding="utf-8")] - _run([args.buck, "-v", "0", "starlark", "typecheck", *checkable], stderr=subprocess.DEVNULL) + _run([buck, "-v", "0", "starlark", "typecheck", *checkable], stderr=subprocess.DEVNULL) _bold("target graph") # Analysis, not a build: it reaches every rule a build would run, without producing anything. # Scoped to this cell, whose platform the parser knows how to detect; what a consuming project # declares is its own to check, with its own pattern. _run( [ - args.buck, + buck, "-v", "0", "bxl", @@ -185,11 +184,12 @@ def _lint(args: argparse.Namespace) -> None: def _check(args: argparse.Namespace) -> None: + buck = nested_buck() _lint(args) _bold("unit tests") # Building an example image is minutes where these are seconds. Everything that needs one is # labelled `image` and covered by `buck test tine//... --include image`, which is what CI runs. - _run([args.buck, "test", "--skip-incompatible-targets", *_targets(args.buck), "--exclude", "image"]) + _run([buck, "test", "--skip-incompatible-targets", *_targets(buck), "--exclude", "image"]) def _print_universe(_args: argparse.Namespace) -> None: @@ -197,13 +197,14 @@ def _print_universe(_args: argparse.Namespace) -> None: def _fmt(args: argparse.Namespace) -> None: + buck = nested_buck() # Ask Buck everything before the formatters touch the tree. This runs under `buck run`, whose # command stays active for as long as the binary does, and buck2 only recognizes a nested # command as nested when it spawned the process itself, which it does for actions but not for # run targets. A query issued after a write therefore needs a newer state than the command it # is nested in and waits for it to finish: a deadlock rather than an error. - cell = _cell_root(args.buck, "tine") - srcs = _starlark_srcs(args.buck) + cell = _cell_root(buck, "tine") + srcs = _starlark_srcs(buck) _bold("ruff") _run([args.ruff, "format", "--no-cache", cell]) _run([args.ruff, "check", "--fix", "--no-cache", cell]) @@ -220,8 +221,9 @@ def _write_dot(path: Path, intra: dict[str, list[str]], rev: dict[str, list[str] def _scc(args: argparse.Namespace) -> None: + buck = nested_buck() # Graph derivation and cycle detection live in buck (rpm_branch); this only formats its output. - graph = buck_output(args.buck, "build", f"{args.branch}:_buildrequires_graph", "--out", "-") + graph = buck_output(buck, "build", f"{args.branch}:_buildrequires_graph", "--out", "-") data = cast(dict[str, Any], json.loads(graph)) edge_caps = cast(dict[str, dict[str, list[str]]], data["edges"]) components: dict[int, set[str]] = {} @@ -393,13 +395,14 @@ def _ty_environment(buck: str, cell: Path, package: Path, python: Path) -> dict[ def _ty(args: argparse.Namespace) -> None: + buck = nested_buck() package = Path(args.package) if package.is_absolute() or ".." in package.parts: fail("--package must be a directory relative to the tine cell") - cell = _cell_root(args.buck, "tine") + cell = _cell_root(buck, "tine") targets = json.loads( buck_output( - args.buck, + buck, "uquery", "attrfilter(labels, 'python-typecheck', tine//...)" " + kind('python_bootstrap_library', tine//...)", @@ -409,7 +412,7 @@ def _ty(args: argparse.Namespace) -> None: ) roots = _ty_import_roots(cell, package, targets) # Finish nested Buck commands before writing files, as in _fmt. - environment = _ty_environment(args.buck, cell, package, Path(args.python)) + environment = _ty_environment(buck, cell, package, Path(args.python)) _ty_zed_files(cell, targets) manifest = cell / package / "pyproject.toml" config = _read_ty_config(cell / "ty.toml", manifest if manifest.is_file() else None) @@ -431,12 +434,6 @@ def _ty(args: argparse.Namespace) -> None: def main(argv: list[str] | None = None) -> None: - common = argparse.ArgumentParser(add_help=False) - common.add_argument( - "--buck", - default=nested_buck(), - help="buck binary to nest (default: $BUCK2_BINARY, else PATH)", - ) starlark = argparse.ArgumentParser(add_help=False) starlark.add_argument("--starlark-fmt", required=True) starlark.add_argument("--starlark-fmt-config", required=True, help="starlark_fmt --config tables") @@ -447,18 +444,18 @@ def main(argv: list[str] | None = None) -> None: ("lint", _lint, "run the source lints (fmt fixes)"), ("check", _check, "run the source lints, then every unit-test suite"), ): - verb = sub.add_parser(name, parents=[common, starlark], help=help_text) + verb = sub.add_parser(name, parents=[starlark], help=help_text) verb.add_argument("--ruff", required=True) verb.set_defaults(func=func) universe = sub.add_parser("universe", help="print the query for what a whole-cell run asks for here") universe.set_defaults(func=_print_universe) - fmt = sub.add_parser("fmt", parents=[common, starlark], help="auto-format and auto-fix lints") + fmt = sub.add_parser("fmt", parents=[starlark], help="auto-format and auto-fix lints") fmt.add_argument("--ruff", required=True) fmt.set_defaults(func=_fmt) - scc = sub.add_parser("scc", parents=[common], help="analyze a branch's BuildRequires cycles") + scc = sub.add_parser("scc", help="analyze a branch's BuildRequires cycles") scc.add_argument("branch", help="branch label, e.g. //packages/fedora/rawhide") scc.add_argument("--why", metavar="PKG", help="show one cycle member's edges and their reasons") scc.add_argument("--dot", type=Path, help="write the cycle subgraph as graphviz") @@ -470,7 +467,7 @@ def main(argv: list[str] | None = None) -> None: ty_config.add_argument("--output", type=Path, required=True) ty_config.set_defaults(func=_ty_config) - ty = sub.add_parser("ty", parents=[common], help="run pinned ty in a package's Python environment") + ty = sub.add_parser("ty", help="run pinned ty in a package's Python environment") ty.add_argument("--python", required=True, help="pinned standalone Python for stdlib-only packages") ty.add_argument("--ty", required=True) ty.add_argument("--package", default=".", help="Python source package relative to the tine cell") From e96b5a89baf7518622d5ce3d2a43ef8a81782fec Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Sat, 10 Oct 2026 15:58:24 +0000 Subject: [PATCH 3/6] util: replace nested_buck() with buck2_argv(), require our own buck2 Every driver built its nested Buck command by hand from `nested_buck()` and passed the binary path through a lot of helper functions. Centralize building a command argv, so that in the future, passing global options like `--no-buckd` happen in this single place. Also stop falling back to a plain `buck` binary. By now we hard-depend on our fork. Let `tine` control the buck2 binary that runs. Signed-off-by: Martin Pitt --- examples/image/expected.py | 4 +- tests/cache_roundtrip.py | 7 ++- tools/catalog.py | 103 ++++++++++++++----------------------- tools/catalog_test.py | 13 ++--- tools/dev.py | 71 ++++++++++++------------- tools/dev_test.py | 15 +++--- util.py | 18 +++---- 7 files changed, 96 insertions(+), 135 deletions(-) diff --git a/examples/image/expected.py b/examples/image/expected.py index 91a82c41..aa6d5f05 100644 --- a/examples/image/expected.py +++ b/examples/image/expected.py @@ -13,7 +13,7 @@ import json import sys -from util import amend_paths, atomic_write_text, fail, nested_buck, package_directory +from util import amend_paths, atomic_write_text, fail, package_directory PACKAGE = "tine//examples/image" EXPECTATIONS = "expected.json" @@ -26,7 +26,7 @@ def main(argv: list[str] | None = None) -> None: ) args = parser.parse_args(argv) - directory = package_directory(nested_buck(), PACKAGE) + directory = package_directory(PACKAGE) path = directory / EXPECTATIONS actual = sorted(directory.glob("actual.*.json")) if not actual: diff --git a/tests/cache_roundtrip.py b/tests/cache_roundtrip.py index 57ebe0b4..c8c4b65a 100644 --- a/tests/cache_roundtrip.py +++ b/tests/cache_roundtrip.py @@ -32,7 +32,7 @@ from typing import cast import cache_shim -from util import buck_output, fail, nested_buck +from util import buck2_argv, buck_output, fail import seaweed @@ -207,8 +207,7 @@ def main() -> None: parser.add_argument("--weed", type=Path, required=True, help="the SeaweedFS binary") arguments = parser.parse_args() - buck = nested_buck() - root = Path(buck_output(buck, "root", "--kind", "project")) + root = Path(buck_output("root", "--kind", "project")) if (root / LOCAL_SETTINGS).exists(): fail(f"{root / LOCAL_SETTINGS} exists, and this writes one of its own; move it away first") @@ -218,7 +217,7 @@ def main() -> None: stack.callback(weed.close) # Create CA/leaf keys. No settings yet, so no shim is started. subprocess.run( - [buck, "-v", "0", "run", "tine//remote_cache:test-ca", "--", str(scratch)], check=True + [*buck2_argv("-v", "0", "run", "tine//remote_cache:test-ca"), "--", str(scratch)], check=True ) run = RoundTrip(root, scratch, weed) stack.callback(run.teardown) diff --git a/tools/catalog.py b/tools/catalog.py index e9b594d9..3d86de76 100644 --- a/tools/catalog.py +++ b/tools/catalog.py @@ -24,7 +24,6 @@ import base64 import contextlib import difflib -import functools import itertools import json import subprocess @@ -37,10 +36,10 @@ from util import ( atomic_write_text, + buck2_argv, buck_output, commit_paths, fail, - nested_buck, package_directory, urlopen, with_retries, @@ -72,20 +71,20 @@ def _catalog_pattern(catalog: str) -> str: return f"{package}:" -def _targets_with_label(buck: str, catalog: str, label: str) -> list[str]: +def _targets_with_label(catalog: str, label: str) -> list[str]: """Targets carrying `label` in the selected catalog package.""" - return sorted(buck_output(buck, "uquery", f"attrfilter(labels, '{label}', {catalog})").split()) + return sorted(buck_output("uquery", f"attrfilter(labels, '{label}', {catalog})").split()) def _name_of(target: str) -> str: return target.rsplit(":", 1)[1] -def _catalog_directory(buck: str, targets: list[str]) -> Path: +def _catalog_directory(targets: list[str]) -> Path: packages = {target.rsplit(":", 1)[0] for target in targets} if len(packages) != 1: fail(f"catalog: expected targets in one package, found {sorted(packages)}") - return package_directory(buck, packages.pop()) + return package_directory(packages.pop()) def _lock_path(kind: str, stem: str, architecture: str) -> Path: @@ -117,7 +116,7 @@ def _box_lock_path(target: str) -> Path: return _lock_path("box", box.removesuffix(".box"), architecture) -def _run(buck: str, target: str) -> str: +def _run(target: str) -> str: """Run a refresh target, returning what it wrote to stdout. Buck execs the target rather than piping it, and its own output is on stderr, so stdout is @@ -125,7 +124,7 @@ def _run(buck: str, target: str) -> str: the project and nothing else, so a path outside it would land in the sandbox's own tmpfs. """ # Mute nested Buck while preserving driver progress on stderr. - command = [buck, "-v", "0", "run", target, "--console", "none", "--", "--out", "-"] + command = buck2_argv("-v", "0", "run", target, "--console", "none", "--", "--out", "-") return subprocess.run(command, check=True, stdout=subprocess.PIPE, encoding="utf-8").stdout @@ -146,10 +145,9 @@ def name(self) -> str: return _name_of(self.target) -def _pinned_repositories(buck: str, catalog: str, label: str, prefix: str) -> list[Pin]: +def _pinned_repositories(catalog: str, label: str, prefix: str) -> list[Pin]: """The repositories in `catalog` whose base URLs come from a `.*` mirror pin.""" out = buck_output( - buck, "uquery", "--json", "--output-attribute=^(architectures|metadata)$", @@ -208,10 +206,9 @@ def restore(self) -> None: raise failed -def _served_architectures(buck: str, repositories: list[str]) -> dict[str, tuple[str, ...]]: +def _served_architectures(repositories: list[str]) -> dict[str, tuple[str, ...]]: """The architectures each repository's mirror serves, and so the locks it keeps.""" out = buck_output( - buck, "uquery", "--json", "--output-attribute=^architectures$", @@ -290,7 +287,7 @@ def _datestamp(snapshot: str) -> str: return snapshot.rsplit("-", 1)[1] -def _pinned_snapshot(buck: str, pin: Pin, architecture: str) -> str: +def _pinned_snapshot(pin: Pin, architecture: str) -> str: """The snapshot id one architecture of a pinned repository is served from, as the mirror spells it. The rule that expands the pin's placeholder writes it into that architecture's snapshot spec, @@ -298,12 +295,12 @@ def _pinned_snapshot(buck: str, pin: Pin, architecture: str) -> str: """ # Keep the subtarget name in sync with `_manifest_subtarget` in package/repository.bzl. subtarget = f"{pin.target}[manifest.{architecture}]" - manifest = buck_output(buck, "build", "--show-full-simple-output", subtarget) + manifest = buck_output("build", "--show-full-simple-output", subtarget) spec = cast(dict[str, str], json.loads(Path(manifest).read_text(encoding="utf-8"))) return spec["baseurl"].rsplit("/", 1)[1] -def _newest_rpmrepo_snapshot(buck: str, pins: list[Pin]) -> str: +def _newest_rpmrepo_snapshot(pins: list[Pin]) -> str: """The newest snapshot every repository and architecture sharing one rpmrepo pin can move to. The answer is a pin like the one read, placeholder and all: a refresh moves the datestamp and @@ -311,7 +308,7 @@ def _newest_rpmrepo_snapshot(buck: str, pins: list[Pin]) -> str: """ offers = { f"{pin.name} ({architecture})": _newest_snapshot( - pin.name, pin.mirror, _series(_pinned_snapshot(buck, pin, architecture)) + pin.name, pin.mirror, _series(_pinned_snapshot(pin, architecture)) ) for pin in pins for architecture in pin.architectures @@ -393,9 +390,7 @@ def timestamps() -> str: return max(with_retries("archive: timestamps", timestamps), pin.snapshot) -def _advance_snapshots( - checkout: _Checkout, buck: str, catalog: str, catalog_dir: Path, selected: list[str] -) -> None: +def _advance_snapshots(checkout: _Checkout, catalog: str, catalog_dir: Path, selected: list[str]) -> None: """Advance the selected mirror-pinned repositories to the newest snapshot their mirror offers. Advancing a pin without re-snapshotting the repository it belongs to would leave a base URL @@ -406,16 +401,11 @@ def _advance_snapshots( wanted = set(selected) advanced = 0 for label, prefix, attribute, newest in ( - ( - RPM_REMOTE_REPOSITORY_LABEL, - "rpmrepo", - "rpmrepo_snapshot", - functools.partial(_newest_rpmrepo_snapshot, buck), - ), + (RPM_REMOTE_REPOSITORY_LABEL, "rpmrepo", "rpmrepo_snapshot", _newest_rpmrepo_snapshot), (PACMAN_REMOTE_REPOSITORY_LABEL, "archlinux", "archive_snapshot", _newest_archive_snapshot), (DEB_REMOTE_REPOSITORY_LABEL, "debian", "archive_snapshot", _newest_debian_snapshot), ): - pinned = _pinned_repositories(buck, catalog, label, prefix) + pinned = _pinned_repositories(catalog, label, prefix) advanced += _advance(checkout, pinned, wanted, newest, declaration, attribute) if not advanced: # Asked for and not done is worth a line: every selected pin is either newest already or @@ -423,10 +413,9 @@ def _advance_snapshots( print("==> no pin advanced", file=sys.stderr) -def _declared_signing_keys(buck: str, repositories: list[str]) -> dict[str, str]: +def _declared_signing_keys(repositories: list[str]) -> dict[str, str]: """Where each signing key the given repositories declare is fetched from, by fingerprint.""" out = buck_output( - buck, "uquery", "--json", "--output-attribute=^signing_keys$", @@ -485,33 +474,29 @@ def fetch() -> bytes: fail(f"catalog: {url} is not an OpenPGP public key, armored or binary") -def _signing_keys( - buck: str, - catalog_dir: Path, - repositories: list[str], -) -> Iterator[tuple[Path, str]]: +def _signing_keys(catalog_dir: Path, repositories: list[str]) -> Iterator[tuple[Path, str]]: """Fetch the declared signing keys the catalog does not hold yet.""" - for fingerprint, url in sorted(_declared_signing_keys(buck, repositories).items()): + for fingerprint, url in sorted(_declared_signing_keys(repositories).items()): path = catalog_dir / SIGNING_KEY_DIRECTORY / (fingerprint + SIGNING_KEY_SUFFIX) if not path.exists(): yield path, _fetch_signing_key(fingerprint, url) -def _snapshot(buck: str, target: str, architecture: str) -> str: +def _snapshot(target: str, architecture: str) -> str: """One repository's current pure metadata, for one architecture. Keep the subtarget name in sync with `_snapshot_subtarget` in package/repository.bzl. """ subtarget = f"{target}[snapshot.{architecture}]" print(f"==> snapshotting {_name_of(target)} for {architecture} (via {subtarget})", file=sys.stderr) - return _run(buck, subtarget) + return _run(subtarget) -def _resolve(buck: str, target: str) -> str: +def _resolve(target: str) -> str: """What one box is made of, for the architecture its lock target resolves.""" box, architecture = _box_lock_parts(target) print(f"==> resolving {box} for {architecture} (via {target})", file=sys.stderr) - return _run(buck, target) + return _run(target) def _select_boxes(all_resolves: list[str], selected_boxes: list[str] | None) -> list[str]: @@ -529,59 +514,50 @@ def _select_boxes(all_resolves: list[str], selected_boxes: list[str] | None) -> return [target for target in all_resolves if _box_lock_parts(target)[0] in selected] -def _repositories_for_boxes(buck: str, boxes: list[str]) -> list[str]: +def _repositories_for_boxes(boxes: list[str]) -> list[str]: """Remote repository targets reachable from the given box targets.""" box_set = " ".join(boxes) query = f"attrfilter(labels, '{REMOTE_REPOSITORY_LABEL}', deps(set({box_set})))" - return sorted(buck_output(buck, "uquery", query).split()) + return sorted(buck_output("uquery", query).split()) -def _plan( - buck: str, - catalog: str, - selected_boxes: list[str] | None, -) -> tuple[Path, list[str], list[str]]: +def _plan(catalog: str, selected_boxes: list[str] | None) -> tuple[Path, list[str], list[str]]: """Pick what to refresh. Selecting boxes also scopes the snapshotted repositories to those the boxes depend on, so a partial refresh or verify never touches a repository outside the selection. """ - all_resolves = _targets_with_label(buck, catalog, BOX_LOCK_LABEL) + all_resolves = _targets_with_label(catalog, BOX_LOCK_LABEL) resolves = _select_boxes(all_resolves, selected_boxes) if selected_boxes is None: - snapshots = _targets_with_label(buck, catalog, REMOTE_REPOSITORY_LABEL) + snapshots = _targets_with_label(catalog, REMOTE_REPOSITORY_LABEL) else: - snapshots = _repositories_for_boxes(buck, resolves) + snapshots = _repositories_for_boxes(resolves) targets = all_resolves + snapshots if not targets: fail(f"catalog: no repository/box refresh targets found in {catalog}") - return _catalog_directory(buck, targets), snapshots, resolves + return _catalog_directory(targets), snapshots, resolves -def _regenerate( - buck: str, - catalog_dir: Path, - snapshots: list[str], - resolves: list[str], -) -> Iterator[tuple[Path, str]]: +def _regenerate(catalog_dir: Path, snapshots: list[str], resolves: list[str]) -> Iterator[tuple[Path, str]]: """Fetch keys, snapshot repositories and resolve boxes, yielding each result and where it belongs. Nothing is written here, so a verify regenerates through the same commands a refresh does and still leaves the checkout exactly as it found it. """ - yield from _signing_keys(buck, catalog_dir, snapshots) + yield from _signing_keys(catalog_dir, snapshots) - served = _served_architectures(buck, snapshots) + served = _served_architectures(snapshots) for target in snapshots: for architecture in served[target]: yield ( catalog_dir / _repository_lock_path(target, architecture), - _snapshot(buck, target, architecture), + _snapshot(target, architecture), ) for target in resolves: - yield catalog_dir / _box_lock_path(target), _resolve(buck, target) + yield catalog_dir / _box_lock_path(target), _resolve(target) def _commit(catalog_dir: Path) -> None: @@ -642,7 +618,6 @@ def main(argv: list[str] | None = None) -> None: help="commit the refreshed catalog", ) args = p.parse_args(argv) - buck = nested_buck() if args.commit and args.verify: p.error("--verify leaves the checkout as it found it, so there is nothing to commit") if args.advance and args.verify: @@ -650,15 +625,15 @@ def main(argv: list[str] | None = None) -> None: catalog = _catalog_pattern(args.catalog) # Run nested commands from the project root so wrappers resolve consistently. - with contextlib.chdir(buck_output(buck, "root", "--kind", "project")) as _: - catalog_dir, snapshots, resolves = _plan(buck, catalog, args.box) + with contextlib.chdir(buck_output("root", "--kind", "project")) as _: + catalog_dir, snapshots, resolves = _plan(catalog, args.box) # Lazy: nothing is snapshotted until the loop below asks, after the pins have moved. - regenerated = _regenerate(buck, catalog_dir, snapshots, resolves) + regenerated = _regenerate(catalog_dir, snapshots, resolves) if not args.verify: checkout = _Checkout() try: if args.advance: - _advance_snapshots(checkout, buck, catalog, catalog_dir, snapshots) + _advance_snapshots(checkout, catalog, catalog_dir, snapshots) for path, content in regenerated: checkout.write(path, content) except BaseException: diff --git a/tools/catalog_test.py b/tools/catalog_test.py index 7cfd0a1e..f3007f08 100644 --- a/tools/catalog_test.py +++ b/tools/catalog_test.py @@ -160,7 +160,7 @@ def setUp(self) -> None: patched = mock.patch.object( catalog, "_pinned_snapshot", - side_effect=lambda buck, pin, architecture: SERVED[(pin.target, architecture)], + side_effect=lambda pin, architecture: SERVED[(pin.target, architecture)], ) patched.start() self.addCleanup(patched.stop) @@ -174,7 +174,7 @@ def enumerating( def test_moves_the_datestamp_and_keeps_the_placeholder(self) -> None: with self.enumerating("t9-x86_64-rolling-20260202"): newest = catalog._newest_rpmrepo_snapshot( - "buck", [pin("t9-$basearch-rolling-20260101", x86_64="t9-x86_64-rolling-20260101")] + [pin("t9-$basearch-rolling-20260101", x86_64="t9-x86_64-rolling-20260101")] ) self.assertEqual(newest, "t9-$basearch-rolling-20260202") @@ -182,7 +182,7 @@ def test_enumerates_the_series_of_every_architecture(self) -> None: with self.enumerating( "t9-aarch64-rolling-20260202", "t9-x86_64-rolling-20260202" ) as newest_snapshot: - catalog._newest_rpmrepo_snapshot("buck", [two_architectures("t9-$basearch-rolling-20260101")]) + catalog._newest_rpmrepo_snapshot([two_architectures("t9-$basearch-rolling-20260101")]) self.assertEqual( [call.args[2] for call in newest_snapshot.call_args_list], ["t9-aarch64-rolling", "t9-x86_64-rolling"], @@ -191,9 +191,7 @@ def test_enumerates_the_series_of_every_architecture(self) -> None: def test_refuses_architectures_on_different_days(self) -> None: with self.enumerating("t9-aarch64-rolling-20260201", "t9-x86_64-rolling-20260202"): with self.assertRaises(SystemExit) as raised: - catalog._newest_rpmrepo_snapshot( - "buck", [two_architectures("t9-$basearch-rolling-20260101")] - ) + catalog._newest_rpmrepo_snapshot([two_architectures("t9-$basearch-rolling-20260101")]) self.assertIn("one pin cannot advance to several snapshots", str(raised.exception)) self.assertIn("(arm64) offers t9-aarch64-rolling-20260201", str(raised.exception)) @@ -202,11 +200,10 @@ def test_refuses_repositories_on_different_days(self) -> None: with self.enumerating("t9-x86_64-rolling-20260201", "t9-x86_64-rolling-20260202"): with self.assertRaises(SystemExit) as raised: catalog._newest_rpmrepo_snapshot( - "buck", [ pin(shared, name="test.core", x86_64="t9-x86_64-rolling-20260101"), pin(shared, name="test.extra", x86_64="t9-x86_64-rolling-20260101"), - ], + ] ) self.assertIn("one pin cannot advance to several snapshots", str(raised.exception)) diff --git a/tools/dev.py b/tools/dev.py index a16c1222..45771d82 100644 --- a/tools/dev.py +++ b/tools/dev.py @@ -10,37 +10,37 @@ import subprocess import sys import tomllib -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from pathlib import Path from typing import Any, cast -from util import buck_output, fail, nested_buck, write_if_changed +from util import buck2_argv, buck_output, fail, write_if_changed def _bold(label: str) -> None: print(f"\033[1m{label}\033[0m", flush=True) -def _run(cmd: list[str | Path], *, stderr: int | None = None) -> None: +def _run(cmd: Sequence[str | Path], *, stderr: int | None = None) -> None: # Tools print their own diagnostics; propagate failures without a traceback. proc = subprocess.run(cmd, stderr=stderr) if proc.returncode != 0: raise SystemExit(proc.returncode) -def _cell_root(buck: str, cell: str) -> Path: - return Path(buck_output(buck, "audit", "cell", cell, "--paths-only")) +def _cell_root(cell: str) -> Path: + return Path(buck_output("audit", "cell", cell, "--paths-only")) -def _starlark_srcs(buck: str) -> list[Path]: +def _starlark_srcs() -> list[Path]: # Check every loaded in-tree Starlark file; ignore dead files, external cells, and JSON. - cells = cast(dict[str, str], json.loads(buck_output(buck, "audit", "cell", "--json"))) - aliases = cast(dict[str, str], json.loads(buck_output(buck, "audit", "cell", "--json", "--aliases"))) + cells = cast(dict[str, str], json.loads(buck_output("audit", "cell", "--json"))) + aliases = cast(dict[str, str], json.loads(buck_output("audit", "cell", "--json", "--aliases"))) roots = {path: name for name, path in sorted(cells.items()) if name not in ("none", "prelude")} universe = " + ".join(sorted(f"{name}//..." for name in roots.values())) - project = Path(buck_output(buck, "root", "--kind", "project")) + project = Path(buck_output("root", "--kind", "project")) files = sorted( - project / f for f in buck_output(buck, "uquery", f"allbuildfiles({universe})").splitlines() if f + project / f for f in buck_output("uquery", f"allbuildfiles({universe})").splitlines() if f ) # Keep the tine cell's own files, plus those of any cell nested inside it. Reject by owning cell # rather than by path prefix, because nested `none`/`prelude` are not on disk, so a prefix test @@ -70,14 +70,14 @@ def owner(path: Path) -> str | None: ) -def _orphan_tests(buck: str, cell: Path) -> list[Path]: +def _orphan_tests(cell: Path) -> list[Path]: """Test files no box_python_test lists in `srcs`, which `buck test` would never run. Each suite names its sources explicitly, so a new file beside the code is invisible until it is added to one; nothing else would report that. """ targets = json.loads( - buck_output(buck, "uquery", "kind('box_python_test', tine//...)", "--output-attribute", "srcs") + buck_output("uquery", "kind('box_python_test', tine//...)", "--output-attribute", "srcs") ) # A label in the root package renders as `cell///file`, which would resolve to an absolute path. claimed = { @@ -102,7 +102,7 @@ def _universe() -> str: ) -def _targets(buck: str) -> list[str]: +def _targets() -> list[str]: """What `_universe()` matches, to hand to a command that takes no query. Named explicitly, a target incompatible by design (an image base without a chosen distribution, a @@ -110,7 +110,7 @@ def _targets(buck: str) -> list[str]: needs `--skip-incompatible-targets`. That does not hide an Arch target the query missed: on a host without Arch, that fails configuration rather than being incompatible. """ - return buck_output(buck, "uquery", _universe()).split() + return buck_output("uquery", _universe()).split() def _starlark_fmt(args: argparse.Namespace, *arguments: str | Path) -> list[str | Path]: @@ -127,11 +127,10 @@ def _fmt_diff(args: argparse.Namespace, src: Path) -> str: def _lint(args: argparse.Namespace) -> None: - buck = nested_buck() - cell = _cell_root(buck, "tine") - srcs = _starlark_srcs(buck) + cell = _cell_root("tine") + srcs = _starlark_srcs() _bold("test targets") - if orphans := _orphan_tests(buck, cell): + if orphans := _orphan_tests(cell): listing = "\n".join(f" {p.relative_to(cell)}" for p in orphans) fail(f"no box_python_test lists these, so they never run:\n{listing}") _bold("ruff") @@ -140,13 +139,13 @@ def _lint(args: argparse.Namespace) -> None: _bold("reuse") # Scoped by --root rather than by the cwd Buck runs this in, so a consuming project checks the # cell it declares and not whatever else sits beside it. - _run([buck, "-v", "0", "run", "tine//tools:reuse", "--", "--root", cell, "lint", "--lines"]) + _run([*buck2_argv("-v", "0", "run", "tine//tools:reuse"), "--", "--root", cell, "lint", "--lines"]) _bold("ty") - targets = buck_output(buck, "uquery", f"attrfilter(labels, 'python-typecheck', {_universe()})").split() + targets = buck_output("uquery", f"attrfilter(labels, 'python-typecheck', {_universe()})").split() if not targets: fail("ty: no generated type-check targets found") _require_manifests(cell, _ty_packages(targets)) - _run([buck, "build", *targets]) + _run(buck2_argv("build", *targets)) _bold("starlark_fmt") # starlark_fmt has no check mode, so diff each file and fail on the first rewrite it would make. if diffs := [diff for src in srcs if (diff := _fmt_diff(args, src))]: @@ -155,21 +154,20 @@ def _lint(args: argparse.Namespace) -> None: # Pass the files rather than the cell directory: standalone, the tine cell is the project root, # which Buck normalizes to an empty path and rejects. _bold("starlark lint") - _run([buck, "-v", "0", "starlark", "lint", "--console", "none", *srcs]) + _run([*buck2_argv("-v", "0", "starlark", "lint", "--console", "none"), *srcs]) _bold("starlark typecheck") # Typecheck errors use stdout; stderr is only the per-file event log. Unlike lint, typecheck # follows load() into data files and parses them as Starlark, which no TOML survives (a JSON # object happens to be a valid Starlark expression), so files with a TOML load stay out; the # probe also matches the `?format=toml` spelling. checkable = [f for f in srcs if 'toml"' not in f.read_text(encoding="utf-8")] - _run([buck, "-v", "0", "starlark", "typecheck", *checkable], stderr=subprocess.DEVNULL) + _run([*buck2_argv("-v", "0", "starlark", "typecheck"), *checkable], stderr=subprocess.DEVNULL) _bold("target graph") # Analysis, not a build: it reaches every rule a build would run, without producing anything. # Scoped to this cell, whose platform the parser knows how to detect; what a consuming project # declares is its own to check, with its own pattern. _run( - [ - buck, + buck2_argv( "-v", "0", "bxl", @@ -179,17 +177,16 @@ def _lint(args: argparse.Namespace) -> None: "--", "--pattern", _universe(), - ] + ) ) def _check(args: argparse.Namespace) -> None: - buck = nested_buck() _lint(args) _bold("unit tests") # Building an example image is minutes where these are seconds. Everything that needs one is # labelled `image` and covered by `buck test tine//... --include image`, which is what CI runs. - _run([buck, "test", "--skip-incompatible-targets", *_targets(buck), "--exclude", "image"]) + _run(buck2_argv("test", "--skip-incompatible-targets", *_targets(), "--exclude", "image")) def _print_universe(_args: argparse.Namespace) -> None: @@ -197,14 +194,13 @@ def _print_universe(_args: argparse.Namespace) -> None: def _fmt(args: argparse.Namespace) -> None: - buck = nested_buck() # Ask Buck everything before the formatters touch the tree. This runs under `buck run`, whose # command stays active for as long as the binary does, and buck2 only recognizes a nested # command as nested when it spawned the process itself, which it does for actions but not for # run targets. A query issued after a write therefore needs a newer state than the command it # is nested in and waits for it to finish: a deadlock rather than an error. - cell = _cell_root(buck, "tine") - srcs = _starlark_srcs(buck) + cell = _cell_root("tine") + srcs = _starlark_srcs() _bold("ruff") _run([args.ruff, "format", "--no-cache", cell]) _run([args.ruff, "check", "--fix", "--no-cache", cell]) @@ -221,9 +217,8 @@ def _write_dot(path: Path, intra: dict[str, list[str]], rev: dict[str, list[str] def _scc(args: argparse.Namespace) -> None: - buck = nested_buck() # Graph derivation and cycle detection live in buck (rpm_branch); this only formats its output. - graph = buck_output(buck, "build", f"{args.branch}:_buildrequires_graph", "--out", "-") + graph = buck_output("build", f"{args.branch}:_buildrequires_graph", "--out", "-") data = cast(dict[str, Any], json.loads(graph)) edge_caps = cast(dict[str, dict[str, list[str]]], data["edges"]) components: dict[int, set[str]] = {} @@ -376,7 +371,7 @@ def _ty_zed_files(cell: Path, targets: dict[str, dict[str, list[str]]]) -> None: write_if_changed(settings, marker + json.dumps(configuration, indent=2) + "\n") -def _ty_environment(buck: str, cell: Path, package: Path, python: Path) -> dict[str, object]: +def _ty_environment(cell: Path, package: Path, python: Path) -> dict[str, object]: """Select pinned Python or the package's explicitly requested box-provided imports.""" manifest = cell / package / "pyproject.toml" config = tomllib.loads(manifest.read_text(encoding="utf-8")) if manifest.is_file() else {} @@ -386,7 +381,7 @@ def _ty_environment(buck: str, cell: Path, package: Path, python: Path) -> dict[ fail(f"{manifest}: tool.tine.ty.box must name a Buck box target") # Tests use boxes even for stdlib-only sources, so their runtime is not an editor requirement. # Build the explicitly chosen box only for the package whose server is starting. - outputs = buck_output(buck, "build", "--show-full-simple-output", box).splitlines() + outputs = buck_output("build", "--show-full-simple-output", box).splitlines() if len(outputs) != 1: fail(f"{manifest}: expected one box output for {box}, got {outputs}") python = Path(outputs[0]) / "usr" @@ -395,14 +390,12 @@ def _ty_environment(buck: str, cell: Path, package: Path, python: Path) -> dict[ def _ty(args: argparse.Namespace) -> None: - buck = nested_buck() package = Path(args.package) if package.is_absolute() or ".." in package.parts: fail("--package must be a directory relative to the tine cell") - cell = _cell_root(buck, "tine") + cell = _cell_root("tine") targets = json.loads( buck_output( - buck, "uquery", "attrfilter(labels, 'python-typecheck', tine//...)" " + kind('python_bootstrap_library', tine//...)", @@ -412,7 +405,7 @@ def _ty(args: argparse.Namespace) -> None: ) roots = _ty_import_roots(cell, package, targets) # Finish nested Buck commands before writing files, as in _fmt. - environment = _ty_environment(buck, cell, package, Path(args.python)) + environment = _ty_environment(cell, package, Path(args.python)) _ty_zed_files(cell, targets) manifest = cell / package / "pyproject.toml" config = _read_ty_config(cell / "ty.toml", manifest if manifest.is_file() else None) diff --git a/tools/dev_test.py b/tools/dev_test.py index 25219b0d..6f947dcb 100644 --- a/tools/dev_test.py +++ b/tools/dev_test.py @@ -130,14 +130,12 @@ def setUp(self) -> None: def launch(self, package: str) -> tuple[list[str], dict[str, str]]: args = argparse.Namespace( package=package, - buck="buck", python=os.environ["TY_TEST_PYTHON"], ty=os.environ["TY_TEST_BINARY"], arguments=["server"], ) - def buck_output(buck: str, command: str, *arguments: str) -> str: - self.assertEqual(buck, "buck") + def buck_output(command: str, *arguments: str) -> str: if command == "uquery": return json.dumps(self.targets) self.assertEqual( @@ -163,18 +161,16 @@ def test_builds_a_box_only_for_opted_in_packages(self) -> None: for package in (".", "cargo", "go"): with self.subTest(package=package): self.assertEqual( - dev._ty_environment("buck", self.root, Path(package), python), + dev._ty_environment(self.root, Path(package), python), {"python": str(python)}, ) buck.assert_not_called() (self.root / "go/pyproject.toml").write_text( '[tool.tine.ty]\nbox = "tine//catalog:fedora.rawhide.box"\n' ) - self.assertEqual( - dev._ty_environment("buck", self.root, Path("go"), python), {"python": "/box/usr"} - ) + self.assertEqual(dev._ty_environment(self.root, Path("go"), python), {"python": "/box/usr"}) buck.assert_called_once_with( - "buck", "build", "--show-full-simple-output", "tine//catalog:fedora.rawhide.box" + "build", "--show-full-simple-output", "tine//catalog:fedora.rawhide.box" ) def test_rejects_invalid_packages(self) -> None: @@ -306,6 +302,7 @@ def test_lint_rejects_a_package_without_a_manifest(self) -> None: (self.root / "cargo/pyproject.toml").unlink() labels = "\n".join(label for label in self.targets if label.endswith("-ty")) with ( + mock.patch.dict(os.environ, {"BUCK2_BINARY": "buck2"}), mock.patch.object(dev, "_cell_root", return_value=self.root), mock.patch.object(dev, "_starlark_srcs", return_value=[]), mock.patch.object(dev, "_orphan_tests", return_value=[]), @@ -313,7 +310,7 @@ def test_lint_rejects_a_package_without_a_manifest(self) -> None: mock.patch.object(dev, "buck_output", return_value=labels), self.assertRaisesRegex(SystemExit, "cargo/pyproject.toml"), ): - dev._lint(argparse.Namespace(buck="buck", ruff="ruff")) + dev._lint(argparse.Namespace(ruff="ruff")) def test_refuses_to_overwrite_custom_zed_settings(self) -> None: settings = self.root / "cargo/.zed/settings.json" diff --git a/util.py b/util.py index 984346b4..9dad4614 100644 --- a/util.py +++ b/util.py @@ -308,30 +308,30 @@ def write_if_changed(path: Path, text: str) -> None: fail(f"cannot write {path}: {error}") -def nested_buck() -> str: - """The Buck a tool running under Buck has to nest. +def buck2_argv(*args: str) -> list[str]: + """Build an argv for invoking our pinned Buck2. - `tine` exports the Buck2 it resolved, so a nested command runs that one and not the wrapper: - refreshing configuration under a command already holding it deadlocks. + `tine` exports the Buck2 it resolved as `BUCK2_BINARY`. Running a driver outside tine is + unsupported, so the variable is hard requirement. """ - return os.environ.get("BUCK2_BINARY", "buck") + return [os.environ["BUCK2_BINARY"], *args] -def buck_output(buck: str, *args: str) -> str: +def buck_output(*args: str) -> str: """One nested Buck command, its stdout stripped.""" # `-v 0` mutes Buck's own chatter on success return subprocess.run( - [buck, "-v", "0", *args], check=True, stdout=subprocess.PIPE, text=True + buck2_argv("-v", "0", *args), check=True, stdout=subprocess.PIPE, text=True ).stdout.strip() -def package_directory(buck: str, package: str) -> Path: +def package_directory(package: str) -> Path: """Where a `cell//path` package label lives on disk, asked of Buck rather than assumed.""" cell, separator, path = package.partition("//") if not separator or not cell or ":" in package or "..." in package: fail(f"expected a cell-relative package label, got {package!r}") - return Path(buck_output(buck, "audit", "cell", cell, "--paths-only")) / path + return Path(buck_output("audit", "cell", cell, "--paths-only")) / path def _record_paths(directory: Path, pathspec: str, how: list[str], message: str | None) -> bool: From 826af4cde18c4486145787ab54e498862b14981a Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Sat, 10 Oct 2026 17:53:20 +0000 Subject: [PATCH 4/6] tine: drop nested tine call support The `BUCK2_BINARY` variable previously had two different jobs: Tell tools running under a `build` where the binary lives, but also as a flag whether it was already running under a parent `buck2`. In that case, `tine` skipped re-reading mount declarations and refreshing shared configuration. But nothing actually uses this: tools under a build call `buck2` directly. The only special case is the `cache_roundtrip` test, but it scrubs the environment to act as a developer's top-level call. Treat every wrapper invocation as top-level. This clarifies what `BUCK2_BINARY` is about, and opens the door for further simplifications. Signed-off-by: Martin Pitt --- bin/tine.py | 9 ++++--- bin/tine_test.py | 47 ++++++++----------------------------- docs/design/architecture.md | 6 ++--- 3 files changed, 17 insertions(+), 45 deletions(-) diff --git a/bin/tine.py b/bin/tine.py index 6604d0d3..6f7a37a3 100644 --- a/bin/tine.py +++ b/bin/tine.py @@ -1748,11 +1748,10 @@ def prepare_buck( ) -> tuple[dict[str, dict[str, str]], list[str]]: """Select mounts and the wrapper before refreshing defaults from its checkout.""" mounts = namespace_mount_targets(root) - nested = bool(os.environ.get("BUCK2_BINARY")) overrides = buckconfig_overrides(settings, root / CONFIG) - declared = declared_mounts(root) if mounts is None and not nested else {} + declared = declared_mounts(root) if mounts is None else {} reexec_configured_wrapper(root, read_project_buckconfig(root, overrides), declared, argv) - if refresh_config and not nested: + if refresh_config: refresh_project_buckconfig(root, overrides) config = read_project_buckconfig(root) if mounts: @@ -1818,7 +1817,7 @@ def buck_command(argv: list[str]) -> None: binary = buck2_binary(settings, wrapper_cell_root(), fetch=command.subcommand != "complete") if binary is None: return - if command.subcommand != "complete" and not os.environ.get("BUCK2_BINARY"): + if command.subcommand != "complete": # The daemon reads the cache address only at startup, so a changed one has to replace it. # Not `daemon_buster`: Buck takes startup constraints from the root `.buckconfig` without # following includes, and the mount digest owns that slot. @@ -1833,7 +1832,7 @@ def buck_command(argv: list[str]) -> None: checkout, collect_project_ignores(checkout, read_project_buckconfig(checkout), [], gitdirs), ) - # BUCK2_BINARY lets a tool nest a Buck2 command without refreshing config under its build. + # BUCK2_BINARY is the resolved Buck2 a tool nests through util.buck2_argv(). environment = {"BUCK2_ARG0": "tine buck", "BUCK2_BINARY": str(binary)} try: os.execve(binary, [str(binary), *argv], os.environ | environment) diff --git a/bin/tine_test.py b/bin/tine_test.py index 5f81358a..7dac26f3 100644 --- a/bin/tine_test.py +++ b/bin/tine_test.py @@ -1044,12 +1044,6 @@ def test_removed_mount_is_revalidated_after_waiting_for_lock(self) -> None: self.assertIn("mount sub: not a valid target", stderr) self.assertEqual(self.declared(), {}) - def test_builds_can_update_mount_declarations(self) -> None: - # Builds hold no lock, so their nested mount commands must remain allowed. - with unittest.mock.patch.dict(os.environ, {"BUCK2_BINARY": "buck2"}): - self.mount("add", str(self.root / "sub"), str(self.source)) - self.assertEqual(self.declared(), {"sub": str(self.source)}) - def test_lock_parent_creation_error_is_reported(self) -> None: (self.root / tine.HOME).touch() with self.assertRaisesRegex(SystemExit, "cannot prepare .* for locking"): @@ -1560,7 +1554,6 @@ def setUp(self) -> None: self.addCleanup(created.stop) patched = unittest.mock.patch.dict(os.environ) patched.start() - os.environ.pop("BUCK2_BINARY", None) os.environ.pop(tine.MARKER, None) self.addCleanup(patched.stop) @@ -1623,25 +1616,18 @@ def test_same_wrapper_and_its_symlinks_need_no_handover(self) -> None: tine.prepare_buck(self.root, {}, ["buck", "build"], refresh_config=False) self.assertEqual((self.made, execve), ([], [])) - def test_nested_and_mounted_invocations_still_select_the_wrapper(self) -> None: + def test_mounted_invocations_still_select_the_wrapper(self) -> None: command = wrapper_command(self.root / "sub") (self.root / ".buckconfig").write_text("[cells]\nroot = .\ntine = sub\n") - for environment, targets in ( - ({"BUCK2_BINARY": "/somewhere/buck2"}, None), - ({tine.MARKER: tine.mount_namespace_marker("a" * 16)}, ["sub"]), + with ( + unittest.mock.patch.dict(os.environ, {tine.MARKER: tine.mount_namespace_marker("a" * 16)}), + unittest.mock.patch.object(tine, "namespace_mount_targets", return_value=["sub"]), + unittest.mock.patch.object(tine, "declared_mounts", side_effect=AssertionError("reread mounts")), + unittest.mock.patch.object(os, "execve", side_effect=SystemExit("handover")) as execve, ): - with ( - self.subTest(environment=environment), - unittest.mock.patch.dict(os.environ, environment), - unittest.mock.patch.object(tine, "namespace_mount_targets", return_value=targets), - unittest.mock.patch.object( - tine, "declared_mounts", side_effect=AssertionError("reread mounts") - ), - unittest.mock.patch.object(os, "execve", side_effect=SystemExit("handover")) as execve, - ): - with self.assertRaisesRegex(SystemExit, "handover"): - self.prepare() - execve.assert_called_once_with(command, [str(command), "buck", "build"], dict(os.environ)) + with self.assertRaisesRegex(SystemExit, "handover"): + self.prepare() + execve.assert_called_once_with(command, [str(command), "buck", "build"], dict(os.environ)) self.assertEqual(self.made, []) def test_completion_uses_the_configured_wrapper(self) -> None: @@ -1688,16 +1674,6 @@ def test_failed_handover_names_the_selected_wrapper(self) -> None: ): self.prepare() - def test_buck_child_process_is_already_inside(self) -> None: - self.declare_one() - with ( - unittest.mock.patch.dict(os.environ, {"BUCK2_BINARY": "/somewhere/buck2"}), - unittest.mock.patch.object(tine, "declared_mounts", side_effect=AssertionError("reread mounts")), - self.running() as execve, - ): - self.prepare() - self.assertEqual((self.made, execve), ([], [])) - def test_matching_current_namespace_needs_no_handover(self) -> None: digest = self.declare_one() with ( @@ -2456,7 +2432,7 @@ def test_toml_project_ignores_are_merged_with_generated_ignores(self) -> None: tine.collect_project_ignores(self.root, self.config(), [], {}), ["custom", *tine.VCS_IGNORES] ) - def test_completion_and_nested_builds_do_not_refresh_defaults(self) -> None: + def test_completion_does_not_refresh_defaults(self) -> None: before = self.path.read_bytes() self.source.write_text(self.updated) with ( @@ -2466,9 +2442,6 @@ def test_completion_and_nested_builds_do_not_refresh_defaults(self) -> None: ): tine.buck_command(["complete"]) self.assertEqual(self.path.read_bytes(), before) - with unittest.mock.patch.dict(os.environ, {"BUCK2_BINARY": "active"}): - tine.buck_command(["build", "tine//..."]) - self.assertEqual(self.path.read_bytes(), before) self.assertFalse((self.root / tine.LOCAL).exists()) def test_completion_script_does_not_refresh_defaults(self) -> None: diff --git a/docs/design/architecture.md b/docs/design/architecture.md index 3e7976d8..5e282b70 100644 --- a/docs/design/architecture.md +++ b/docs/design/architecture.md @@ -242,9 +242,9 @@ The selected tine checkout pins Buck2 in `tine.lock.json`. Projects can override `[buck2]` table of `tine.toml` or `tine.local.toml`, with per-platform fields under `[buck2.platforms.]`. -tine exports the pinned binary's path as `BUCK2_BINARY`. A nested Buck command still selects the -configured wrapper, but inherits the current mounts and skips refreshing shared configuration -underneath the build that started it. +tine exports the pinned binary's path as `BUCK2_BINARY`. A tool running under a build never invokes the +`tine` wrapper, but directly calls the exported binary (`util.buck2_argv()`), inherits the current +mounts, and runs against the shared configuration the invoking command already wrote. ### Component model From 66ccccbeaa74466da7ab322411851dffa54ee83b Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Sat, 10 Oct 2026 18:41:21 +0000 Subject: [PATCH 5/6] tine: build the wrapper's buck2 commands through buck2_argv() Use `util.buck2_argv()` for all of tine.py's top-level buck commands as well. Rename `buck2_binary()` to `ensure_buck2_binary()` and let that export the environment. It only reports success, so that nothing can break the "go via `buck2_argv()`" contract any more. Signed-off-by: Martin Pitt --- bin/tine.py | 55 ++++++++++++++++++++++------------------ bin/tine_test.py | 66 ++++++++++++++++++++++++++++-------------------- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/bin/tine.py b/bin/tine.py index 6f7a37a3..d793b081 100644 --- a/bin/tine.py +++ b/bin/tine.py @@ -20,7 +20,7 @@ from typing import cast import cache_shim -from util import cache_home, fail, object_table, resolved, write_if_changed +from util import buck2_argv, cache_home, fail, object_table, resolved, write_if_changed import isolation @@ -802,12 +802,22 @@ def _pinned_binary( return _download_binary(url, sha256, binary, compressed=compressed) -def buck2_binary(config: Mapping[str, object], cell: Path, *, fetch: bool = True) -> Path | None: - """The pinned Buck2 binary. The pin is this cell's, overridable key by key in `[buck2]`.""" +def ensure_buck2_binary(config: Mapping[str, object], cell: Path, *, fetch: bool = True) -> bool: + """Resolve and export the pinned Buck2. The pin is this cell's, overridable key by key in `[buck2]`. + + Call this once at the beginning, then all Buck2 invocations go via util.buck2_argv(). + Without `fetch`, an uncached pin does not download and returns False. + """ platform = _host_platform() pin = declared_pin(cell, BUCK2, platform) | buck2_pin_overrides(config, platform) source = f"{cell / PINS} and [{BUCK2}] in {SETTINGS}" - return _pinned_binary(pin, BUCK2, platform, source, compressed=True, fetch=fetch) + binary = _pinned_binary(pin, BUCK2, platform, source, compressed=True, fetch=fetch) + if binary is None: + return False + os.environ["BUCK2_BINARY"] = str(binary) + # buck2 reports BUCK2_ARG0 as its own name, so messages match how the user invoked it. + os.environ["BUCK2_ARG0"] = "tine buck" + return True def mount_namespace_marker(digest: str) -> str: @@ -1191,13 +1201,14 @@ def _replace_completion_fragment(script: str, old: str, new: str) -> str: return script.replace(old, new) -def _buck2_completion_script(binary: Path, shell: str) -> str: +def _buck2_completion_script(shell: str) -> str: import subprocess + command = buck2_argv("completion", shell) try: - proc = subprocess.run([binary, "completion", shell], stdout=subprocess.PIPE, text=True) + proc = subprocess.run(command, stdout=subprocess.PIPE, text=True) except OSError as error: - fail(f"cannot run {binary}: {error}") + fail(f"cannot run {command[0]}: {error}") if proc.returncode != 0: raise SystemExit(proc.returncode) return proc.stdout @@ -1759,14 +1770,12 @@ def prepare_buck( return config, mounts or [] -def kill_daemon(binary: Path, isolation: str | None) -> None: +def kill_daemon(isolation: str | None) -> None: import subprocess - command = [str(binary)] - if isolation: - command += [FLAG_ISOLATION, isolation] print("tine: replacing the Buck daemon, which reads the cache address only at startup", file=sys.stderr) - proc = subprocess.run([*command, "kill"], capture_output=True, text=True) + command = buck2_argv(*([FLAG_ISOLATION, isolation] if isolation else []), "kill") + proc = subprocess.run(command, capture_output=True, text=True) if proc.returncode != 0: fail(f"cannot replace the Buck daemon: {(proc.stderr or proc.stdout).strip()}") @@ -1784,10 +1793,9 @@ def before_shim( where it is: the build that produces the shim is told on its command line not to use the cache, rather than by taking the address away from a daemon and killing it for a shim start. """ - binary = buck2_binary(settings, wrapper_cell_root()) - assert binary is not None + ensure_buck2_binary(settings, wrapper_cell_root()) refresh_local_buckconfig(root, collect_project_ignores(root, config, [], {}), cache) - return [str(binary), *([FLAG_ISOLATION, isolation] if isolation else [])] + return buck2_argv(*([FLAG_ISOLATION, isolation] if isolation else [])) def buck_command(argv: list[str]) -> None: @@ -1814,15 +1822,14 @@ def buck_command(argv: list[str]) -> None: root, settings, ["buck", *argv], refresh_config=command.subcommand != "complete" ) # Hand over first so wrapper_cell_root() reads the configured checkout's pin. - binary = buck2_binary(settings, wrapper_cell_root(), fetch=command.subcommand != "complete") - if binary is None: + if not ensure_buck2_binary(settings, wrapper_cell_root(), fetch=command.subcommand != "complete"): return if command.subcommand != "complete": # The daemon reads the cache address only at startup, so a changed one has to replace it. # Not `daemon_buster`: Buck takes startup constraints from the root `.buckconfig` without # following includes, and the mount digest owns that slot. if published != cache_client(cache): - kill_daemon(binary, command.isolation) + kill_daemon(command.isolation) gitdirs = namespace_gitdirs(root) if mounts else {} refresh_local_buckconfig(root, collect_project_ignores(root, config, mounts, gitdirs), cache) if (cell := cells_of(config).get(CELL)) is not None and (checkout := root / cell) != root: @@ -1832,12 +1839,11 @@ def buck_command(argv: list[str]) -> None: checkout, collect_project_ignores(checkout, read_project_buckconfig(checkout), [], gitdirs), ) - # BUCK2_BINARY is the resolved Buck2 a tool nests through util.buck2_argv(). - environment = {"BUCK2_ARG0": "tine buck", "BUCK2_BINARY": str(binary)} + buck = buck2_argv(*argv) try: - os.execve(binary, [str(binary), *argv], os.environ | environment) + os.execv(buck[0], buck) except OSError as error: - fail(f"cannot run {binary}: {error}") + fail(f"cannot run {buck[0]}: {error}") def completion_command(arguments: list[str]) -> None: @@ -1853,10 +1859,9 @@ def completion_command(arguments: list[str]) -> None: settings = project_settings(root) prepare_buck(root, settings, ["completion", *arguments], refresh_config=False) configured = validate_commands(settings.get(COMMANDS, {}), str(root / CONFIG)) - binary = buck2_binary(settings, wrapper_cell_root()) - assert binary is not None + ensure_buck2_binary(settings, wrapper_cell_root()) print( - rewrite_completion_script(_buck2_completion_script(binary, arguments[0]), arguments[0], configured), + rewrite_completion_script(_buck2_completion_script(arguments[0]), arguments[0], configured), end="", ) diff --git a/bin/tine_test.py b/bin/tine_test.py index 7dac26f3..b5e2d6ae 100644 --- a/bin/tine_test.py +++ b/bin/tine_test.py @@ -1457,13 +1457,19 @@ def mounted() -> str: digest = tine.create_mount_namespace(self.root, {"sub": str(source)}) with ( contextlib.chdir(self.root), - unittest.mock.patch.dict(os.environ, {tine.MARKER: tine.mount_namespace_marker(digest)}), + unittest.mock.patch.dict( + os.environ, + { + tine.MARKER: tine.mount_namespace_marker(digest), + "BUCK2_BINARY": str(self.root / "buck2"), + }, + ), unittest.mock.patch.object(tine, "COMMAND_PATH", self.root / "sub" / tine.COMMAND), - unittest.mock.patch.object(tine, "buck2_binary", return_value=self.root / "buck2"), + unittest.mock.patch.object(tine, "ensure_buck2_binary", return_value=True), unittest.mock.patch.object( tine, "namespace_gitdirs", wraps=tine.namespace_gitdirs ) as gitdirs, - unittest.mock.patch.object(os, "execve") as execve, + unittest.mock.patch.object(os, "execv") as execve, ): tine.buck_command(["build", "tine//..."]) execve.assert_called_once() @@ -1586,7 +1592,9 @@ def test_unmounted_handover_precedes_reading_defaults_and_pins(self) -> None: with ( contextlib.chdir(self.root), unittest.mock.patch.object(os, "execve", side_effect=SystemExit("handover")) as execve, - unittest.mock.patch.object(tine, "buck2_binary", side_effect=AssertionError("read outer pin")), + unittest.mock.patch.object( + tine, "ensure_buck2_binary", side_effect=AssertionError("read outer pin") + ), self.assertRaisesRegex(SystemExit, "handover"), ): tine.main(arguments) @@ -1744,18 +1752,20 @@ def cell(self, spec: object | None = None) -> Path: def test_the_cell_declares_the_pin(self) -> None: home = self.cache("a" * 64) + binary = home / "tine" / "buck2" / ("a" * 64) / "buck2" with unittest.mock.patch.dict(os.environ, {"XDG_CACHE_HOME": str(home)}): - self.assertEqual( - tine.buck2_binary({}, self.cell()), home / "tine" / "buck2" / ("a" * 64) / "buck2" - ) + self.assertTrue(tine.ensure_buck2_binary({}, self.cell())) + self.assertEqual(os.environ["BUCK2_BINARY"], str(binary)) + self.assertEqual(os.environ["BUCK2_ARG0"], "tine buck") def test_a_project_pin_overrides_the_cell_key_by_key(self) -> None: platform = tine._host_platform() home = self.cache("b" * 64) config = {"buck2": {"platforms": {platform: {"sha256": "b" * 64}}}} with unittest.mock.patch.dict(os.environ, {"XDG_CACHE_HOME": str(home)}): - binary = tine.buck2_binary(config, self.cell()) - self.assertEqual(binary, home / "tine" / "buck2" / ("b" * 64) / "buck2") + self.assertTrue(tine.ensure_buck2_binary(config, self.cell())) + binary = os.environ["BUCK2_BINARY"] + self.assertEqual(binary, str(home / "tine" / "buck2" / ("b" * 64) / "buck2")) def test_the_override_is_read_from_tine_toml(self) -> None: platform = tine._host_platform() @@ -1806,25 +1816,25 @@ def test_every_platform_hash_is_validated(self) -> None: def test_an_uncached_pin_is_nothing_to_complete_with(self) -> None: with unittest.mock.patch.dict(os.environ, {"XDG_CACHE_HOME": str(scratch(self))}): - self.assertIsNone(tine.buck2_binary(self.pin(), self.cell(), fetch=False)) + self.assertFalse(tine.ensure_buck2_binary(self.pin(), self.cell(), fetch=False)) def test_a_cell_declaring_no_buck2(self) -> None: with self.assertRaisesRegex(SystemExit, "declares no buck2"): - tine.buck2_binary({}, self.cell({"ruff": {}})) + tine.ensure_buck2_binary({}, self.cell({"ruff": {}})) def test_a_cell_with_no_pins(self) -> None: with self.assertRaisesRegex(SystemExit, "cannot read .*tine.lock.json"): - tine.buck2_binary({}, scratch(self, "tine-test-cell.")) + tine.ensure_buck2_binary({}, scratch(self, "tine-test-cell.")) def test_a_machine_buck2_is_not_published_for(self) -> None: uname = os.uname_result(("Linux", "host", "7.0", "#1", "m68k")) with unittest.mock.patch.object(os, "uname", return_value=uname): with self.assertRaisesRegex(SystemExit, "unsupported platform: Linux-m68k"): - tine.buck2_binary(self.pin(), scratch(self, "tine-test-cell.")) + tine.ensure_buck2_binary(self.pin(), scratch(self, "tine-test-cell.")) def test_a_pin_that_is_not_a_sha256(self) -> None: with self.assertRaisesRegex(SystemExit, "64 lowercase hexadecimal"): - tine.buck2_binary(self.pin("abc"), self.cell()) + tine.ensure_buck2_binary(self.pin("abc"), self.cell()) class TestDownload(unittest.TestCase): @@ -1902,6 +1912,10 @@ def setUp(self) -> None: os.chdir(self.root) self.binary = self.root / "buck2" self.binary.write_text("") + # As under a real wrapper run: resolution exported the environment before anything here runs. + environment = unittest.mock.patch.dict(os.environ, {"BUCK2_BINARY": str(self.binary)}) + environment.start() + self.addCleanup(environment.stop) @contextlib.contextmanager def running(self) -> collections.abc.Iterator[list[object]]: @@ -1915,8 +1929,8 @@ def running(self) -> collections.abc.Iterator[list[object]]: patches.enter_context( patch(cache_shim, "ensure", side_effect=lambda _, ready: self.served.append(ready())) ) - patches.enter_context(patch(tine, "buck2_binary", return_value=self.binary)) - patches.enter_context(patch(os, "execve", side_effect=lambda *a: execve.extend(a))) + patches.enter_context(patch(tine, "ensure_buck2_binary", return_value=True)) + patches.enter_context(patch(os, "execv", side_effect=lambda *a: execve.extend(a))) yield execve def configure_cache(self) -> cache_shim.CacheSettings: @@ -1987,7 +2001,7 @@ def test_a_changed_address_replaces_the_daemon_holding_the_old_one(self) -> None self.killed.clear() (self.root / tine.LOCAL_SETTINGS).write_text("[cache]\nunsigned = true\nport = 21000\n") tine.buck_command(["build", "//..."]) - self.assertEqual(self.killed, [(self.binary, None)]) + self.assertEqual(self.killed, [(None,)]) def test_a_project_may_not_name_the_cache_address_itself(self) -> None: self.configure_cache() @@ -2005,11 +2019,8 @@ def test_a_project_naming_it_with_no_cache_configured_is_its_own_business(self) def test_it_configures_then_hands_over(self) -> None: with self.running() as execve: tine.buck_command(["build", "//x"]) - binary, argv, environment = execve - self.assertEqual((binary, argv), (self.binary, [str(self.binary), "build", "//x"])) - assert isinstance(environment, dict) - self.assertEqual(environment["BUCK2_BINARY"], str(self.binary)) - self.assertEqual(environment["BUCK2_ARG0"], "tine buck") + binary, argv = execve + self.assertEqual((binary, argv), (str(self.binary), [str(self.binary), "build", "//x"])) self.assertTrue((self.root / tine.LOCAL).is_file()) def test_no_mounts_skip_private_git_metadata(self) -> None: @@ -2107,10 +2118,8 @@ def test_a_run_without_a_home_gets_one(self) -> None: with unittest.mock.patch.dict(os.environ, environment, clear=True): with self.running() as execve: tine.buck_command(["build", "//x"]) + self.assertTrue(execve) self.assertEqual(os.environ["HOME"], str(self.root / tine.HOME)) - home = execve[2] - assert isinstance(home, dict) - self.assertEqual(home["HOME"], str(self.root / tine.HOME)) self.assertTrue((self.root / tine.HOME).is_dir()) def test_a_run_with_a_home_keeps_it(self) -> None: @@ -2437,8 +2446,9 @@ def test_completion_does_not_refresh_defaults(self) -> None: self.source.write_text(self.updated) with ( contextlib.chdir(self.root), - unittest.mock.patch.object(tine, "buck2_binary", return_value=self.root / "buck2"), - unittest.mock.patch.object(os, "execve"), + unittest.mock.patch.dict(os.environ, {"BUCK2_BINARY": str(self.root / "buck2")}), + unittest.mock.patch.object(tine, "ensure_buck2_binary", return_value=True), + unittest.mock.patch.object(os, "execv"), ): tine.buck_command(["complete"]) self.assertEqual(self.path.read_bytes(), before) @@ -2449,7 +2459,7 @@ def test_completion_script_does_not_refresh_defaults(self) -> None: self.source.write_text(self.updated) with ( contextlib.chdir(self.root), - unittest.mock.patch.object(tine, "buck2_binary", return_value=self.root / "buck2"), + unittest.mock.patch.object(tine, "ensure_buck2_binary", return_value=True), unittest.mock.patch.object(tine, "_buck2_completion_script", return_value="script"), unittest.mock.patch.object(tine, "rewrite_completion_script", return_value="rendered"), contextlib.redirect_stdout(io.StringIO()) as output, From bdcef83cb4a7b2631845c68d238c8e56b88f7fc0 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Sat, 10 Oct 2026 17:50:44 +0000 Subject: [PATCH 6/6] tine: rename MARKER to TINE_MOUNTS_ENV This constant has confused me too many times. Signed-off-by: Martin Pitt --- bin/tine.py | 10 +++++----- bin/tine_test.py | 36 ++++++++++++++++++++++++------------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/bin/tine.py b/bin/tine.py index d793b081..a179b533 100644 --- a/bin/tine.py +++ b/bin/tine.py @@ -45,7 +45,7 @@ MOUNTS = "mounts" MOUNT_CONFIG = f"{HOME}/tine-mounts.toml" MOUNT_TARGET_LABEL = "tine:mount-target" -MARKER = "TINE_MOUNTS" +TINE_MOUNTS_ENV = "TINE_MOUNTS" MOUNT_LOCK = f"{HOME}/tine-mount.lock" PRIVATE_MOUNTS = ".buckconfig.d/tine-mounts" PRIVATE_CONFIG = f"{PRIVATE_MOUNTS}/config" @@ -835,7 +835,7 @@ def mount_namespace_marker(digest: str) -> str: def namespace_mount_targets(root: Path) -> list[str] | None: """Recover mounted targets without rereading mutable declarations or source paths.""" - value = os.environ.get(MARKER, "") + value = os.environ.get(TINE_MOUNTS_ENV, "") digest = value.partition(" ")[0] if not is_hex(digest, 16) or value != mount_namespace_marker(digest): return None @@ -1003,7 +1003,7 @@ def reexec_configured_wrapper( if mounts: working = cwd() digest = create_mount_namespace(root, mounts) - environment[MARKER] = mount_namespace_marker(digest) + environment[TINE_MOUNTS_ENV] = mount_namespace_marker(digest) try: os.chdir(working) except OSError as error: @@ -1814,8 +1814,8 @@ def buck_command(argv: list[str]) -> None: fail(f"[{RE_CLIENT}] is reserved while [{cache_shim.SECTION}] in {SETTINGS} configures a cache") # Before entering the namespace, or the shim would hold every mount in it for as long as it # runs. The cost is that its pin comes from the outer checkout rather than a mounted cell's. - # MARKER means entering the namespace already re-executed this, which ensured it once. - if command.subcommand not in BUCK_COMMAND_NO_ACTION and not os.environ.get(MARKER): + # TINE_MOUNTS means entering the namespace already re-executed this, which ensured it once. + if command.subcommand not in BUCK_COMMAND_NO_ACTION and not os.environ.get(TINE_MOUNTS_ENV): cache_shim.ensure(cache, lambda: before_shim(root, config, settings, cache, command.isolation)) # A completing shell must neither download nor rewrite shared configuration on a keypress. config, mounts = prepare_buck( diff --git a/bin/tine_test.py b/bin/tine_test.py index b5e2d6ae..ad053b48 100644 --- a/bin/tine_test.py +++ b/bin/tine_test.py @@ -1210,7 +1210,9 @@ def mounted() -> str: self.assertEqual(config["buck2"][tine.DAEMON_BUSTER], tine.BUSTER_PREFIX + digest) self.assertEqual(config["tine"][tine.DEV], "sub") self.assertNotIn(tine.DAEMON_BUSTER, tine.read_project_buckconfig(self.root).get("buck2", {})) - with unittest.mock.patch.dict(os.environ, {tine.MARKER: tine.mount_namespace_marker(digest)}): + with unittest.mock.patch.dict( + os.environ, {tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker(digest)} + ): self.assertEqual(tine.namespace_mount_targets(self.root), ["sub"]) self.assertNotIn(tine.MOUNTS, config["tine"]) for path, content in before.items(): @@ -1242,7 +1244,7 @@ def second() -> str: second_mounts = tine.declared_mounts(self.root) digest = tine.create_mount_namespace(self.root, second_mounts) with unittest.mock.patch.dict( - os.environ, {tine.MARKER: tine.mount_namespace_marker(digest)} + os.environ, {tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker(digest)} ): _, prepared = tine.prepare_buck(self.root, {}, ["buck", "build"]) self.assertEqual(prepared, sorted(second_mounts)) @@ -1254,7 +1256,7 @@ def second() -> str: # Resume A only after B has prepared its replacement, at the point where a shared # buster used to make A advertise B's digest while retaining A's checkout. with unittest.mock.patch.dict( - os.environ, {tine.MARKER: tine.mount_namespace_marker(first_digest)} + os.environ, {tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker(first_digest)} ): _, prepared = tine.prepare_buck(self.root, {}, ["buck", "build"]) self.assertEqual(prepared, sorted(mounts)) @@ -1303,7 +1305,9 @@ def test_defaults_come_from_the_mounted_cell(self) -> None: def mounted() -> str: digest = tine.create_mount_namespace(self.root, {"sub": str(self.source)}) with ( - unittest.mock.patch.dict(os.environ, {tine.MARKER: tine.mount_namespace_marker(digest)}), + unittest.mock.patch.dict( + os.environ, {tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker(digest)} + ), unittest.mock.patch.object(tine, "COMMAND_PATH", self.root / cell / tine.COMMAND), ): config, targets = tine.prepare_buck( @@ -1365,7 +1369,9 @@ def mounted() -> str: digest = tine.create_mount_namespace(self.root, {"sub": str(self.source)}) self.source.rename(retired) replacement.rename(self.source) - with unittest.mock.patch.dict(os.environ, {tine.MARKER: tine.mount_namespace_marker(digest)}): + with unittest.mock.patch.dict( + os.environ, {tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker(digest)} + ): config, targets = tine.prepare_buck(self.root, {}, ["buck", "build"]) ignores = tine.collect_project_ignores( self.root, config, targets, tine.namespace_gitdirs(self.root) @@ -1399,7 +1405,9 @@ def mounted() -> str: self.source.rename(retired) replacement.rename(self.source) declare(self.root, {}) - with unittest.mock.patch.dict(os.environ, {tine.MARKER: tine.mount_namespace_marker(digest)}): + with unittest.mock.patch.dict( + os.environ, {tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker(digest)} + ): _, targets = tine.prepare_buck(self.root, {}, ["buck", "build"]) ignores = tine.collect_project_ignores( self.root, {"cells": {"root": "."}}, targets, tine.namespace_gitdirs(self.root) @@ -1460,7 +1468,7 @@ def mounted() -> str: unittest.mock.patch.dict( os.environ, { - tine.MARKER: tine.mount_namespace_marker(digest), + tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker(digest), "BUCK2_BINARY": str(self.root / "buck2"), }, ), @@ -1560,7 +1568,7 @@ def setUp(self) -> None: self.addCleanup(created.stop) patched = unittest.mock.patch.dict(os.environ) patched.start() - os.environ.pop(tine.MARKER, None) + os.environ.pop(tine.TINE_MOUNTS_ENV, None) self.addCleanup(patched.stop) @contextlib.contextmanager @@ -1628,7 +1636,9 @@ def test_mounted_invocations_still_select_the_wrapper(self) -> None: command = wrapper_command(self.root / "sub") (self.root / ".buckconfig").write_text("[cells]\nroot = .\ntine = sub\n") with ( - unittest.mock.patch.dict(os.environ, {tine.MARKER: tine.mount_namespace_marker("a" * 16)}), + unittest.mock.patch.dict( + os.environ, {tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker("a" * 16)} + ), unittest.mock.patch.object(tine, "namespace_mount_targets", return_value=["sub"]), unittest.mock.patch.object(tine, "declared_mounts", side_effect=AssertionError("reread mounts")), unittest.mock.patch.object(os, "execve", side_effect=SystemExit("handover")) as execve, @@ -1685,7 +1695,9 @@ def test_failed_handover_names_the_selected_wrapper(self) -> None: def test_matching_current_namespace_needs_no_handover(self) -> None: digest = self.declare_one() with ( - unittest.mock.patch.dict(os.environ, {tine.MARKER: tine.mount_namespace_marker(digest)}), + unittest.mock.patch.dict( + os.environ, {tine.TINE_MOUNTS_ENV: tine.mount_namespace_marker(digest)} + ), unittest.mock.patch.object(tine, "namespace_mount_targets", return_value=["sub"]), self.running() as execve, ): @@ -1707,7 +1719,7 @@ def test_stale_marker_does_not_skip_namespace_creation(self) -> None: # A manually exported or inherited marker does not prove that this process is in its namespace. digest = self.declare_one() with ( - unittest.mock.patch.dict(os.environ, {tine.MARKER: f"{digest} wrong-namespace"}), + unittest.mock.patch.dict(os.environ, {tine.TINE_MOUNTS_ENV: f"{digest} wrong-namespace"}), self.running() as execve, ): self.prepare() @@ -1722,7 +1734,7 @@ def test_mounts_create_an_equivalent_namespace_and_handover(self) -> None: _, argv, environment = execve self.assertEqual(argv, [str(TOOL_PATH.absolute()), "buck", "build"]) assert isinstance(environment, dict) - self.assertEqual(environment[tine.MARKER], tine.mount_namespace_marker(digest)) + self.assertEqual(environment[tine.TINE_MOUNTS_ENV], tine.mount_namespace_marker(digest)) class TestBuck2Binary(unittest.TestCase):