Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/capy/build.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
let { Attrs, BuildSpec, Local, OutputBin, OutputData, Test, .. } = import "minimal.ncl" in
let base = import "../base/build.ncl" in
let coreutils = import "../coreutils/build.ncl" in
let glibc = import "../glibc/build.ncl" in
let ca-certificates = import "../ca-certificates/build.ncl" in
let git = import "../git/build.ncl" in
let node-lts = import "../node-lts/build.ncl" in

let version = "0.6.1" in
{
name = "capy",
build_deps = [
{ file = "build.sh" } | Local,
node-lts, # node + npm to install the package
],
runtime_deps = [
base,
glibc,
coreutils, # `capy` shebang resolves node via /usr/bin/env
ca-certificates, # HTTPS to the Capy API (sync / co-decrypt)
git, # capy shells out to git for repo/branch detection
node-lts, # capy is a Node CLI (#!/usr/bin/env node)
],

cmd = "./build.sh",
build_args = {
include version,
},

# npm fetches @capysc/cli + its deps from the registry during the build.
# Follow-up: vendor the tarball + transitive deps as hash-pinned Sources for a
# fully hermetic / SLSA build.
needs = { dns = {}, internet = {} },

outputs = {
capy = { glob = "usr/bin/capy" } | OutputBin,
node_modules = { glob = "usr/lib/node_modules/**" } | OutputData,
},
attrs =
{
upstream_version = version,
source_provenance = {
category = 'GithubRepo,
owner = "capysc",
repo = "capy-cli",
},
# 1a shared-session model: pinhole the host ~/.capy into the box so in-box
# capy reuses the developer's existing login (no key transport). rw because
# capy refreshes its session and writes caches. Exposes the full host
# session to the box: trusted dev shells only, never untrusted agent tasks.
env_dir_mappings = [{ read_only = false, path = "~/.capy", class = 'Credential }],
} | Attrs,

tests = {
runs =
{
class = 'Standalone,
test_deps = [base, node-lts],
cmds = [
# capy must run and report its version with no auth/network/config.
["/bin/bash", "-c", "capy --version | grep -q '%{version}'"],
],
} | Test,
},
} | BuildSpec
4 changes: 4 additions & 0 deletions packages/capy/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
set -ex

npm install -g --prefix=$OUTPUT_DIR/usr @capysc/cli@$MINIMAL_ARG_VERSION
28 changes: 28 additions & 0 deletions packages/microvm-rootfs/build.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
let { BuildSpec, Local, OutputData, .. } = import "minimal.ncl" in
let base = import "../base/build.ncl" in
let e2fsprogs = import "../e2fsprogs/build.ncl" in
let socat = import "../socat/build.ncl" in
{
name = "microvm-rootfs",

# A read-only ext4 guest rootfs for a libkrun microVM. build.sh snapshots the
# runtime userland (base + socat), drops in a small bring-up init, prunes
# build-only bulk, and packs an ext4 image to load as a virtio-blk block
# device. e2fsprogs is build-only — it provides mke2fs to pack the image — and
# its files are removed from the tree before packing, so the runtime image
# ships only the runtime closure.
runtime_deps = [
base,
socat,
],
build_deps = [
{ file = "build.sh" } | Local,
e2fsprogs,
],

cmd = "./build.sh",

outputs = {
rootfs = { glob = "usr/share/microvm-rootfs/rootfs.img" } | OutputData,
},
} | BuildSpec
122 changes: 122 additions & 0 deletions packages/microvm-rootfs/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/bin/bash
# Assemble a libkrun microVM guest rootfs as a read-only ext4 image.
#
# The build sandbox hardlinks this package's runtime closure (base + socat +
# their libs) and its build-only deps (e2fsprogs, for mke2fs) into the sandbox
# root at standard paths. We snapshot the userland into a staging tree, drop the
# build-only e2fsprogs files, add a small bring-up init, prune bulk, and pack an
# ext4 image with mke2fs. The image is loaded as a virtio-blk block device (e.g.
# root=/dev/vda); a block root has no overlaid init, so the kernel runs the init
# below directly. devtmpfs auto-mounts /dev, giving the init /dev/vsock.
set -euo pipefail

STAGE="$(pwd)/stage"
rm -rf "$STAGE"
mkdir -p "$STAGE"

# Snapshot the runtime userland composed into this build sandbox.
for d in usr bin sbin lib lib64 etc; do
if [ -e "/$d" ]; then
cp -a "/$d" "$STAGE/"
fi
done

# e2fsprogs is a build-only dependency: it provides mke2fs to pack the image
# below (invoked from the build sandbox PATH, not from $STAGE), but the guest
# never needs it at runtime. Drop its staged files so the runtime image carries
# only the runtime closure. Done before the init is created, so nothing we ship
# is at risk. The symlink guard skips a usr-merged /usr/sbin.
if [ -d "$STAGE/usr/sbin" ] && [ ! -L "$STAGE/usr/sbin" ]; then
rm -rf "$STAGE/usr/sbin"
fi
rm -f "$STAGE"/usr/bin/chattr "$STAGE"/usr/bin/lsattr "$STAGE"/usr/bin/uuidgen \
"$STAGE"/usr/bin/compile_et "$STAGE"/usr/bin/mk_cmds
rm -f "$STAGE"/usr/lib/libext2fs*.so* "$STAGE"/usr/lib/libe2p*.so* "$STAGE"/usr/lib/libss*.so*

mkdir -p "$STAGE/bin" "$STAGE/sbin" "$STAGE/etc/microvm"

# Kernel mountpoints. devtmpfs auto-mounts on /dev at boot (CONFIG_DEVTMPFS_MOUNT)
# — without the directory it fails with "devtmpfs: error mounting -2" and the
# guest has no /dev/vsock node. /proc and /sys are conventional mountpoints.
mkdir -p "$STAGE/dev" "$STAGE/proc" "$STAGE/sys" "$STAGE/run" "$STAGE/tmp"
chmod 1777 "$STAGE/tmp"

# Guarantee /bin/sh for the init script's shebang.
if [ ! -e "$STAGE/bin/sh" ]; then
if [ -e "$STAGE/bin/bash" ]; then
ln -sf bash "$STAGE/bin/sh"
elif [ -e "$STAGE/usr/bin/bash" ]; then
ln -sf ../usr/bin/bash "$STAGE/bin/sh"
fi
fi

# Bring-up init: signal readiness by connecting out to the host (vsock CID 2)
# port 7350 and writing "READY\n", then serve an echo on vsock port 2222 for
# host<->guest connectivity checks. Retry the marker briefly in case the vsock
# device is not live the instant init starts.
cat > "$STAGE/sbin/microvm-init" <<'INIT'
#!/bin/sh
i=0
while [ "$i" -lt 50 ]; do
printf 'READY\n' | socat -t2 - VSOCK-CONNECT:2:7350 && break
i=$((i + 1))
sleep 0.1
done
# Fail loudly if the READY handshake never succeeded, rather than starting the
# listener anyway and turning a boot failure into a downstream timeout.
[ "$i" -lt 50 ] || {
echo "microvm-init: failed to publish READY on vsock 7350" >&2
exit 1
}
exec socat VSOCK-LISTEN:2222,fork EXEC:cat
INIT
chmod +x "$STAGE/sbin/microvm-init"

# Machine-readable record of the bring-up contract.
cat > "$STAGE/etc/microvm/manifest" <<'MANIFEST'
# microvm guest rootfs contract
# format=ext4-block-image
# init=/sbin/microvm-init
# vsock_port_ready=7350 guest CONNECTs out (host listen=false); writes "READY\n" once
# vsock_port_echo=2222 guest LISTENs (host listen=true); echoes per connection
# net=none
MANIFEST

# Prune build-time-only bulk the guest never needs: headers, static libs,
# docs/man, and especially glibc's locale archive (the bulk of the closure).
# The bring-up workload is sh + socat; the C locale fallback is sufficient.
# `|| true` is scoped to `find` only — a failure in `cd` or `rm -rf` must still
# fail the build (set -euo pipefail), while `find`'s noncritical errors are ok.
( cd "$STAGE" && \
rm -rf usr/include usr/share/man usr/share/doc usr/share/info \
usr/share/locale usr/share/i18n usr/lib/locale usr/lib/pkgconfig \
usr/share/aclocal usr/share/gtk-doc usr/share/bash-completion \
usr/share/gdb && \
{ find . \( -name '*.a' -o -name '*.la' -o -name '*.o' \) -delete 2>/dev/null || true; } )

# Fail loudly (not silently with an empty output) if the image tool is absent.
command -v mke2fs >/dev/null || {
echo "ERROR: mke2fs not found on PATH ($PATH) — is e2fsprogs in build_deps?" >&2
exit 1
}

# Pack the staging tree into a raw ext4 image (mke2fs -d copies the tree).
# Size = tree + 10% + 8 MiB headroom, in 1 KiB blocks. The headroom must cover
# ext4 metadata (inode tables) that `du` does not account for, or `mke2fs -d`
# can fail to fit the tree.
OUT="$OUTPUT_DIR/usr/share/microvm-rootfs"
mkdir -p "$OUT"
KB="$(du -sk "$STAGE" | cut -f1)"
BLOCKS=$(( KB + KB / 10 + 8192 ))
# No journal (`-O ^has_journal`): the root mounts read-only, so the journal is
# pure overhead and its ~4 MiB+ reservation can overflow a tight image.
mke2fs -q -t ext4 -O ^has_journal -d "$STAGE" -b 1024 -F "$OUT/rootfs.img" "$BLOCKS"

# Assert the output exists so a silent mke2fs failure surfaces here rather than
# downstream as a missing materialize output.
[ -s "$OUT/rootfs.img" ] || {
echo "ERROR: mke2fs did not produce $OUT/rootfs.img" >&2
ls -la "$OUT" >&2 || true
exit 1
}
echo "built rootfs.img: $(wc -c < "$OUT/rootfs.img") bytes"
34 changes: 34 additions & 0 deletions packages/virtio-kernel-raw/build.ncl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
let { Attrs, BuildSpec, Local, OutputData, .. } = import "minimal.ncl" in
let base = import "../base/build.ncl" in
let virtio-linux = import "../virtio-linux/build.ncl" in

# Tracks the virtio-linux kernel version; this package only decompresses it.
let version = "6.12.43" in
{
name = "virtio-kernel-raw",

# Produce a raw, directly-loadable kernel Image from the virtio-linux kernel.
# On aarch64 that is a gzip-compressed Image (Image.gz); decompress it so
# libkrun can load it via KRUN_KERNEL_FORMAT_RAW, skipping its in-VMM gzip
# decompress for a faster microVM boot. On x86_64 it is already a bzImage and
# passes through unchanged. base provides gzip + sh at build time.
build_deps = [
{ file = "build.sh" } | Local,
base,
virtio-linux,
],

cmd = "./build.sh",
build_args = {
include version,
},

outputs = {
image = { glob = "usr/share/virtio-linux/Image" } | OutputData,
},

attrs =
{
upstream_version = version,
} | Attrs,
} | BuildSpec
16 changes: 16 additions & 0 deletions packages/virtio-kernel-raw/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
# Produce a raw, directly-loadable kernel Image from virtio-linux. virtio-linux
# is a build_dep, so its vmlinuz output is present in the sandbox at
# /usr/share/virtio-linux/vmlinuz. On aarch64 that is a gzip-compressed Image
# (Image.gz); decompress it so libkrun can load it via KRUN_KERNEL_FORMAT_RAW
# without an in-VMM gzip step. On x86_64 it is already a bzImage (not gzip), so
# pass it through unchanged.
set -euo pipefail

OUT="$OUTPUT_DIR/usr/share/virtio-linux"
mkdir -p "$OUT"
if gzip -t /usr/share/virtio-linux/vmlinuz 2>/dev/null; then
gunzip -c /usr/share/virtio-linux/vmlinuz > "$OUT/Image"
else
cp /usr/share/virtio-linux/vmlinuz "$OUT/Image"
fi