91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
487 lines
18 KiB
Python
487 lines
18 KiB
Python
"""Runtime compatibility checks — host/guest OS + arch + hardware acceleration.
|
|
|
|
Answers two questions for any Image before you try to boot it locally:
|
|
|
|
1. Is the required runtime installed (or auto-installable) on this host?
|
|
2. Does the host OS/arch support hardware acceleration for the guest OS/arch?
|
|
|
|
Usage::
|
|
|
|
from cua_sandbox import Image
|
|
from cua_sandbox.runtime.compat import check_local_support
|
|
|
|
support = check_local_support(Image.linux())
|
|
print(support.supported) # True if Docker is present
|
|
print(support.hw_accel) # True for containers (native), True on Linux VMs with KVM
|
|
print(support.reason) # human-readable summary
|
|
|
|
# Or via the Image method:
|
|
support = Image.macos().local_support()
|
|
|
|
In pytest, use the bundled helper::
|
|
|
|
from cua_sandbox.runtime.compat import skip_if_unsupported
|
|
|
|
async def test_something():
|
|
skip_if_unsupported(Image.macos())
|
|
async with Sandbox.ephemeral(Image.macos(), local=True) as sb:
|
|
...
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import platform
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from cua_sandbox.image import Image
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Result type
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class RuntimeSupport:
|
|
"""Result of a local runtime compatibility check for a given Image.
|
|
|
|
Attributes:
|
|
supported: Image can run locally at all (runtime available or
|
|
auto-installable, and host/guest OS is compatible).
|
|
hw_accel: Hardware acceleration is available for the guest on
|
|
this host (HVF on Apple Silicon, KVM on Linux, etc.).
|
|
Always True for containers — they run natively.
|
|
runtime_installed: Required runtime binary/daemon is found right now
|
|
without any installation step.
|
|
auto_installable: Runtime can be downloaded and installed automatically
|
|
by the SDK without user intervention (e.g. Android SDK).
|
|
runtime_name: Name of the runtime that would be used
|
|
("docker", "lume", "qemu", "android_emulator", "hyperv").
|
|
host_os: Detected host OS ("darwin", "linux", "windows").
|
|
host_arch: Detected host CPU architecture ("arm64", "x86_64").
|
|
guest_os: Guest OS from the image ("linux", "macos", "windows", "android").
|
|
reason: Human-readable explanation — always set, describes
|
|
the limiting factor when supported=False or hw_accel=False.
|
|
"""
|
|
|
|
supported: bool
|
|
hw_accel: bool
|
|
runtime_installed: bool
|
|
auto_installable: bool
|
|
runtime_name: str
|
|
host_os: str
|
|
host_arch: str
|
|
guest_os: str
|
|
reason: str
|
|
|
|
def __str__(self) -> str:
|
|
accel = "hw-accel" if self.hw_accel else "software-only"
|
|
status = "supported" if self.supported else "unsupported"
|
|
installed = (
|
|
"installed"
|
|
if self.runtime_installed
|
|
else ("auto-installable" if self.auto_installable else "not-installed")
|
|
)
|
|
return (
|
|
f"RuntimeSupport({self.guest_os} on {self.host_os}/{self.host_arch}: "
|
|
f"{status}, {accel}, runtime={self.runtime_name} [{installed}])"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal host probes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _host_os() -> str:
|
|
s = platform.system().lower()
|
|
if s == "darwin":
|
|
return "darwin"
|
|
if s == "windows":
|
|
return "windows"
|
|
return "linux"
|
|
|
|
|
|
def _host_arch() -> str:
|
|
m = platform.machine().lower()
|
|
if m in ("arm64", "aarch64"):
|
|
return "arm64"
|
|
return "x86_64"
|
|
|
|
|
|
def _has_docker() -> bool:
|
|
from cua_sandbox.runtime.docker import _has_docker as _docker_check
|
|
|
|
return _docker_check()
|
|
|
|
|
|
def _has_kvm() -> bool:
|
|
from pathlib import Path
|
|
|
|
return _host_os() == "linux" and Path("/dev/kvm").exists()
|
|
|
|
|
|
def _has_hvf_for_arm64_guest() -> bool:
|
|
"""HVF for ARM64 guests (macOS VMs, ARM Android) — Apple Silicon only."""
|
|
return _host_os() == "darwin" and _host_arch() == "arm64"
|
|
|
|
|
|
def _has_hvf_for_x86_guest() -> bool:
|
|
"""HVF for x86_64 guests (Windows, Linux VMs) — Intel Macs only.
|
|
|
|
On Apple Silicon, QEMU must use TCG software emulation for x86_64 guests
|
|
because HVF does not support cross-architecture virtualisation.
|
|
On Intel Macs, QEMU can use HVF (-accel hvf) for x86_64 guests.
|
|
"""
|
|
return _host_os() == "darwin" and _host_arch() == "x86_64"
|
|
|
|
|
|
def _x86_guest_hw_accel() -> tuple[bool, str]:
|
|
"""Return (hw_accel, reason) for an x86_64 guest on the current host."""
|
|
os_ = _host_os()
|
|
arch = _host_arch()
|
|
if os_ == "linux" and _has_kvm():
|
|
return True, "KVM hardware acceleration."
|
|
if os_ == "darwin" and arch == "x86_64":
|
|
return True, "Apple Hypervisor.framework (HVF) via QEMU -accel hvf on Intel Mac."
|
|
if os_ == "windows" and _has_hyperv():
|
|
return True, "Hyper-V hardware acceleration."
|
|
if os_ == "darwin" and arch == "arm64":
|
|
return False, (
|
|
"QEMU cannot use HVF for x86_64 guests on Apple Silicon — TCG software emulation only. "
|
|
"Expect slow performance."
|
|
)
|
|
return False, (
|
|
f"No hardware acceleration for x86_64 guest on {os_}/{arch}. "
|
|
"Enable KVM (Linux) or Hyper-V (Windows) for acceleration."
|
|
)
|
|
|
|
|
|
def _has_lume() -> bool:
|
|
from cua_sandbox.runtime.lume import _has_lume as _lume_check
|
|
|
|
return _lume_check()
|
|
|
|
|
|
def _has_android_sdk() -> bool:
|
|
"""Return True if the Android emulator binary is present.
|
|
|
|
Delegates to android_emulator._sdk_path() — the exact same logic the
|
|
runtime uses at boot — then checks that emulator/emulator exists there.
|
|
"""
|
|
|
|
from cua_sandbox.runtime.android_emulator import _sdk_path
|
|
|
|
sdk = _sdk_path()
|
|
return (sdk / "emulator" / "emulator").exists() or (
|
|
sdk / "emulator" / "emulator.exe"
|
|
).exists() # Windows
|
|
|
|
|
|
def _has_java() -> bool:
|
|
"""Return True if a usable JRE is found.
|
|
|
|
Calls android_emulator._java_env() — the exact same probe the runtime uses.
|
|
Returns False if _java_env() raises (Java not found).
|
|
"""
|
|
from cua_sandbox.runtime.android_emulator import _java_env
|
|
|
|
try:
|
|
_java_env()
|
|
return True
|
|
except RuntimeError:
|
|
return False
|
|
|
|
|
|
def _has_hyperv() -> bool:
|
|
from cua_sandbox.runtime.hyperv import _has_hyperv as _hyperv_check
|
|
|
|
return _hyperv_check()
|
|
|
|
|
|
def _has_qemu() -> bool:
|
|
"""Return True if a QEMU binary is available.
|
|
|
|
Delegates to qemu_installer.qemu_bin() which checks PATH, Homebrew,
|
|
MacPorts, and the cached portable install — the same resolution the
|
|
runtime uses at boot time.
|
|
"""
|
|
arch = _host_arch()
|
|
from cua_sandbox.runtime.qemu_installer import qemu_bin
|
|
|
|
try:
|
|
qemu_bin(arch)
|
|
return True
|
|
except RuntimeError:
|
|
# Also try the other arch — a cross-arch QEMU is still useful
|
|
other = "x86_64" if arch == "arm64" else "arm64"
|
|
try:
|
|
qemu_bin(other)
|
|
return True
|
|
except RuntimeError:
|
|
return False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main check
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def check_local_support(image: "Image") -> RuntimeSupport:
|
|
"""Return a RuntimeSupport describing whether *image* can run locally.
|
|
|
|
Args:
|
|
image: The Image to check. Use Image.linux(), Image.macos(), etc.
|
|
|
|
Returns:
|
|
RuntimeSupport with .supported, .hw_accel, .reason, and more.
|
|
"""
|
|
os_ = _host_os()
|
|
arch = _host_arch()
|
|
guest = image.os_type
|
|
kind = image.kind or "vm"
|
|
|
|
# ── Linux container ────────────────────────────────────────────────────
|
|
if guest == "linux" and kind == "container":
|
|
installed = _has_docker()
|
|
return RuntimeSupport(
|
|
supported=installed,
|
|
hw_accel=True, # containers run natively — no emulation overhead
|
|
runtime_installed=installed,
|
|
auto_installable=False,
|
|
runtime_name="docker",
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason=(
|
|
"Docker is running."
|
|
if installed
|
|
else "Docker is not running or not installed. Install from https://docker.com"
|
|
),
|
|
)
|
|
|
|
# ── Linux VM ──────────────────────────────────────────────────────────
|
|
if guest == "linux" and kind == "vm":
|
|
docker_ok = _has_docker()
|
|
qemu_ok = _has_qemu()
|
|
installed = docker_ok or qemu_ok
|
|
hw, hw_reason = _x86_guest_hw_accel()
|
|
runtime = "docker+qemu" if docker_ok else ("qemu" if qemu_ok else "qemu")
|
|
if not installed:
|
|
reason = (
|
|
"No runtime for Linux VMs found. "
|
|
"Install Docker (preferred) or qemu-system-x86_64."
|
|
)
|
|
else:
|
|
reason = f"Linux VM via QEMU. {hw_reason}"
|
|
return RuntimeSupport(
|
|
supported=installed,
|
|
hw_accel=hw,
|
|
runtime_installed=installed,
|
|
auto_installable=False,
|
|
runtime_name=runtime,
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason=reason,
|
|
)
|
|
|
|
# ── macOS VM ──────────────────────────────────────────────────────────
|
|
if guest == "macos":
|
|
# macOS VMs require macOS host + Apple Silicon (HVF via Virtualization.framework)
|
|
if os_ != "darwin":
|
|
return RuntimeSupport(
|
|
supported=False,
|
|
hw_accel=False,
|
|
runtime_installed=False,
|
|
auto_installable=False,
|
|
runtime_name="lume",
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason=f"macOS VMs require a macOS host. Current host OS: {os_}.",
|
|
)
|
|
if arch != "arm64":
|
|
return RuntimeSupport(
|
|
supported=False,
|
|
hw_accel=False,
|
|
runtime_installed=False,
|
|
auto_installable=False,
|
|
runtime_name="lume",
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason=(
|
|
"macOS VMs require Apple Silicon (ARM64) for Apple Hypervisor.framework. "
|
|
f"Current host arch: {arch} (Intel Macs cannot run macOS VMs via Lume)."
|
|
),
|
|
)
|
|
# Apple Silicon macOS — HVF always available
|
|
installed = _has_lume()
|
|
return RuntimeSupport(
|
|
supported=True, # Lume can be auto-installed via `lume install`
|
|
hw_accel=True, # HVF always present on Apple Silicon
|
|
runtime_installed=installed,
|
|
auto_installable=True, # Lume installs via a single curl/brew command
|
|
runtime_name="lume",
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason=(
|
|
"macOS VM with Apple Hypervisor.framework (HVF) acceleration."
|
|
if installed
|
|
else (
|
|
"Lume CLI not installed but can be auto-installed. "
|
|
"Run: brew install trycua/tap/lume or curl -fsSL https://lume.sh | sh"
|
|
)
|
|
),
|
|
)
|
|
|
|
# ── Windows VM ────────────────────────────────────────────────────────
|
|
if guest == "windows":
|
|
if os_ == "windows" and _has_hyperv():
|
|
return RuntimeSupport(
|
|
supported=True,
|
|
hw_accel=True,
|
|
runtime_installed=True,
|
|
auto_installable=False,
|
|
runtime_name="hyperv",
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason="Windows VM with Hyper-V hardware acceleration.",
|
|
)
|
|
docker_ok = _has_docker()
|
|
qemu_ok = _has_qemu()
|
|
installed = docker_ok or qemu_ok
|
|
hw, hw_reason = _x86_guest_hw_accel()
|
|
runtime = "docker+qemu" if docker_ok else ("qemu" if qemu_ok else "qemu")
|
|
if not installed:
|
|
reason = (
|
|
"No runtime for Windows VMs found. "
|
|
"Install Docker (preferred) or qemu-system-x86_64."
|
|
)
|
|
else:
|
|
reason = f"Windows VM via QEMU. {hw_reason}"
|
|
return RuntimeSupport(
|
|
supported=installed,
|
|
hw_accel=hw,
|
|
runtime_installed=installed,
|
|
auto_installable=False,
|
|
runtime_name=runtime,
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason=reason,
|
|
)
|
|
|
|
# ── Android VM ────────────────────────────────────────────────────────
|
|
if guest == "android":
|
|
sdk_ok = _has_android_sdk()
|
|
java_ok = _has_java()
|
|
# HW accel matrix for Android:
|
|
# Apple Silicon (arm64): HVF for ARM64 Android system images
|
|
# Intel Mac (x86_64): HVF for x86_64 Android system images
|
|
# Linux x86_64 w/ KVM: KVM for x86_64 Android system images
|
|
if os_ == "darwin" and arch == "arm64":
|
|
hw = True
|
|
hw_reason = "Apple Hypervisor.framework (HVF) with ARM64 Android system image."
|
|
elif os_ == "darwin" and arch == "x86_64":
|
|
hw = True
|
|
hw_reason = (
|
|
"Apple Hypervisor.framework (HVF) with x86_64 Android system image on Intel Mac."
|
|
)
|
|
elif os_ == "linux" and _has_kvm():
|
|
hw = True
|
|
hw_reason = "KVM with x86_64 Android system image."
|
|
elif os_ == "windows":
|
|
# WHPX (Windows Hypervisor Platform) is used by the Android emulator on Windows
|
|
hw = _has_hyperv() # WHPX requires Hyper-V / Windows Hypervisor Platform
|
|
hw_reason = (
|
|
"Windows Hypervisor Platform (WHPX) acceleration."
|
|
if hw
|
|
else "WHPX not available. Emulator will use software rendering (slow)."
|
|
)
|
|
else:
|
|
hw = False
|
|
hw_reason = (
|
|
f"No hardware acceleration for Android on {os_}/{arch}. "
|
|
"Emulator will use software rendering (slow)."
|
|
)
|
|
|
|
installed = sdk_ok and java_ok
|
|
auto = not installed # SDK auto-installs on all platforms if Java is present
|
|
|
|
if not java_ok:
|
|
reason = (
|
|
"Java not found. Install via: brew install openjdk (macOS), "
|
|
"apt install default-jdk (Linux), or "
|
|
"winget install EclipseAdoptium.Temurin.21.JDK (Windows). " + hw_reason
|
|
)
|
|
elif not sdk_ok:
|
|
reason = "Android SDK not found but will be auto-installed on first boot. " + hw_reason
|
|
else:
|
|
reason = "Android SDK installed. " + hw_reason
|
|
|
|
return RuntimeSupport(
|
|
supported=java_ok, # Java is a hard prerequisite; SDK auto-installs if Java is present
|
|
hw_accel=hw,
|
|
runtime_installed=installed,
|
|
auto_installable=auto and java_ok,
|
|
runtime_name="android_emulator",
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason=reason,
|
|
)
|
|
|
|
# ── Unknown / registry image with unresolved kind ─────────────────────
|
|
return RuntimeSupport(
|
|
supported=False,
|
|
hw_accel=False,
|
|
runtime_installed=False,
|
|
auto_installable=False,
|
|
runtime_name="unknown",
|
|
host_os=os_,
|
|
host_arch=arch,
|
|
guest_os=guest,
|
|
reason=(
|
|
f"Cannot determine runtime for os_type={guest!r}, kind={kind!r}. "
|
|
"Use Image.linux() / .macos() / .windows() / .android() "
|
|
"or pass runtime= explicitly."
|
|
),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pytest helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def skip_if_unsupported(image: "Image", *, require_hw_accel: bool = False) -> None:
|
|
"""Call at the top of a pytest test/fixture to skip when the host can't run *image*.
|
|
|
|
Args:
|
|
image: The Image the test intends to boot with local=True.
|
|
require_hw_accel: If True, also skip when hardware acceleration is unavailable
|
|
(e.g. skip Android tests that would run in software emulation).
|
|
|
|
Example::
|
|
|
|
async def test_macos_brew_install():
|
|
skip_if_unsupported(Image.macos())
|
|
async with Sandbox.ephemeral(Image.macos().brew_install("wget"), local=True) as sb:
|
|
assert (await sb.shell.run("wget --version")).success
|
|
"""
|
|
import pytest # imported here so compat.py has no hard pytest dep at import time
|
|
|
|
support = check_local_support(image)
|
|
if not support.supported:
|
|
pytest.skip(f"[{support.runtime_name}] {support.reason}")
|
|
if require_hw_accel and not support.hw_accel:
|
|
pytest.skip(
|
|
f"[{support.runtime_name}] hardware acceleration not available: {support.reason}"
|
|
)
|