EasyVM is a macOS virtualization suite for Apple Silicon, built on Apple's Virtualization framework. One install gives you:
- 🖱 EasyVM.app — a SwiftUI app for point-and-click VM management.
- ⌨️
easyvmCLI — a single-binary CLI for scripting, CI, and batch operations. - 📦 OCI-compatible distribution — push/pull VM images like container images (any Docker Registry v2 host: GHCR, Docker Hub, ECR, Harbor, …).
The app and the CLI share one core (EasyVMCore). A VM bundle produced by either runs unchanged in the other.
中文文档:README.zh-CN.md
What's new in v1.1.0 — OCI registry
push/pull(tart-style image distribution), Rosetta + bridged networking, macOS 14 snapshots (--restore/--save-on-stop), a non-blocking VM runner, APFSclonefile-backedclone, NATip/sshhelpers, and a guest-agent scaffold. Full notes →
brew tap everettjf/tap
brew install --cask easyvm
open -a EasyVMThen in the app: File → New VM → pick macOS or Linux → follow the wizard.
# 1. Install
brew install everettjf/tap/easyvm # or build from source, see below
# 2. Create and run a Linux VM
easyvm create demo --os linux --storage /tmp/easyvm \
--image ~/Downloads/ubuntu-24.04-arm64.iso \
--cpu 4 --memory-gb 8 --disk-gb 64
easyvm run /tmp/easyvm/demo
# 3. Talk to it (after the guest finishes boot + DHCP)
easyvm ip /tmp/easyvm/demo
easyvm ssh /tmp/easyvm/demo --user ubuntu
# 4. Clean up
easyvm stop /tmp/easyvm/demoeasyvm pull ghcr.io/someone/ubuntu-arm:24.04 --storage /tmp/easyvm
easyvm run /tmp/easyvm/ubuntu-armBoth Path A and Path B produce the same on-disk bundle layout. You can create a VM in the CLI and open it in the app, or vice versa.
- What's in the box
- Requirements
- Installation
- GUI usage
- CLI reference
- Distribution via OCI registries
- Networking
- Rosetta (x86 binaries in Linux guests)
- Snapshots (macOS 14+)
- Guest agent (scaffold)
- Architecture
- Exit codes
- Troubleshooting
- Homebrew release workflow
- Contributing
- License
| Component | Location | Purpose |
|---|---|---|
EasyVM.app |
EasyVM/EasyVM.xcodeproj |
SwiftUI GUI for creating, running, and editing VMs |
easyvm |
Sources/EasyVMCLI |
CLI: lifecycle, OCI push/pull, SSH, snapshots, agent |
easyvm-guest |
Sources/EasyVMGuest |
Agent that runs inside a macOS guest (scaffold) |
EasyVMCore |
Sources/EasyVMCore |
Shared Swift library — models, VZ configuration, OCI, networking |
A VM bundle is a directory that looks like this:
demo/
├── config.json # device/memory/CPU config (with schemaVersion)
├── state.json # runtime state pointer
├── Disk.img # qcow-less raw disk
├── MachineIdentifier # VZ platform identity
├── NVRAM # (Linux) EFI variable store
├── HardwareModel # (macOS) VZ hardware identity
├── AuxiliaryStorage # (macOS) OS boot bits
├── console.log # (Linux) serial console log, rotated on each run
└── guest-agent/ # virtiofs rendezvous for the optional guest agent
- Apple Silicon Mac (M1 / M2 / M3 / M4).
- macOS 13 Ventura or later.
- macOS 14 Sonoma or later for snapshots (
--restore/--save-on-stop). - Bridged networking requires the CLI to be codesigned with the
com.apple.vm.networkingentitlement. The Homebrew bottle and./deploy.shpipeline handle this automatically; for source builds follow Build from source.
brew tap everettjf/tap
brew install --cask easyvm # GUI app (drag-install)
brew install easyvm # CLIgit clone https://github.com/everettjf/EasyVM.git
cd EasyVM
# Build the CLI
swift build -c release
# Sign it so it can use Virtualization + bridged networking
codesign --force --sign - \
--entitlements Sources/EasyVMCLI/EasyVMCLI.entitlements \
./.build/release/easyvm
# (Optional) Install to PATH
cp ./.build/release/easyvm /usr/local/bin/
# Build the GUI app
open EasyVM/EasyVM.xcodeproj # then Cmd+R
⚠️ The CLI must be codesigned withSources/EasyVMCLI/EasyVMCLI.entitlements(notEasyVM.entitlements). Bridged networking and some Rosetta paths silently fail without it.
- Launch EasyVM.
- Create a macOS VM: choose macOS and either tap Download Latest (fetched from Apple) or provide a local
.ipsw(grab one from ipsw.me). - Create a Linux VM: choose Linux and provide an ARM64 ISO. Supported out of the box: Ubuntu 24.04 (ARM64), Fedora 40, Debian 12, Alpine 3.20 (see
easyvm image listfor current URLs). - After the guest is installed, VMs live under your chosen storage folder. Use the sidebar to start/stop/clone/edit.
easyvm create Create a VM bundle
easyvm list List VM bundles in a directory
easyvm run Run a VM (detached by default; --foreground to stay attached)
easyvm stop Stop a running VM (SIGTERM, escalates to SIGKILL after timeout)
easyvm clone Clone a VM bundle (APFS clonefile when possible)
easyvm network list List host bridged interfaces available to VMs
easyvm image list List curated Linux ARM64 ISO URLs
easyvm push Push a VM bundle to an OCI registry
easyvm pull Pull a VM bundle from an OCI registry
easyvm ip Resolve a NAT VM's IP from Apple's DHCP leases
easyvm ssh SSH into a NAT VM
easyvm agent status Read the last heartbeat from the in-guest agent
easyvm agent ping Send a ping command to the in-guest agent
Run easyvm <subcommand> --help for the full option list. The most useful flags:
easyvm create <name> --os <macOS|linux> \
[--storage <dir>] [--image <iso-or-ipsw>] \
[--cpu <n>] [--memory-gb <n>] [--disk-gb <n>] \
[--bridged-interface <bsdName>] [--rosetta] \
[--output text|json]--bridged-interfacetakes absdNamefromeasyvm network list(e.g.en0).--rosettaenables Linux x86_64 translation via arosettavirtiofs share (macOS 13+).--output jsonprints a machine-readable summary for scripting.- macOS VMs created via CLI are skeletons — finish the install in the GUI.
easyvm run <vm-path> [--foreground] [--recovery]
[--restore <state.vzstate>] [--save-on-stop <state.vzstate>]- By default
runspawns a_run-workerchild process; the shell returns immediately. Logs go to<vm-path>/.easyvm-run.log. --foregroundstreams VZ logs to stdout and blocks until the VM exits.--recovery(macOS only) boots to Recovery.--restore/--save-on-stoprequire macOS 14+ (see Snapshots).
easyvm list --storage /tmp/easyvm --output json
easyvm ip /tmp/easyvm/demo
easyvm ssh /tmp/easyvm/demo --user ubuntu -- -L 8080:localhost:8080Arguments after -- are passed through to /usr/bin/ssh so you can forward ports, pin keys, etc.
Uses clonefile(2) on APFS volumes (same volume → instantaneous, no extra disk). Falls back to byte copy across volumes. Also re-randomises the MachineIdentifier so the clone boots as a distinct machine.
easyvm clone /tmp/easyvm/golden /tmp/easyvm/job-$CI_JOB_IDVM bundles pack into a single tar.gz layer with media type application/vnd.easyvm.bundle.v1.tar+gzip, plus a small JSON config blob. Any Docker Registry v2 compatible registry works.
# Public pull (no credentials needed)
easyvm pull ghcr.io/someone/ubuntu-arm:24.04 --storage /tmp/easyvm
# Authenticated push (GHCR example)
export EASYVM_REGISTRY_USER=yourname
export EASYVM_REGISTRY_PASSWORD=ghp_xxx # PAT with write:packages
easyvm push /tmp/easyvm/my-vm ghcr.io/yourname/my-vm:v1Credentials come from the EASYVM_REGISTRY_USER / EASYVM_REGISTRY_PASSWORD environment variables. Bearer-token auth (GHCR-style challenge response) and HTTP Basic are both supported.
NAT (default) — works with zero setup. VMs live on 192.168.64.0/24. Look up the guest's IP with easyvm ip (parses /var/db/dhcpd_leases).
Bridged — VM gets an IP from your LAN's DHCP:
easyvm network list # find bsdNames
easyvm create web --os linux --bridged-interface en0 ...Bridged mode needs the CLI to carry com.apple.vm.networking. The bundled entitlements file (Sources/EasyVMCLI/EasyVMCLI.entitlements) has it — just re-run the codesign command from Build from source if you ever change binaries.
Run x86_64 Linux binaries inside an ARM64 Linux VM via Apple's Rosetta:
# 1. Install Rosetta on the host
softwareupdate --install-rosetta --agree-to-license
# 2. Create the VM with --rosetta
easyvm create linux-dev --os linux --rosetta --image ubuntu-arm64.iso ...Inside the guest, mount the virtiofs share named rosetta and register it with binfmt_misc. Apple's official guide walks through the in-guest steps.
Save and restore the VM's execution state (not just disk state):
# Run and arrange to save on clean stop
easyvm run /tmp/easyvm/demo --save-on-stop /tmp/easyvm/demo/state.vzstate
# ...later...
easyvm stop /tmp/easyvm/demo # pause + save before exit
# Next time, start from that state instead of booting cold
easyvm run /tmp/easyvm/demo --restore /tmp/easyvm/demo/state.vzstateHandy for "always-on" dev environments: save on shutdown → restore in < 1s next day.
An optional in-guest helper that rendezvous with the host through a shared folder. Current status: scaffold — only ping is implemented.
# On the host
easyvm agent status /tmp/easyvm/demo # read heartbeat
easyvm agent ping /tmp/easyvm/demo # round-trip test
# Inside a macOS guest, after mounting the host's guest-agent/ folder
./easyvm-guest /Volumes/easyvm-agentRoadmap: clipboard sync, host→guest command execution, resolution auto-resize, Linux cross-compiled builds. See Sources/EasyVMGuest/EasyVMGuestMain.swift.
┌───────────────────────┐ ┌───────────────────────┐
│ EasyVM.app (GUI) │ │ easyvm (CLI) │
│ SwiftUI, Xcode │ │ SwiftPM executable │
└──────────┬────────────┘ └───────────┬───────────┘
│ │
│ import EasyVMCore │
│ │
▼ ▼
┌───────────────────────────────────────────────┐
│ EasyVMCore (shared) │
│ • VMConfigModel / VMStateModel (schema v1) │
│ • VZVirtualMachineConfiguration builder │
│ • Runner (DispatchSource-driven, macOS 14 │
│ snapshot restore/save hooks) │
│ • OCI Docker Registry v2 client │
│ • DHCP lease parser │
│ • Guest-agent protocol types │
└───────────────────────────────────────────────┘
The app's own model types live in EasyVM/EasyVM/Core/VMKit/Model/ and bridge to Core through CoreBridge.swift.
| Code | Case | Meaning |
|---|---|---|
0 |
— | Success |
1 |
message |
Generic failure |
2 |
notFound |
Bundle / file / interface not found |
3 |
alreadyExists |
Destination already exists |
4 |
invalidState |
VM already running / not running when expected |
5 |
hostUnsupported / rosettaNotInstalled |
Host capability missing |
Scripts can branch on these without parsing stderr.
| Symptom | Likely cause / fix |
|---|---|
run exits silently |
Check <bundle>/.easyvm-run.log |
network list prints nothing |
CLI not signed with com.apple.vm.networking — re-run the codesign step |
ssh or ip returns nothing |
VM still booting / DHCP'ing — wait 10-30s |
push returns HTTP 401 |
Set EASYVM_REGISTRY_USER / EASYVM_REGISTRY_PASSWORD |
--rosetta fails with "not installed" |
softwareupdate --install-rosetta --agree-to-license |
| Linux guest hangs at boot | Confirm ISO is ARM64; verify <bundle>/MachineIdentifier and NVRAM exist |
CLI macOS VM fails to start |
Expected — CLI creates skeletons. Finish the install in GUI |
| Snapshot flags refused | Requires macOS 14+ |
For the macOS App Store submission path, run scripts/prepare_mas.sh for an automated pre-flight check of entitlements, Info.plist keys, and remaining manual steps.
Self-hosted tap (everettjf/homebrew-tap):
scripts/release_homebrew_tap.sh --help
scripts/release_homebrew_tap.sh \
--version 1.0.2 \
--tap-repo everettjf/homebrew-tap \
--app-dmg /absolute/path/to/EasyVM.dmgOne-shot release:
./deploy.sh # bump patch + build + dmg + push formula/cask
./deploy.sh --only-cli # CLI only
./deploy.sh --only-app # App only
./deploy.sh --skip-tests # skip swift testVersion helpers (RepoRead-style):
./inc_patch_version.sh
./inc_minor_version.sh
./inc_major_version.shIssues and PRs welcome. Before opening a PR:
swift test # core + OCI + runner tests
xcodebuild -project EasyVM/EasyVM.xcodeproj -scheme EasyVM \
-destination 'platform=macOS,arch=arm64' build # app buildIf you're using Claude Code, a project-local skill is checked in at .claude/skills/easyvm-cli/SKILL.md — it teaches Claude how to drive every subcommand.
MIT. See LICENSE.