actiond is a local Remote Execution API worker/cache for running Bazel
actions in a Linux sandbox.
It has two execution modes:
linux-actiond: runs actions directly on a Linux host using chroot, private mount/network namespaces, loopback-only TCP, read-only bind mounts, and best-effort cgroups.darwin-actiond serve-vm: runs on macOS and proxies execution into a tiny Linux VM built with Apple's Virtualization.framework.
The macOS VM path is the main reason this project exists: it lets a Mac act like a local Linux remote-execution worker without giving actions normal macOS process access.
For deeper implementation details, see ARCHITECTURE.md.
The repo is fully Bazelized:
- Zig 0.16.0 binaries for macOS, Linux hosts, and the Linux VM guest.
- A minimal arm64 Linux kernel from
http_archive. - A compressed initramfs containing
linux-actiond-guest. - Zstd-compressed SquashFS runtime images containing selected glibc versions.
- Standalone binaries that embed their runtime artifacts.
Important targets:
bazel build //cmd/darwin_actiond:darwin-actiond-standalone
bazel build //cmd/linux_actiond:linux-actiond-standalone \
--platforms=//platforms:linux_aarch64
bazel build //cmd/linux_actiond:linux-actiond-standalone \
--platforms=//platforms:linux_x86_64
bazel build //vm:linux_kernel_zst //vm:initramfs //runtimes:runtimes_squashfsThe Linux standalone target follows Bazel's target platform. Its embedded
runtime image comes from //runtimes:runtimes_squashfs, which selects the
matching runtime SquashFS for the target CPU.
The VM kernel is built by linux.bzl from the Linux archive declared in
MODULE.bazel. The repository currently uses a local_path_override for
linux.bzl until the needed ruleset changes are published.
bazel build //vm:linux_kernel_zstFor optimized artifacts:
bazel build -c opt //cmd/darwin_actiond:darwin-actiond-standalone_pkg
bazel build -c opt //cmd/linux_actiond:linux-actiond-standalone_pkg \
--platforms=//platforms:linux_aarch64
bazel build -c opt //cmd/linux_actiond:linux-actiond-standalone_pkg \
--platforms=//platforms:linux_x86_64Build and run the standalone Darwin worker:
bazel build //cmd/darwin_actiond:darwin-actiond-standalone
bazel-bin/cmd/darwin_actiond/darwin-actiond-standalone serve-vm \
--listen=127.0.0.1:8980 \
--root=/tmp/actiond-vmVM mode stores CAS and ActionCache state inside the guest on an ext4 disk image
attached as virtio-blk. By default the image path is
/tmp/actiond-vm/cas.ext4; pass --cas-image=/path/cas.ext4 to reuse a
specific preformatted image.
The standalone binary embeds:
- compressed Linux kernel
Image.zst - compressed initramfs
- runtime SquashFS
At startup, darwin-actiond extracts those payloads, inflates the boot files
that Virtualization.framework needs as raw files, starts the VM, and proxies
REAPI calls into the guest over virtio-vsock.
On Linux:
bazel build //cmd/linux_actiond:linux-actiond-standalone
bazel-bin/cmd/linux_actiond/linux-actiond-standalone serve \
--listen=127.0.0.1:8980 \
--root=/tmp/actiondThe Linux standalone binary embeds the runtime SquashFS in the ELF
.actiond.runtimes section and extracts it under the worker root at startup
when --runtime-image and --runtime-root are omitted.
Linux execution requires the privileges needed for chroot, bind mounts, mount namespaces, and cgroups. The Docker e2e harness runs privileged for this reason.
Normal checks:
bazel build //...
bazel test //...Linux e2e from macOS using Docker:
tools/docker/run_linux_e2e.shVM e2e on macOS:
tools/e2e.sh vmHeavier LLVM smoke against an already-running VM worker:
e2e/llvm_tblgen_smoke.shFull LLVM smoke comparison, including VM startup and a mac-host baseline:
e2e/run_llvm_vm_smoke.shThe VM smoke uses --platforms=@llvm//platforms:linux_arm64_musl and
--host_platform=@llvm//platforms:linux_arm64_musl so generated exec tools run
inside the Linux VM and do not depend on glibc inside actiond chroots.
The fresh-VM runner defaults to --jobs=8 for stable comparisons; override it
with ACTIOND_LLVM_SMOKE_JOBS, or set that variable to an empty value to let
Bazel choose its default. The latest checked-in timing summary lives in
e2e/LLVM_VM_SMOKE_TIMINGS.md.
Standalone artifact e2e:
ACTIOND_E2E_STANDALONE=1 tools/docker/run_linux_e2e.sh
ACTIOND_E2E_STANDALONE=1 tools/e2e.sh vmThe test/ directory is a separate Bazel workspace used as a stress harness.
It generates many inputs, source-directory style inputs, tree artifacts, output
files, and output directories.
The runtime SquashFS currently packages these glibc runtime names:
glibc2.31glibc2.35glibc2.39
Bazel actions can request one with an execution/platform property:
execution_requirements = {"libc": "glibc2.35"}Unsupported libc names fail instead of silently falling back.
VM execution uses actiondfs for lazy CAS-backed inputs. By default, actiondfs treats declared inputs as immutable: reading input files is allowed, but opening an input for write or truncation fails. Actions may create new output files under input directories, and those writes are staged outside the CAS until output collection.
Actions that intentionally rewrite, delete, or replace input files must opt into the overlayfs compatibility path:
execution_requirements = {"mutates_inputs": "1"}Use this only for tools that really mutate their input tree or declare outputs that overlap input files. Normal actions should use the default strict actiondfs path.
This is active systems work, not a polished product. The core path works:
- REAPI CAS/ByteStream/ActionCache/Execution surface
- Linux chroot execution
- macOS VM execution
- guest-native CAS in VM mode
- compressed VM/runtime packaging
The design prioritizes correctness, hermeticity, and measurable copies before polishing the operator experience.