Skip to content

JRickey/msl

Repository files navigation

msl running Ubuntu in a macOS Terminal window

msl

msl is a WSL2-style Linux subsystem for MacOS, built in Swift and Rust, for Apple Silicon.

Author's Notes

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.

Status

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.1 on MacOS.
  • Run Mac commands from Linux through the mac shim.
  • Transparently execute Mach-O binaries from Linux through binfmt.
  • Export and install .msl distro bundles; double-click .msl install is handled by msl.app.
  • Enable Rosetta per distro for x86-64 Linux binaries.
  • Create Mac app launchers for installed distros in /Applications/msl, with MSL_APPLICATIONS_DIR as 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.fsmodule entitlement. 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.

Requirements

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-musl target.
  • cargo-zigbuild and 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 by tools/mk-libxkbcommon.sh.

The ordinary CLI/daemon path uses the standard virtualization entitlement and can be ad-hoc signed for local development.

Build

Initialize submodules first:

git submodule update --init --recursive

Common 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 test

The 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.

Release Packaging

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 TEAMID

Build and notarize:

make release-pkg VERSION=0.1.0
make notarize VERSION=0.1.0

GitHub 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.

Quick Start

Build the CLI:

make host sign

Install a catalog distro:

host/.build/release/msl catalog list
host/.build/release/msl catalog show ubuntu
host/.build/release/msl install ubuntu

Use --name when the installed distro name should differ from the catalog family:

host/.build/release/msl install ubuntu@24.04 --name ubuntu-lts

Or 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.msl

Installs create an msl-owned launcher app for the distro:

host/.build/release/msl launcher list
host/.build/release/msl launcher open ubuntu

Open a shell:

host/.build/release/msl shell ubuntu

Run a command:

host/.build/release/msl run ubuntu -- /usr/bin/uname -a

Check daemon and distro state:

host/.build/release/msl status
host/.build/release/msl list

Export a shareable bundle:

host/.build/release/msl export ubuntu --output ubuntu.msl

Enable Rosetta for a distro:

host/.build/release/msl config ubuntu --rosetta on

Mount a distro in Finder:

host/.build/release/msl fskit enable
host/.build/release/msl mount ubuntu --read-only --reveal
host/.build/release/msl unmount ubuntu

Read-write FSKit mounts are the default for msl mount, but live use requires a profile-backed FSKit appex build.

CLI Reference

Help is built in:

msl help
msl help run
msl help daemon install

User-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.

Agent Skill

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/msl

Install it into Claude Code with:

mkdir -p ~/.claude/skills
cp -R skills/msl ~/.claude/skills/msl

See skills/README.md for repo-scoped install paths and packaging notes.

Architecture

Host

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-menubar and MSLMenuBarCore: menu-bar app and .msl open/install flow.
  • msl-fskit: FSKit app extension source. SwiftPM type-checks it, but the shippable appex is built by Xcode through host/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.

Guest

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-side mac shim 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

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.

Interop

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.

Filesystems

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-only keeps the certified EROFS path.
  • msl unmount <distro> --force can 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 Direction

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.

Validation

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 initramfs

FSKit live checks:

tools/fskit-e2e.sh
tools/fskit-e2e.sh --write

Those 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.

Repository Layout

.
├── 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.

License

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.

About

MacOS Subsystem for Linux, built in Swift and Rust, for a seamless Linux experience on MacOS.

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors