Source bundling
When you write python -m hello as your entrypoint, the VM needs to find hello/ somewhere on disk. mvmforge puts it there.
Per ADR-0008, v0.1 implements bake-into-rootfs source bundling. At compile time, the user’s source tree is copied into the artifact directory; the generated flake builds it as a Nix derivation; mkGuest’s preStart hook symlinks the package output to the entrypoint’s working_dir at boot.
The pipeline
app.py declares source = local_path("src") ↓mvmforge compile reads app.source.path = "src" (relative to manifest dir) ↓walks the tree, applies include/exclude globs, copies filesdeterministically into <artifact>/src/ ↓generated flake.nix: appPkg = pkgs.stdenv.mkDerivation { pname = "<id>-app"; src = ./src; installPhase = "mkdir -p $out && cp -R . $out/"; }; ↓mkGuest packages = [ appPkg ] ++ imagePackages ↓services.<id>.preStart symlinks ${appPkg} → ${working_dir} ↓services.<id>.command does: cd ${working_dir}; exec <command> ↓VM boots, your code is at /app, python -m hello finds itWhat’s in the artifact
<artifact-dir>/ flake.nix # references ./src launch.json # carries source.tree_hash src/ # your code, deterministically copiedlaunch.json’s source field describes the result:
{ "source": { "kind": "local_path", "subdir": "src", "file_count": 42, "tree_hash": "0539ab1327c962939c92c43069475f74fccf4f01320534d2a08d755d2a7e9b61" }}tree_hash is content-addressed: same bytes in, same hash out, regardless of mtime, machine, or user. The exact byte format is pinned in ADR-0008 §6.
Globs
app.source.include and app.source.exclude filter which files are copied. Defaults: include = ["**"], exclude = []. Glob syntax is the standard recursive form:
| Pattern | Matches |
|---|---|
** | Zero or more path segments. |
* | A single segment with no /. |
? | A single character with no /. |
[abc] | One of the listed characters. |
A path is included iff it matches at least one include pattern AND no exclude pattern. Patterns match against the path relative to source.path, with forward-slash separators on every host OS.
mv.app( name="my-app", source=mv.local_path( ".", include=["**/*.py", "requirements.txt"], exclude=["**/__pycache__/**", "tests/**", ".git/**"], ), ...).git/ is unconditionally excluded regardless of user globs, as a safety default.
Symlinks
Symlinks in the source tree are subject to a safety rule:
- In-tree symlinks (resolved target lies within
source.path) are preserved verbatim. Their relative target string is recorded intree_hash. - Out-of-tree symlinks (absolute paths or targets that climb above
source.pathafter canonicalization) are rejected withE_SOURCE_OUT_OF_TREE_SYMLINK. This prevents the rootfs from inadvertently inheriting bytes the user did not intend (e.g., a straylink -> /etc/passwd).
If you need to vendor an external dependency, copy it into the source tree explicitly.
Devices, sockets, FIFOs
Skipped silently. They don’t have meaningful contents inside a microVM rootfs, and recording them in tree_hash would be a portability hazard.
Reproducibility
The source copy is deterministic:
- File entries are visited in lexicographic order (sorted at every directory level).
- File mtimes are normalized to the Unix epoch when written. Nix uses content + mode for derivation hashing, not mtime, so this is safe.
- File modes are preserved exactly.
- Directory creation is lazy: directories are only created in
<artifact>/src/when a leaf inside survives the filters. Empty source dirs are not preserved.
Result: same source contents + same IR + same toolchain version → byte-identical artifact directory, including tree_hash.
What working_dir means now
Before bundling: working_dir was an absolute path inside the rootfs that the entrypoint cd’d to. With bundling, it still is — the difference is that the bundled source is now at working_dir at boot, via the preStart symlink. The IR field doesn’t change semantics; the rootfs just has more in it.
Known issue: rootfs writability
The preStart symlink is created via ln -sfn, which requires working_dir’s parent to be writable at boot time. mkGuest’s populateImageCommands pre-create /var, /tmp, /run, /home, /root, /mnt/* but not /app. If your working_dir falls outside the writable set and mkGuest mounts the rootfs read-only, preStart may fail.
For v0.1, the workaround is to choose a working_dir under /var/lib/<workload-id>/ or another path under one of the pre-created writable trees. ADR-0008 carries this as a known issue with a documented deferred fix (smart-default location).
Out of scope for v0.1
- Mount-at-launch. A future ADR-0010 may add
mvmctl --volumepassthrough so users get instant code-change reload without recompiling. Not present in v0.1. - Language service factories (
mkPythonService/mkNodeService). v0.1 emits plainmkGuestregardless of the runtime; language-typed factories withpythonPackages/portetc. await ADR-0009. - Non-
local_pathsource kinds.nix_derivationandoci_imageare reserved variants in the schema but rejected at validation.