diff --git a/packages/capy/build.ncl b/packages/capy/build.ncl new file mode 100644 index 0000000..f84bb73 --- /dev/null +++ b/packages/capy/build.ncl @@ -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 diff --git a/packages/capy/build.sh b/packages/capy/build.sh new file mode 100755 index 0000000..ac2d134 --- /dev/null +++ b/packages/capy/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -ex + +npm install -g --prefix=$OUTPUT_DIR/usr @capysc/cli@$MINIMAL_ARG_VERSION diff --git a/packages/microvm-rootfs/build.ncl b/packages/microvm-rootfs/build.ncl new file mode 100644 index 0000000..131e395 --- /dev/null +++ b/packages/microvm-rootfs/build.ncl @@ -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 diff --git a/packages/microvm-rootfs/build.sh b/packages/microvm-rootfs/build.sh new file mode 100755 index 0000000..9981d43 --- /dev/null +++ b/packages/microvm-rootfs/build.sh @@ -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" diff --git a/packages/virtio-kernel-raw/build.ncl b/packages/virtio-kernel-raw/build.ncl new file mode 100644 index 0000000..e507f27 --- /dev/null +++ b/packages/virtio-kernel-raw/build.ncl @@ -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 diff --git a/packages/virtio-kernel-raw/build.sh b/packages/virtio-kernel-raw/build.sh new file mode 100755 index 0000000..fbacd8a --- /dev/null +++ b/packages/virtio-kernel-raw/build.sh @@ -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