35 payloads move to tooling/scripts/blender/ and stay outside package scope. They run under Blender's bundled Python, which cannot see the repo venv, so they physically cannot import tooling.core — holding them to the D-263 contract would either fail the gate forever or force the contract to be weakened for everyone, and the second is how a gate stops meaning anything. Count verified by import rather than filename: 33 import bpy/bmesh directly, and the two that do not are still payloads per their own usage lines. garment-fit/make_logo.py is the one genuine non-payload and stays for T-1290. The bash wrapper is retired rather than kept. Keeping it would have put the install-resolution logic in two places, which is the duplication T-1286 had just finished collapsing three copies of. domains/blender/service.py owns the decisions — resolve_blender (native beats flatpak, ordering preserved), resolve_payload, absolutise — and only run_payload performs. test_blender.py pins all of them without launching Blender, which matters here more than usual: the thing being launched is a 200 MB GUI application that writes GLBs. `reach blender run` takes a registered payload name OR a path to any script, because the wrapper served both — the spikes and the glb-gen skill hand it one-off scripts of their own. An unknown name enumerates all 35 and exits 2. The exclusion now defends itself. check_carve_out_stays_carved fails if `scripts` is added to PACKAGE_ROOTS, if the payload directory empties (an empty exclusion proves nothing), or if an __init__.py appears there (which would make the payloads importable — the coupling the carve-out exists to prevent). All three arms mutation-proved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
66 lines
2.1 KiB
Python
66 lines
2.1 KiB
Python
"""Transport for the `blender` domain — args in, delegate, format out."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import typer
|
|
|
|
from tooling.core import cli, console
|
|
from tooling.core.command import command
|
|
from tooling.domains.blender import service
|
|
|
|
app = cli.domain("blender", "Run the Blender payloads — the one carve-out.")
|
|
|
|
|
|
@app.callback()
|
|
def _domain() -> None:
|
|
"""Keeps `blender` a group (Typer collapses a single-command app)."""
|
|
|
|
|
|
@app.command("list")
|
|
@command
|
|
def list_payloads() -> None:
|
|
"""List every payload `run` accepts.
|
|
|
|
Worth a verb of its own: the payload names are not guessable and this is
|
|
the only place the vocabulary is written down.
|
|
"""
|
|
names = service.payloads()
|
|
for name in names:
|
|
console.out(name)
|
|
console.verdict(f"{len(names)} payload(s) in {service.payload_dir().name}/")
|
|
|
|
|
|
@app.command("which")
|
|
@command
|
|
def which() -> None:
|
|
"""Report which Blender would be used, without launching it."""
|
|
invocation = service.resolve_blender()
|
|
console.out(f"kind {invocation.kind}")
|
|
console.out(f"command {' '.join(invocation.argv_prefix)}")
|
|
console.verdict(
|
|
"flatpak — paths are absolutised before they cross the sandbox"
|
|
if invocation.sandboxed
|
|
else "native install — preferred when both are present"
|
|
)
|
|
|
|
|
|
@app.command("run", context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
@command
|
|
def run(
|
|
ctx: typer.Context,
|
|
payload: str = typer.Argument(
|
|
..., help="Payload name (see `reach blender list`), or a path to any script."
|
|
),
|
|
background: bool = typer.Option(
|
|
True, "--background/--no-background", help="Headless, or open the Blender GUI."
|
|
),
|
|
) -> None:
|
|
"""Run a payload under Blender, passing any remaining arguments through.
|
|
|
|
Extra arguments go to the payload after `--`, and any that name an existing
|
|
path are made absolute first — flatpak's sandbox resolves relative paths
|
|
against a different root, and the resulting file-not-found comes from
|
|
inside Blender, a long way from its cause.
|
|
"""
|
|
service.run_payload(payload, list(ctx.args), background=background)
|