msl is a WSL2-style Linux subsystem for MacOS, built in Swift and Rust, for Apple Silicon.
The objective of this project is to provide the same feeling that WSL provides for Windows, but on Mac. That is, the feeling of a seamless Linux Machine that lives within your Mac: An instant Linux shell, named distros, localhost in Linux that also opens on Mac, command interop between Mac and Linux, the ability to access your Linux files in Finder, and a native GUI experience. GUI apps open in MacOS windows, work seamlessly with retina displays, virtual desktops, and minimizing to the toolbar. Your Mac visual settings like dark mode carry through to Linux GUI apps.
This is not a generic VM manager.
I do not like using VMs. They are clunky, they feel disposable, easy to forget about and lose work in, and most importantly, fake. msl obviously has to use virtualization but provides a lot more UX features besides dropping a linux distro in a container. I personally always prefer using an actual platform to do platform specific work or testing. However, my work typically requires me to test my projects on Linux, or to use Linux specific utilities like GDB. I do not want to make a disposable VM or Apple Container for Linux. I like WSL's style and UX a lot, and wanted to bring that over to my Macbook, which is my preferred development platform.
msl is built around Apple's Virtualization.framework:
- A Swift host owns the CLI, resident daemon, VM lifecycle, AppKit/menu-bar UI, and FSKit integration.
- A static-musl Rust guest agent runs inside a shared utility VM and talks to the host over vsock.
- One msl kernel is shared by all distros; each distro is a containerized userland inside the VM.
- Guests are native arm64 by default. Rosetta x86-64 Linux translation is an explicit per-distro opt-in.
This repository is under active development.
Working:
- Boot a headless Linux VM with a Rust PID-1-adjacent agent.
- Run Ubuntu-style systemd distros as named containerized userlands.
- Use a resident per-user daemon with lazy shared-VM boot, idle shutdown, and faithful shell/command exit codes.
- Install from the built-in catalog or local image sources; list, remove, configure, set default, export, and re-import distros.
- Share the Mac home directory into Linux at
/mnt/mac, with opt-out per distro. - Mirror guest TCP listeners onto
127.0.0.1on MacOS. - Run Mac commands from Linux through the
macshim. - Transparently execute Mach-O binaries from Linux through binfmt.
- Export and install
.msldistro bundles; double-click.mslinstall is handled bymsl.app. - Enable Rosetta per distro for x86-64 Linux binaries.
- Create Mac app launchers for installed distros in
/Applications/msl, withMSL_APPLICATIONS_DIRas the development override. - Mount a distro in Finder at
~/msl/<distro>through FSKit. Read-only mode is certified; the read-write protocol, guest worker, host FSKit surface, CLI mode, and write E2E harness have landed. - Present Linux GUI windows through the
msl-way/AppKit prototype path, with native windows, popups, resize pacing, and measured frame/input latency.
Planned next:
- Productize GUI app support beyond the prototype gate.
- Improve Mac file-sharing performance beyond Apple's virtiofs metadata limits.
- Add a Mac-native services layer: Keychain-backed Secret Service, XDG desktop portals, notifications, SSH agent bridging, trust/certificate sync, Touch ID for Linux authorization, and related Mac integration.
- Decide whether source builds need a zero-Apple-account Finder fallback for cases where the developer cannot sign a profile-backed FSKit appex.
Known constraints:
- MacOS 26+ on Apple Silicon is the target platform.
- The base VM is headless and NAT-only. Bridged networking requires an Apple-restricted entitlement and is not part of the current design.
- FSKit Finder integration requires the distributed app to include a signed
app-extension with the
com.apple.developer.fskit.fsmoduleentitlement. Users of a properly signed/notarized release should not need their own Apple Developer account; source builders who want FSKit do. - Virtualization.framework exposes no Linux GPU acceleration. GUI apps use software rendering; correct behavior is the goal before GPU-class speed.
- Rosetta is an escape hatch, not the primary path, and Apple's long-term Rosetta policy may force a future fallback.
Developer builds currently assume:
- Apple Silicon Mac running MacOS 26 or newer.
- Full Xcode with the MacOS 26 SDK and Swift 6.x toolchain.
- Rust with the
aarch64-unknown-linux-musltarget. cargo-zigbuildand Zig for Linux-musl guest builds from MacOS.- A checked-out
kernel/submodule containing the msl kernel build recipe. - Optional for FSKit appex work:
xcodegen, an Apple Development signing identity, an App ID with File System Module and App Groups capabilities, and a provisioning profile for the local Mac. - Optional for
msl-way: Homebrew bison >= 3.8 and the static libxkbcommon build path used bytools/mk-libxkbcommon.sh.
The ordinary CLI/daemon path uses the standard virtualization entitlement and can be ad-hoc signed for local development.
Initialize submodules first:
git submodule update --init --recursiveCommon build targets:
make host # Swift host release build
make sign # ad-hoc sign the CLI with virtualization entitlement
make guest # Rust guest workspace for aarch64-musl
make initramfs # assemble build/initramfs.cpio
make builder-initramfs # assemble build/builder-initramfs.cpio
make app # assemble build/msl.app
make smoke # boot the minimal VM and run an exec smoke testThe guest uses cargo zigbuild, not plain cargo build, because MacOS does
not provide a Linux ELF linker, musl sysroot, or C cross-compiler.
SwiftPM does not re-sign products after rebuilds. Use make host sign or a
Makefile target that depends on signing before running a Virtualization-backed
binary.
Public releases are Developer ID .pkg installers. The pkg installs
/Applications/msl.app, adds /usr/local/bin/msl, embeds the kernel and
initramfs assets in the app bundle, and enables the FSKit module for the
installing console user via msl fskit enable --no-restart. Each release also
publishes msl-agent-skill-${VERSION}.tar.gz for agent harnesses that can
install skill bundles.
Local release builds read optional private values from .env:
APPLE_TEAM_ID=...
FSKIT_PROVISION_PROFILE=/path/to/fskit.provisionprofile
RELEASE_APP_SIGN_IDENTITY=Developer ID Application: ...
RELEASE_INSTALLER_SIGN_IDENTITY=Developer ID Installer: ...
NOTARYTOOL_ARGS=--apple-id you@example.com --password xxxx-xxxx-xxxx-xxxx --team-id TEAMIDBuild and notarize:
make release-pkg VERSION=0.1.0
make notarize VERSION=0.1.0GitHub releases use the same targets on macos-26 and require these secrets:
APPLE_TEAM_ID, DEVELOPER_ID_APPLICATION_CERT_P12_BASE64,
DEVELOPER_ID_APPLICATION_CERT_PASSWORD,
DEVELOPER_ID_APPLICATION_CERT_SHA1,
DEVELOPER_ID_INSTALLER_CERT_P12_BASE64,
DEVELOPER_ID_INSTALLER_CERT_PASSWORD,
APP_DEVELOPER_ID_PROFILE_BASE64, FSKIT_DEVELOPER_ID_PROFILE_BASE64,
APPLE_ID, and
APPLE_APP_SPECIFIC_PASSWORD.
DEVELOPER_ID_APPLICATION_CERT_SHA1 must be the SHA-1 identity hash authorized
by both provisioning profiles, not just any cert with the same display name.
Build the CLI:
make host signInstall a catalog distro:
host/.build/release/msl catalog list
host/.build/release/msl catalog show ubuntu
host/.build/release/msl install ubuntuUse --name when the installed distro name should differ from the catalog
family:
host/.build/release/msl install ubuntu@24.04 --name ubuntu-ltsOr install a local distro source. A .msl bundle can carry its own default
name:
host/.build/release/msl install ubuntu --from ./ubuntu-rootfs.tar.xz
host/.build/release/msl install --from ./ubuntu-custom.mslInstalls create an msl-owned launcher app for the distro:
host/.build/release/msl launcher list
host/.build/release/msl launcher open ubuntuOpen a shell:
host/.build/release/msl shell ubuntuRun a command:
host/.build/release/msl run ubuntu -- /usr/bin/uname -aCheck daemon and distro state:
host/.build/release/msl status
host/.build/release/msl listExport a shareable bundle:
host/.build/release/msl export ubuntu --output ubuntu.mslEnable Rosetta for a distro:
host/.build/release/msl config ubuntu --rosetta onMount a distro in Finder:
host/.build/release/msl fskit enable
host/.build/release/msl mount ubuntu --read-only --reveal
host/.build/release/msl unmount ubuntuRead-write FSKit mounts are the default for msl mount, but live use requires a
profile-backed FSKit appex build.
Help is built in:
msl help
msl help run
msl help daemon installUser-facing commands:
| Command | Purpose |
|---|---|
msl catalog list [--all] |
List catalog distros available for install. |
msl catalog show <selector> |
Show catalog details and the matching install command. |
msl install <selector> [--name <name>] |
Download and install a catalog distro such as ubuntu or ubuntu@24.04. |
msl install [name] --from <path> |
Install an ext4 image, rootfs tarball, or .msl bundle. |
msl list |
List installed distros with state, image size, and hostname. |
msl default <name> |
Set the default distro. |
msl config <name> |
Show or change hostname, default user, Mac sharing, and Rosetta. |
msl shell [name] [-- <argv...>] |
Open a daemon-backed shell. |
msl run [name] -- <command> [args...] |
Run one command and preserve its exit status. |
msl status |
Show daemon, VM, memory, forwarded ports, distro state, and sessions. |
msl stop [name] |
Stop one distro gracefully. |
msl stop --all |
Stop all distros and the shared VM; keep the daemon alive. |
msl shutdown |
Stop all distros, stop the VM, and exit the daemon. |
msl export <name> |
Export a distro to .tar or .msl. |
msl mount [name] |
Mount a distro at ~/msl/<distro> through FSKit. |
msl unmount [name] |
Unmount a distro Finder view. |
msl fskit enable/status/disable |
Manage the MacOS FSKit enabled-modules setting. |
msl launcher list/create/remove/refresh/reveal/open |
Manage Mac app launchers for installed distros. |
msl desktop probe/launch <name> |
Probe for or launch a supported distro desktop session. |
msl daemon run/install/uninstall |
Run or install the per-user daemon. |
msl up |
Direct boot path for registered or one-off rootfs images. |
msl boot |
Low-level developer VM boot command. |
There is also a hidden msl gui-spike command for the GUI forwarding prototype.
This repository includes a portable Agent Skills-compatible consumer skill at
skills/msl/. This is for your agents to learn how to use msl if you're working
on a project that benefits from the utility msl provides.
Install it into Codex with:
mkdir -p ~/.codex/skills
cp -R skills/msl ~/.codex/skills/mslInstall it into Claude Code with:
mkdir -p ~/.claude/skills
cp -R skills/msl ~/.claude/skills/mslSee skills/README.md for repo-scoped install paths and packaging notes.
The Swift package in host/ contains:
msl: command-line interface.MSLCore: VM lifecycle, daemon, registry, protocols, interop, port forwarding, FSKit mount lifecycle, GUI presenter support, and shared helpers.MSLFSWire: Swift FSKit file-service codec shared by the daemon and appex.msl-menubarandMSLMenuBarCore: menu-bar app and.mslopen/install flow.msl-fskit: FSKit app extension source. SwiftPM type-checks it, but the shippable appex is built by Xcode throughhost/fskit-appex.yml.msl-fskit-probe-server: local signing/auth probe utility.
The daemon is a per-user LaunchAgent. It owns the shared VM, boots it lazily, attaches registered distro images, brokers shell/run sessions, keeps active mounts alive, mirrors guest ports to host localhost, and exposes Mac interop surfaces.
The Rust workspace in guest/ contains:
msl-agent: guest control plane and distro lifecycle manager.msl-wire: shared guest-side wire framing and FS protocol code.msl-shim: Linux-sidemacshim and transparent Mach-O exec helper.msl-fsd: FSKit file-service worker launched inside a distro namespace.msl-way: headless Wayland compositor and remoting prototype.
Guest crates use Rust edition 2024 and deny unsafe code. The production guest artifacts are static-musl binaries.
kernel/ is a submodule. The msl repository is Apache-2.0; the kernel source
recipe is GPLv2 in the kernel repo. Kernel source and msl source do not move
between repositories.
Linux-to-Mac command interop uses a guest-initiated vsock channel. Interactive and noninteractive stdio are framed separately, and exit codes propagate back to Linux.
Inside a distro:
mac sw_vers
mac open .Transparent Mach-O exec registers binfmt entries inside each distro, so a Mac
binary on /mnt/mac can be invoked without the mac prefix when the target is
safe to map back to the host.
Mac home sharing uses virtiofs at /mnt/mac. It is convenient and correct, but
metadata-heavy workloads are slower than guest ext4 because Apple's virtiofs
path serializes and compounds APFS create/delete costs. Keep build scratch and
large dependency trees on guest ext4 when performance matters.
Finder access to Linux files uses FSKit:
msl mount <distro>mounts at~/msl/<distro>.msl mount <distro> --read-onlykeeps the certified EROFS path.msl unmount <distro> --forcecan clear a stranded mount if needed.
The FSKit path is architecturally preferred because msl owns both ends of the
protocol and can bound failure as EIO/ENODEV. The distribution challenge is
Apple's restricted FSKit entitlement; an NFS fallback remains the no-paid-account
design path.
GUI forwarding is built around a guest-terminated Wayland compositor
(msl-way) and a Swift AppKit/Metal host presenter. The host does not parse
Wayland; it receives an msl-owned window/surface protocol over vsock.
The prototype has demonstrated native windows, popups, resize handling, Retina scale handling, host-driven pacing, and low input/present latency for software rendered apps. GPU acceleration is not available through Virtualization.framework today, so GL-heavy and Electron-style apps are expected to be correct before they are fast.
Common host checks:
swift test --package-path host
make host
make app
swift format lint --strict <changed-swift-files>
swiftlint lint --strict --quiet <changed-swift-files>Common guest checks:
(cd guest && cargo fmt --check)
(cd guest && cargo test --workspace)
(cd guest && cargo clippy --all-targets --quiet -- -D warnings)
(cd guest && cargo clippy --target aarch64-unknown-linux-musl --all-targets --quiet -- -D warnings)
(cd guest && cargo deny check)
make guest
make initramfsFSKit live checks:
tools/fskit-e2e.sh
tools/fskit-e2e.sh --writeThose FSKit checks require a signed, enabled appex environment. If that state is not available, run the build, unit, lint, and type-check gates that do not depend on the MacOS extension runtime.
.
├── entitlements/ # dev, release, and FSKit appex entitlements
├── guest/ # Rust guest workspace
├── host/ # SwiftPM host package
├── kernel/ # GPLv2 kernel source/build submodule
├── tools/ # initramfs/rootfs/FSKit/libxkb/license helpers
├── Makefile # build, sign, app, smoke, and packaging targets
├── LICENSE # Apache-2.0 for this repository
├── NOTICE # attribution notices
└── THIRD-PARTY-LICENSES
The local development workflow may also use ignored working documents under
docs/ for roadmap, ADRs, specs, reports, and research notes.
msl is Apache-2.0. The kernel submodule is GPLv2 and is kept as a separate
source boundary. See LICENSE, NOTICE, THIRD-PARTY-LICENSES, and the kernel
submodule's COPYING file.
