Skip to content

Commit 9ca7c06

Browse files
recursixclaude
andauthored
fix(infra-local): install qemu lazily in the VM resource path (#237)
LocalInfraConfig.install() ran a blanket script on every make() that built qemu (qemu-img/qemu-system-x86_64) whenever it was absent — even for Docker/ browser cubes that never boot a VM. On a Homebrew prefix without a qemu bottle that became a slow source build on every local run. qemu is touched only inside the VMResourceConfig branches of provision()/ launch(), so ensure it there via _ensure_qemu() (best-effort, no-op when present) and drop the install() override (base no-op). Docker/browser cubes now never trigger qemu. Renames install_local_infra.sh -> install_qemu.sh (its sole remaining job). Local-backend slice of capability-scoped provisioning (#191); supersedes the earlier CUBE_SKIP_QEMU opt-out. Signed-off-by: Alexandre Lacoste <alex.lacoste.shmu@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6892e24 commit 9ca7c06

4 files changed

Lines changed: 72 additions & 58 deletions

File tree

src/cube/infra_local.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,23 @@ def _download(url: str, dest: Path) -> None:
264264
logger.info("Downloaded %s (%.1f GB)", dest.name, dest.stat().st_size / 1024**3)
265265

266266

267+
def _ensure_qemu() -> None:
268+
"""Best-effort install of qemu, run lazily when a VM resource is provisioned/launched.
269+
270+
qemu (qemu-img, qemu-system-x86_64) is needed only by VMResourceConfig, so installing
271+
it here — rather than in a blanket install() that runs for every cube — keeps
272+
Docker/browser cubes from paying for it (cube-standard #191). No-op when qemu is
273+
already present; a failed install warns but does not raise, so a VM cube fails later
274+
with a clear "qemu-system-x86_64 not found" instead of aborting setup.
275+
"""
276+
if shutil.which("qemu-system-x86_64"):
277+
return
278+
ref = importlib.resources.files("cube").joinpath("scripts/install_qemu.sh")
279+
with importlib.resources.as_file(ref) as script:
280+
if script.exists():
281+
subprocess.run(["bash", str(script)], check=False)
282+
283+
267284
def _convert_to_qcow2(src: Path, dst: Path) -> None:
268285
"""Convert src image to qcow2 format using qemu-img."""
269286
if dst.exists():
@@ -490,17 +507,10 @@ class LocalInfraConfig(InfraConfig):
490507

491508
# ── InfraConfig interface ─────────────────────────────────────────────────
492509

493-
def install(self) -> None:
494-
"""Install local system dependencies if not already present.
495-
496-
qemu (VM-backed cubes only) is best-effort: a failed qemu install warns but does
497-
NOT abort, so offline / Docker / browser cubes stay runnable on `local` infra even
498-
where qemu can't build (e.g. Homebrew on Apple Silicon). See the script's comment
499-
and cube-standard #191 (scope provisioning to declared task capabilities)."""
500-
ref = importlib.resources.files("cube").joinpath("scripts/install_local_infra.sh")
501-
with importlib.resources.as_file(ref) as script:
502-
if script.exists():
503-
subprocess.run(["bash", str(script)], check=True)
510+
# No install() override: system deps are installed lazily per resource type
511+
# (qemu via _ensure_qemu() in the VM provision/launch path), so Docker/browser
512+
# cubes never pay for qemu. Scoping provisioning to declared capabilities is
513+
# cube-standard #191; this is the local-backend slice of it.
504514

505515
def fingerprint(self) -> str:
506516
return "local"
@@ -542,6 +552,7 @@ def provision(self, resource: ResourceConfig) -> None:
542552
if not isinstance(resource, VMResourceConfig):
543553
raise UnsupportedResourceType(resource, self)
544554

555+
_ensure_qemu() # qemu-img is needed for the qcow2 convert below
545556
image_dir = Path(self.image_dir)
546557
image_dir.mkdir(parents=True, exist_ok=True)
547558
dest = image_dir / f"{resource.name}.qcow2"
@@ -578,6 +589,7 @@ def launch(self, resource: ResourceConfig) -> ResourceHandle:
578589
if not isinstance(resource, VMResourceConfig):
579590
raise UnsupportedResourceType(resource, self)
580591

592+
_ensure_qemu() # qemu-system-x86_64 boots the VM below
581593
from cube.provision_store import ProvisionStore
582594

583595
resource_info = ProvisionStore().get(resource, self)

src/cube/scripts/install_local_infra.sh

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/cube/scripts/install_qemu.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
# Best-effort install of qemu (qemu-img + qemu-system-x86_64), needed only by
3+
# VM-backed cubes (osworld, windows-agent-arena). Invoked lazily by
4+
# LocalInfraConfig._ensure_qemu() when it provisions/launches a VMResourceConfig —
5+
# never for Docker/browser cubes. Safe to run multiple times.
6+
#
7+
# BEST-EFFORT: a failure (e.g. Homebrew's qemu bottle not building on Apple
8+
# Silicon) warns but does not abort — the VM cube then fails later with a clear
9+
# "qemu-system-x86_64 not found" when it actually boots a VM.
10+
set -euo pipefail
11+
12+
qemu_warn() {
13+
echo "WARNING: could not install qemu — VM-backed cubes (osworld, " \
14+
"windows-agent-arena) won't run until you install it manually." >&2
15+
}
16+
17+
case "$(uname)" in
18+
Linux)
19+
{ sudo apt-get update -qq && sudo apt-get install -y qemu-system-x86 qemu-utils; } || qemu_warn
20+
;;
21+
Darwin)
22+
brew install qemu || qemu_warn
23+
;;
24+
*)
25+
echo "Unsupported platform: $(uname). Install qemu-system-x86_64 manually for VM cubes." >&2
26+
;;
27+
esac

