Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

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}"
)