Darwin compatibility environment
This backend runs Intel macOS binaries inside a dedicated Linux VM using Darling.
It is separate from the modeled platform used by run, client, replay
and coverage. Neither backend substitutes for the other. Production builds
and physical Mac qualification remain unchanged.
Machine management, an authenticated controller, a guest agent and a browser console are implemented. The initial environment is incomplete: Cocoa input, document persistence, full LLDB debugging, isolation and aslice compatibility still require acceptance evidence. Infrastructure tests do not pass those gates.
Feasibility gate
The chosen runtime is Darling, pinned to
commit 60ba801decee7a00782f74f6be4c8ffb013f79ff. The guest target is Ubuntu 26.04
x86-64. The commit pins the source tree and its direct gitlinks; a provisioned
runtime must additionally record recursive submodule identities, build commands,
compiler, exact package manifest and hashes of all downloaded/build artifacts.
Provisioning retains downloaded packages, versions and hashes, compiler output,
submodule identities, runtime patches and the debugger archive hash. Initial
package resolution still uses Ubuntu repositories; a recorded manifest does not
by itself provide a complete offline rebuild recipe.
Use the upstream build instructions
to investigate build requirements; installing their unversioned package list
does not establish a reproducible runtime identity.
Run preflight without creating a machine:
python -m tools.simulator machine doctor --output build/darwin-preflight.json
python -m tools.simulator machine doctor --acceleration tcg
Windows selects WHPX and Linux selects KVM. Software emulation requires explicit
--acceleration tcg and is reported as such. Missing tools, unsupported hosts
and inaccessible KVM are reported with exit status 1. Exit status 0 means the
tool checks passed, not that acceleration can boot a VM. QEMU's advertised
accelerators are not a successful WHPX/KVM initialization probe. Every report
keeps boot_verified and complete false until execution verification exists.
The current report records host OS, architecture and processor description;
CPUID feature capture still needs implementation in the VM execution lane.
On the implementation host, Darling built and installed in an Ubuntu VM using KVM under WSL. The unchanged Intel macOS hyperfine 1.20.0 artifact printed its version, spawned shell children, wrote a file and exported JSON. These measured CLI results do not qualify Cocoa or debugging. No Mac-built source corpus, matching symbols, aslice binaries or SDK was supplied.
LLDB initially crashed while launching its shell helper: Darling's execve
implementation dereferenced a null environment pointer. The separate runtime
patch runtime-patches/0002-execve-empty-environment.patch treats that pointer
as an empty environment. With the patch, LLDB stopped at an address breakpoint
in unchanged hyperfine, stepped an instruction and returned stacks. A runtime-built
probe also allowed attachment and stack inspection. Expression evaluation and
process termination still showed failures; source and child debugging remain
unqualified. Repeated attachment also triggered a Darling server panic while its
launcher service remained active. The health API distinguishes an active launcher
from an observed live server; process presence alone does not prove responsiveness.
The Mac corpus builder includes an empty-environment reproduction
whose native execution is checked by the corpus workflow.
The browser displayed the real guest through noVNC. A Cocoa probe built against Darling's replacement libraries created a window, but its contents stayed black. That probe tests infrastructure only. Unchanged Hex Fiend 2.12 failed to load the Tcl framework; version 2.7 failed on a missing DiskArbitration callback symbol. These failures leave Cocoa rendering, input and document persistence unqualified.
Checked corpus
The Apache-2.0 corpus source is in tests/simulator/corpus. Build on a Mac with its own SDK:
python tests/simulator/corpus/build.py --output build/mac-corpus
python -m tools.simulator machine corpus build/mac-corpus/corpus.json
The builder produces four thin x86-64 Mach-O executables, matching dSYM files,
source and license copies, compiler/SDK/command provenance and SHA-256 identities.
The CLI probe writes and renames a file, then forks and execs a child. The Cocoa
probe has a window, input, button and menu action. The document probe adds
open/save dialogs. The debugger probe exposes parent_marker and child_marker
for source breakpoints. These sources have not yet been compiled or run on a Mac.
The corpus checker validates hashes, Intel executable headers, load-command
bounds and executable/dSYM UUID agreement. It refuses path traversal, links,
universal binaries, missing debugger symbols and artifacts over 512 MiB.
Its zero exit status means all four corpus roles were ingested successfully.
It does not verify the declared build origin, execute code or grant compatibility
credit. Darling-built entries remain infrastructure-only candidates. The checked
manifest's complete field stays false even with all artifacts present.
Acceptance must exercise the unchanged Mac-built artifacts inside the dedicated VM. Record CLI output and file contents, visible rendering and real input for both GUI probes, and document persistence after restart. LLDB/debugserver must demonstrate launch, attachment, source mapping, breakpoints, stepping, threads, stacks and variables in the parent and exec'd child. Logs alone cannot pass GUI or debugger acceptance. Runtime defects need minimal reproductions and separate, reproducible patches. No preload hooks, binary rewriting or simulator-specific application rebuilds may satisfy unchanged-binary acceptance.
Stopped-image management
Install qemu-img on the host. Supply a standalone raw or qcow2 base image plus
a runtime identity JSON object. The identity schema is version 1, with exactly
these fields:
| Field | Required value |
|---|---|
version |
1 |
guest |
ubuntu-26.04-x86_64 |
darling_commit |
The pinned 40-character commit above |
submodules |
Nonempty map of recursive source paths to 40-character Git commits |
artifacts |
Nonempty map of build-input names to SHA-256 digests |
build |
Nonempty strings compiler, packages, command, source_tree |
packages must identify the exact guest package manifest; include that manifest
in the hashed artifacts. Include the guest image, compiler, source archives,
dependencies and runtime outputs in the build records. The parser checks the
shape of these declarations. It does not independently attest their completeness
or prove that the declared runtime is installed in the supplied image.
python -m tools.simulator machine --root build/darwin-machines create dev --image build/guest/base.qcow2 --runtime build/guest/runtime.json
python -m tools.simulator machine --root build/darwin-machines status
python -m tools.simulator machine --root build/darwin-machines snapshot create dev clean
python -m tools.simulator machine --root build/darwin-machines clone dev experiment
python -m tools.simulator machine --root build/darwin-machines snapshot restore dev clean
python -m tools.simulator machine --root build/darwin-machines delete experiment
Creation defaults to four virtual CPUs, 8192 MiB RAM and a sparse 64 GiB qcow2
overlay. Override --cpus, --memory-mib and --disk-gib before creation. The
base is copied into the store by content identity and shared by overlays. Base
images with backing chains, external data files or encryption are refused.
Disk size must accommodate the base's virtual size. The Darling prefix resides
inside the guest disk.
All mutations require an exclusive OS lease. Process death releases the lease; another controller can reopen the store and report interrupted operation records. Operations journal intent before changing image state. Copies publish only after completion and file synchronization. This provides host-process interruption recovery, not a claim about host power-loss durability. Directory fsync and a physical power-loss validation remain necessary for that milestone.
Snapshots and clones require metadata state stopped. Restore verifies base,
runtime and snapshot disk identities, copies into a new disk, then atomically
switches the metadata pointer. The previous disk remains available. Interrupted
copies and incomplete machines remain visible for diagnosis. Deletion moves the
machine into the store's trash directory; it does not erase disks or shared
bases. Overlay backing paths are absolute: moving the store is unsupported.
The supervisor holds a machine lease while QEMU runs. The controller discovers
surviving supervisors after restart. Do not boot these disks through external
QEMU commands: external processes do not participate in that ownership protocol.
Remaining implementation and acceptance
The environment must use one authenticated, versioned control API for CLI and console. Host credentials must remain outside guest storage. Provision ordinary and administrative guest accounts, remove Darling's default guest-host directory exposure, and verify that guest privilege cannot grant host authority. The VM must expose no host mounts, clipboard, devices or credentials by default. Network access must default off and provisioning downloads must be separate from app execution. Explicit imports and exports must be bounded and recoverable.
After the binary/GUI/debugger proof, implement startup, graceful and forced stop, restart and rediscovery of surviving VMs without duplicate launches. Test actual VM ownership, controller crashes, transfers and restoration. A stopped-image unit test is not evidence for those live-machine requirements.
The loopback console must bundle xterm.js and noVNC locally, display the software rendered guest X11 session and label it as an application display rather than a macOS desktop. Provide lifecycle controls, terminal, process/service/filesystem/ network inspectors, interactive debugging, a searchable event timeline and diagnostic bundles. Browser disconnection must leave application processes alive; debugger detach/terminate must be explicit. Bound captured output and preserve truncation notices. Guest LLDB/debugserver handles Mach-O debugging; Linux tools handle the Darling runtime itself.
Fault controls must distinguish requested placement, acknowledged effects, observed effects and uncertain outcomes. Implement process/service interruption, network loss, disk-full and VM forced stop. Precise filesystem and durability faults belong at the runtime/storage boundary; a VM kill is not proof of macOS power-loss semantics. Keep deterministic replay in the modeled platform. Binary scenarios initially use snapshot restoration and re-execution with explicit nondeterministic differences.
Record exact runtime, guest, CPU capabilities, compiler, dependencies, binaries,
symbols and corpus identities in generated evidence under ignored build/.
Only a dedicated VM execution lane can qualify representative CLI/Cocoa behavior,
debugging, isolation and lifecycle recovery. The preparation CI lane cannot.
Actual aslice compatibility remains pending until Mac-built aslice binaries and
symbols exist. Missing package features remain application gaps. Finder, Metal-
heavy applications, general GUI compatibility, real kexts and full Recovery are
outside the initial milestone; physical Mac qualification remains separate.
Troubleshooting and tests
For a missing qemu-img, install QEMU image utilities and rerun preflight. For
an identity mismatch, preserve the store and inspect the base/runtime/snapshot
hashes; do not edit metadata to bypass validation. For an incomplete creation or
copy, inspect operations, retained .partial files and the active disk pointer.
Never delete the lease file to take ownership from another process. Keep the
store private to the host user; path checks do not defend against a hostile
process already running as that user.
python tests/simulator/test_darwin.py
python tests/simulator/test_runtime.py
python tests/simulator/test_guest_agent.py
python -m tools.simulator coverage --gate development --output build/requirement-coverage.json
The host suite tests ownership, process-crash reconciliation, interrupted copies,
identity mismatches, restore retention and corpus refusal. If qemu-img is
available it also creates and checks real qcow2 overlays. Otherwise that test
reports an explicit skip. Synthetic disk and Mach-O fixtures never constitute
Darling compatibility evidence.
Controller tests cover authentication, concurrent ownership, supervisor crashes
and fragmented guest responses. Guest PTY and transfer tests require a disposable
Linux environment running as root. For browser formatting and lint checks, run
npm ci and npm run check in tools/simulator/web.
Run npm run build there to generate the console in build/simulator-web.
The npm manifest and lockfile pin xterm.js, noVNC and the build tools. The Python
controller serves the generated JavaScript, CSS and license notices locally;
browser use needs no CDN or npm connection. Rebuild after changing console source
or dependencies. Both node_modules and generated assets are ignored by Git.
Without built assets, the controller API still works and the console reports the
setup commands instead of displaying a broken page.
Developing the console
Use Node.js 24.15 or later in the 24.x line, or Node.js 26 or later.
From tools/simulator/web, run npm ci, then
npm run dev. Vite serves the UI at http://127.0.0.1:5173 with hot reload.
This mode requires no simulator: an in-memory adapter supplies sample machines,
sessions, inspectors and file transfers. The banner identifies the demo, terminal
input executes no commands, and reloading resets its state. The sample display
supports text input and a button; it supplies no Cocoa compatibility evidence.
Runtime diagnostic downloads require a live controller.
Demo terminal samples let you exercise Unicode and colors, inline images, percentage and indeterminate progress, pause/error states, truncated output, connection interruption and process exit. Start a sample machine and open a terminal, then expand Demo terminal samples. Enable inline images before using the image sample. Interrupt output followed by Reconnect output exercises cursor recovery on the same session. Captured demo output is bounded to 64 KiB and uses the same absolute byte-cursor contract as the guest agent. These controls and sample generators are excluded from the live JavaScript build.
Append ?scenario=empty, ?scenario=error or ?scenario=slow to exercise an empty
machine list, failed requests or delayed responses. npm run build:demo builds
this preview separately in build/simulator-web-demo; npm run preview:demo
serves it for review.
For live UI development, start python -m tools.simulator console and use the
origin and token from its authenticated URL. In PowerShell:
$env:SIMULATOR_CONTROLLER_URL = 'http://127.0.0.1:CONTROLLER_PORT'
npm run dev:live
Open http://127.0.0.1:5173/#TOKEN, replacing TOKEN with the console URL's
fragment. The UI exchanges it for the controller's HTTP-only cookie and removes
the fragment. Keep the token private. Vite proxies HTTP and display WebSocket
traffic to the loopback controller; live mode does not fall back to sample data.
npm run build generates the assets served by the Python controller. Plain
npm run preview (also npm run serve) previews these assets without providing
a controller API; use dev:live to work against the simulator.
The source kernel, src/app.ts, composes the features and builds to app.js.
Each feature owns its model, UI behavior and tests. Machine selection, sessions,
inspection and transfers are separate domains; the shared gateway validates
controller responses before passing them to features. Development adapters live
under src/development and are excluded from the live build. Rendering uses DOM
APIs; event subscriptions and polling are disposed when the application unloads
or Vite replaces it. noVNC loads when the display is opened.
TypeScript 7 runs with strict checking. npm run check checks types, runs
type-aware Oxlint, checks Prettier formatting and runs Vitest. Use
npm run typecheck:watch, npm run test:watch, npm run lint:fix and
npm run format during development. npm run test:coverage writes its report
under build/simulator-web-coverage. These tests cover UI behavior and controller
contracts, not runtime compatibility.
The terminal uses xterm 6 with Unicode 11 character widths. It fits its panel and sends size changes to the guest PTY. Reconnect output resumes from the last received cursor without launching a process or replaying input. Input is ordered; an uncertain write stops later queued input until the session is reselected. Debugger buttons require a debugger session.
Terminal options enable WebGL rendering with automatic fallback if initialization fails or its context is lost. Inline SIXEL and iTerm images are optional: the image cache is limited to 32 MiB, individual images to four megapixels, and encoded image sequences to 4 MiB. The controller permits WebAssembly compilation for the image decoder while retaining its restriction on JavaScript string evaluation. The progress indicator displays values reported by programs through OSC 9;4; it does not estimate progress for commands that emit no such information.
Export terminal display downloads up to 4000 scrollback rows, capped at 16 MiB, with text, colors, dimensions and cursor state. It excludes inline images and process state and is separate from diagnostic evidence. Headless xterm tests verify serialization and progress parsing without adding a Node.js service to the controller. Web fonts remain deferred because the published addon requires a prerelease xterm version; the console uses local system fonts.
Pinned upstream CLI acquisition
The first available upstream candidate is hyperfine 1.20.0. Fetch its unchanged Intel macOS executable, source archive and both upstream licenses into a fresh output directory:
python -m tools.simulator machine fetch-corpus --output build/upstream-corpus
python -m tools.simulator machine corpus build/upstream-corpus/corpus.json
This is an explicit provisioning download, separate from application execution. The acquisition pins the release archive SHA-256 and the source commit/archive SHA-256. Each download is bounded to 16 MiB and published only after hash verification. It copies only named regular files from the release archive and never executes the binary. Interrupted or mismatched downloads retain partial files without publishing them as validated artifacts.
The source workflow declares a macOS build runner. Exact compiler and SDK versions are not captured in the release archive and remain unverified. The generated acquisition record and corpus manifest preserve that limitation. The expected CLI scenario launches a shell child, writes a file and exports a JSON result. This scenario passed in the development VM. No debugger symbols, Cocoa probe or document probe are supplied by this acquisition, so the corpus command returns 1 for missing roles. Successful download is not compatibility acceptance.
Using the machine and console
Install QEMU, OpenSSH and the controller dependencies, then provision a separate
builder. Windows uses WHPX; replace kvm below with whpx on Windows or explicitly
select tcg when software emulation is intended. Provisioning downloads use the
builder's network; application machines default to networking disabled.
python -m pip install -r tools/simulator/runtime-requirements.txt
python -m tools.simulator machine provision builder --acceleration kvm
python -m tools.simulator machine provision-status builder
python -m tools.simulator machine provision-finalize builder
Finalization requires a completed runtime identity and a stopped builder. Use its
returned image and manifest paths as BASE and RUNTIME below. The base becomes
immutable input to each machine's overlay.
python -m tools.simulator machine create dev --image BASE --runtime RUNTIME
python -m tools.simulator machine start dev --acceleration kvm
python -m tools.simulator console
python -m tools.simulator machine import dev ./program program --executable
python -m tools.simulator machine exec dev /Users/aslice/Imports/program --version
python -m tools.simulator machine shell dev
python -m tools.simulator machine inspect dev
python -m tools.simulator machine debug dev --program /Users/aslice/Imports/program
python -m tools.simulator machine export dev result.json ./result.json
python -m tools.simulator machine stop dev
Transfer paths are relative to the guest Imports directory. Transfers are bounded
to 64 MiB and verify hashes before publication. For exec/shell, place options such
as --detach or --mode linux before the machine name. Linux mode inspects the
runtime and is explicitly distinct from Darwin application execution.
The console bundles xterm.js and noVNC. It displays guest X11 application windows, not an Apple desktop. Terminal processes survive browser disconnects. A persistent guest service keeps Darling alive when a terminal exits. Debugging uses an LLDB terminal with commands for stepping, breakpoints, threads, stacks, variables and source mapping. Detach and termination are explicit. Structured debugger views and unchanged parent/child breakpoint acceptance remain unfinished. The process inspector shows Linux PIDs and, for nested processes, namespace PIDs. Use the runtime PID for Darwin debugger attachment and the Linux PID for Linux debugging. Session labels identify the Linux launcher PID separately.
Inspect .controller/startup.log, each machine's evidence directory and
control/ssh.log for host failures. Use machine serial NAME for guest boot output
and machine health NAME for sessions and build status. A shutdown request that
does not finish is not a confirmed stop. machine stop NAME --force provides an
explicit forced termination.
No host mounts are attached. Complete adversarial guest isolation testing and removal of Darling's guest-root exposure remain pending; removing convenience links alone does not establish that boundary. Inspectors currently report Linux runtime processes and services, not complete Darwin launchd service behavior.