tests/test_infra_local.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
_register_active,
2525
)
2626
from cube.infra_utils import build_volume_setup_script
27-
from cube.resource import DockerServiceConfig, VolumeSpec
27+
from cube.resource import DockerServiceConfig, VMResourceConfig, VolumeSpec
2828

2929
# ── build_volume_setup_script ─────────────────────────────────────────────────
3030

@@ -510,3 +510,24 @@ def test_unreachable_daemon_raises_actionable_error(self, monkeypatch) -> None:
510510
def test_active_docker_context_host_swallows_missing_cli(self) -> None:
511511
with patch.object(_mod.subprocess, "run", side_effect=FileNotFoundError("docker")):
512512
assert _mod._active_docker_context_host() is None
513+
514+
515+
class TestQemuLazyInstall:
516+
"""qemu is installed lazily, only for VM resources — never for Docker/browser cubes."""
517+
518+
def test_docker_provision_does_not_install_qemu(self, monkeypatch: pytest.MonkeyPatch) -> None:
519+
calls: list[bool] = []
520+
monkeypatch.setattr(_mod, "_ensure_qemu", lambda: calls.append(True))
521+
monkeypatch.setattr(LocalInfraConfig, "_provision_docker_service", lambda self, r: None)
522+
LocalInfraConfig().provision(DockerServiceConfig(name="svc", scope="task"))
523+
assert calls == []
524+
525+
def test_vm_provision_installs_qemu(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
526+
calls: list[bool] = []
527+
monkeypatch.setattr(_mod, "_ensure_qemu", lambda: calls.append(True))
528+
monkeypatch.setattr(_mod, "_download", lambda url, dest: dest.write_bytes(b""))
529+
monkeypatch.setattr(_mod, "_convert_to_qcow2", lambda src, dst: dst.write_bytes(b""))
530+
monkeypatch.setattr(LocalInfraConfig, "register", lambda self, r, info: None)
531+
cfg = LocalInfraConfig(image_dir=str(tmp_path))
532+
cfg.provision(VMResourceConfig(name="vm", scope="benchmark", source_url="http://x/img.raw"))
533+
assert calls == [True]

0 commit comments

Comments
 (0)