# containerization-framework
Rust bindings for Apple's [Containerization](https://github.com/apple/containerization)
framework: Linux containers.
```rust
use containerization_framework as cfw;
use cfw::model::{Dns, LinuxProcessConfiguration, NatInterface, VmResources};
let cache = std::path::Path::new("/Users/me/.cache/containers");
let store = cfw::Store::at(
cache,
cache.join(format!("vmlinux-{}", cfw::KERNEL_VERSION)),
cfw::INITFS_REFERENCE,
cache.join(format!("vminit-{}.ext4", cfw::INITFS_VERSION)),
);
let session = cfw::Session::new(store);
let mut spec = cfw::BootSpec::new("example", "docker.io/library/debian:stable-slim");
spec.vm = VmResources { cpus: 4, memory_in_bytes: (4 << 30) + VmResources::GUEST_MEMORY_OVERHEAD };
spec.configuration.cpus = 4;
spec.configuration.memory_in_bytes = 4 << 30;
spec.configuration.process = LinuxProcessConfiguration::new(&["/bin/sleep", "infinity"]);
spec.configuration.interfaces = vec![NatInterface::new("192.168.64.7/24", "192.168.64.1")];
spec.configuration.dns = Some(Dns { nameservers: vec!["192.168.64.1".into()], ..Dns::default() });
session.boot(&spec)?;
let code = session.exec(
"example",
"hello",
&LinuxProcessConfiguration::new(&["/bin/echo", "hello"]),
cfw::Stdio::inherit(false),
)?;
```
A container belongs to the process that booted it and dies with it. Nothing
lists containers, though other processes may join running ones.
## Requirements
- macOS 26 on Apple silicon, and Xcode 26 to build.
- Network on a first build: the build script compiles the bundled Swift package,
which resolves Containerization and its dependencies through SwiftPM. Versions
are pinned by the `Package.resolved` that ships with this crate.
On non-macOS platforms, this crate compiles but returns errors on every call.
## Codesigning
**A binary using this crate must carry the `com.apple.security.virtualization`
entitlement.** Without it Virtualization.framework refuses to start a VM, and
`Session::boot` fails saying so.
A `containerization.entitlements` file ships with this crate; binaries compiled
against `containerization-framework` should pass it, or a copy of it, to `codesign`
after compilation.
```sh
cargo build --release
codesign --force --sign - --entitlements containerization.entitlements \
target/release/your-binary
```
Signing ad hoc (`--sign -`) satisfies the entitlement but gives the binary a new
code identity on every rebuild, so anything keyed to that identity — Keychain
access, for one — prompts again. Sign with a development identity to keep it
stable.
A rebuild drops the signature, so this runs after every build.
## Linking
The Swift runtime this links against is dynamic and referenced as
`@rpath/libswift_Concurrency.dylib`, which dyld resolves against `/usr/lib/swift`
in macOS. Anything that links this crate — a binary of yours, and the
test binaries of any crate of yours that links it — needs that rpath, or it
links and then dies in dyld at launch.
A build script's link arguments reach only its package's targets, so the rpath
belongs in `.cargo/config.toml`, where a rustflag covers every kind of target:
```toml
[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-arg=-Wl,-rpath,/usr/lib/swift"]
```
## Shape
- [`Store`] is an image store on disk, in Containerization's layout.
- [`Builder`] provisions it (a kernel and the `vminitd` init image) and turns a
[`BuildPlan`] into an image: pull the base, unpack it to a writable ext4
block, boot it, run each step, and store the result as a single-layer image.
No daemon, no builder image, no Dockerfile.
- [`Session`] boots a [`BootSpec`]'s container from an image and runs
processes in it.
Configuration types in `model` mirror Containerization's
(`LinuxContainerConfiguration`, `LinuxProcessConfiguration`, `Mount`, ...), with
the same names and defaults, so its documentation applies. One difference:
fields the image seeds (arguments, working directory, user) are `Option`s, and
`None` keeps the image's.
Build caching is by rootfs snapshot rather than by layer: a rebuild resumes from
the deepest step whose `cache_key` still matches. This crate only stores and
compares them -- caching, cache invalidation, etc. are the responsibility of
callers.
## Unimplemented
The framework is larger than these bindings. Not exposed: signals to a guest
process, `LinuxPod` (several containers in one VM), container statistics,
filesystem freeze/thaw/trim, host↔guest file copy, registry authentication and
push, OCI layout import/export, per-process rlimits and capabilities, and Rosetta
(so no linux/amd64 — arm64 only).
An OCI runtime (and so seccomp) is configurable, but requires an init image with
`runc`, which Apple does not publish. Pass one's reference to `Store::at`.
## Versioning
The Containerization release and the kernel are pinned separately, and a
mismatch fails at runtime rather than at build time. `Session::version()` names
both.
The pins are public (`KERNEL_VERSION`, `KERNEL_URL`, `INITFS_VERSION`,
`INITFS_REFERENCE`). As in Containerization, the caller chooses where the kernel
and unpacked init image live (`Store::at`). Provisioning fills empty paths and
leaves existing files alone, so name paths for the pinned versions, as above,
to pick up upgrades. Another init image must carry the pinned release's
`vminitd`.
## Testing
`cargo nextest run` runs the unit tests.
The suite in `tests/` boots real containers, so it sits behind the `integration`
feature and runs through `bin/dev/test-integration`, which signs each test binary
with `containerization.entitlements` first — the entitlement is checked against the
calling process.
Those tests share an image store at `~/.cache/containerization-framework-tests`,
kept between runs. The first run fills it — a kernel download, the init image, and
a small image built from `alpine:3` — so it needs the network; later runs reuse it.
This directory can be deleted.
## License
MIT. Containerization itself is Apache-2.0 and is fetched at build time, not
vendored here.