Building, shipping and hosting D9010 VmApp releases
For: maintainers who produce releases of the D9010 VmApp Launcher, and anyone hosting D9010 VmApp (the loader downloads, the runtime images, an app registry). App developers shipping their own apps should read Distributing instead.
1. What a release is#
One download per platform, the D9010 VmApp Launcher. It carries everything apps need:
Inside D9010VmAppLauncher.app (macOS, arm64) |
Size | Built by |
|---|---|---|
Electron 44.4.5 + the loader (app.asar, 216 KB of loader code) |
~300 MB | loader/build/package.mjs (@electron/packager) |
Contents/Resources/vmapp/vmapp-vm: the VM launcher, signed with the virtualization entitlement |
0.8 MB | tools/build.sh |
Image (Linux 6.18.56 kernel, 7.6 MB) + initramfs.cpio (vmapp-init) |
8 MB | tools/build.sh (the kernel: kernel/build.sh) |
runtimes/native.img, node22.img, dotnet8.img |
0.2 + 122 + 111 MB | tools/mkruntime |
| Total app | ~540 MB on disk; ~300 MB zipped | tools/package-mac.sh |
The VM ships once, here. Apps (.vmapp) never contain a VM, kernel or runtime. Updating the loader updates the
kernel and runtimes for every app.
Users need nothing else installed: no Node, no npm, no Docker. Electron carries its own Node, which only the loader's trusted main process uses (FAQ).
2. Build a release (macOS)#
What the build machine needs (everything else is in this repository; no sibling folder is needed):
- macOS 13+ on Apple silicon, with the Xcode command-line tools (
clang,codesign); - Rust with the
aarch64-unknown-linux-musltarget (rustup target add aarch64-unknown-linux-musl). The scripts useRUSTUP_HOME/CARGO_HOMEif you set them, else a sibling../_toolchainsworkspace toolchain if there is one, elsecargoandrustcfromPATH(tools/lib/rust-env.sh); they check the target and therust-lldlinker and say what to install; - Node.js 22 (the packager, the CLI, the tests);
- Docker, only to build the guest kernel and the runtime images (
kernel/build.sh,tools/mkruntime).
Where things go: products in out/, and all scratch files and caches (the cargo target folder, the downloaded
kernel source, the Docker build contexts, test data) in out/work/. Set VMAPP_WORK to put those elsewhere. Both are
git-ignored.
cd vmapp
tools/package-mac.sh # builds what's missing, packages, applies fuses, signs (ad hoc), zips
# → out/dist/D9010VmAppLauncher.app
# → out/dist/D9010-VmApp-Launcher-<version>-mac-arm64.zip
What it does, in order:
tools/build.sh(if needed): the guest init, initramfs,vmapp-vmand the kernel. The kernel step usesout/Imagewhen it is up to date forkernel/(pins, config, lists, scripts), otherwise runskernel/build.sh: it downloads the pinned Linux source once (cached inout/work/cache/kernel/), verifies it against kernel.org's PGP-signed checksums, builds the arm64Imagein a digest-pinned Ubuntu container, and checks the final config againstkernel/kconfig-required.txtandkconfig-forbidden.txt. On an M-series Mac the first build takes about 3 minutes (150 MB download, build container, one minute of compiling); after a config change, about 1 minute.tools/build.sh --kernel-from PATHinstalls a prebuiltImageinstead (no Docker);--rebuild-kernelforces a rebuild.tools/mkruntimefor any missing runtime image. This needs Docker on the build machine.npm ciinloader/(dev dependencies only: Electron, @electron/packager 20.3.0, @electron/fuses 2.1.3).Package the loader into
.appwithapp.asar, and copy the VM resources intoContents/Resources/vmapp/.Electron fuses, the production lock-down:
Fuse State RunAsNode off NODE_OPTIONS off --inspectoff asar integrity validation on only load the app from asar on cookie encryption on file:// extra privileges off Sign inside-out: every framework, helper and binary, then the app.
vmapp-vmis the only binary withcom.apple.security.virtualization.Zip with
ditto.
Production hardening in the packaged loader:
- Startup: it refuses
--remote-debugging-*,--inspect*,--no-sandbox,--disable-web-securityand similar switches at startup. - Developer access: DevTools are off for the loader's own windows. An app's own windows get them only if the app's
core asked (
webPreferences.devTools: true), so ship production builds of your app without it. EveryVMAPP_TEST_*hook and resource override is ignored. - Folder choices: very broad folders (home,
/Volumes, system folders) need a second confirmation;/and the loader's own data folder are refused.
Details: ../loader/PACKAGING.md.
2.1 Signing for real: Developer ID and notarization#
Ad-hoc builds run on the build machine. Other Macs show Gatekeeper warnings. A public release needs an Apple Developer ID and notarization:
xcrun notarytool store-credentials "<PROFILE>" --apple-id "<APPLE_ID>" --team-id "<TEAMID>" # once; it asks for an app-specific password, so none is on the command line
SIGN_IDENTITY="Developer ID Application: <NAME> (<TEAMID>)" tools/package-mac.sh --with-examples --examples-dir=<a folder of the examples you may publish>
bash tools/notarize-mac "<PROFILE>"
tools/notarize-mac refuses an ad-hoc build, sends the zip to Apple's notary service and waits for the verdict, prints Apple's log if it is not "Accepted", and
otherwise staples the ticket and re-creates the zip. By hand, those steps are xcrun notarytool submit <zip> --keychain-profile "<PROFILE>" --wait,
xcrun stapler staple out/dist/D9010VmAppLauncher.app, ditto -c -k --keepParent out/dist/D9010VmAppLauncher.app <zip> (re-zip after stapling) and
spctl --assess --type execute --verbose=4 out/dist/D9010VmAppLauncher.app, which must say "accepted, source=Notarized Developer ID".
Apple opens archives inside archives. With --with-examples the app carries .vmapp packages, and Apple's checker looks inside them (a package is a tar,
and its core a cpio). Any macOS program in there that is not signed with your Developer ID and a secure timestamp makes the whole submission "Invalid". Our
packages hold Linux programs, which Apple ignores, so the build must not leave macOS or Windows files in them: apps/terminal/build.sh deletes the
prebuilds/darwin-* and prebuilds/win32-* folders that the Gemini CLI's dependencies bring. Which examples a release carries is chosen with
tools/pack-examples --without NAME.
Developer ID builds use entitlements.mac.plist, which is without disable-library-validation. That entitlement
exists only so ad-hoc local builds can load Electron's framework.
2.2 Windows and Linux#
They aren't built yet. See Platforms for the design and the work list. Each platform will get the
same package-<os> flow: an MSIX or NSIS installer for Windows, AppImage, deb and rpm for Linux.
3. Reproducibility and trust#
The loader is open source, so its users should be able to check that a release is exactly the published source.
| Artifact | Reproducible today? |
|---|---|
vmapp-init, initramfs.cpio |
yes (static build, deterministic cpio) |
the kernel Image |
yes: pinned base image digest, dated apt snapshot, verified source, fixed build identity and timestamp (kernel/pins.env); the same inputs give the same bytes |
| runtime images | yes: the same pinned base digests give byte-identical images (native and node22 verified twice) |
.vmapp packages |
yes: pack twice, identical bytes |
vmapp-vm |
deterministic Rust build, with the code signature applied on top |
| the Electron app bundle | mostly; Electron itself comes from the official release. Signatures differ per signer. |
Every release publishes:
SHA256SUMSfor every artifact;- the git commit;
- the runtime image digests;
- an SBOM;
- the Linux kernel source offer (GPL): kernel/SOURCE-OFFER.md and the build's
out/kernel-info/(source verification evidence,BUILD-INFO.txt, the finalkernel.config,licenses/linux/).
Runtime images also carry their own licences: Node.js (MIT), .NET (MIT) and Debian packages.
4. Hosting#
| What | Where to start | Notes |
|---|---|---|
| loader downloads | GitHub Releases | one zip or DMG per platform, plus SHA256SUMS |
| runtime images (if not all bundled) | GitHub Releases + a signed runtime index (runtimes.json: name, version, sha256, size, URL) |
lets the loader fetch, say, dotnet8 on first use, verified by hash. Not built yet; today all runtimes are bundled. |
| apps | anywhere: the publisher's site, GitHub, the D9010 VmApp registry | each build is identified by its package id |
| the app registry | one container (registry/Dockerfile + compose.yaml) behind a TLS reverse proxy or a Cloudflare Tunnel, with its packages and event log in an S3-compatible bucket (R2) or on its own disk |
../registry/README.md (storage design), ../registry/deploy/HOSTING.md (EC2 + Cloudflare, step by step, with costs and backups) |
5. Updates#
Not built yet. The plan, consistent with INV-G3 (no network without the user):
- the loader checks a signed release feed only when the user allows it, with an off switch;
- each update downloads the full app, verified by signature;
- runtime updates arrive with loader updates. A runtime-only update can use the signed runtime index.
6. Release checklist#
-
tools/check-invariantspasses - unit, e2e, apps, template, convert, registry and loader suites pass on a clean checkout
-
tools/package-mac.shwith the Developer ID; notarized and stapled;spctl --assessaccepts it - the packaged app launches with no Node on
PATHand runs a sample app end to end -
SHA256SUMS, the SBOM, runtime digests, the commit hash and the kernel source offer are published - the release notes list SPEC or format changes and any invariant changes (and there should be none without a GOVERNANCE pull request)