8000 miniosv/nix at agentic · TUM-DSE/miniosv · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
 
 

nix

README.md

nix/

Nix layer for miniOSv. Wraps the kernel Makefile build in reproducible derivations, wires each user application into a full matrix of image / QEMU / AWS-deploy outputs, and pins the LLVM toolchain sub-builds so they're only rebuilt when their own inputs change.

Layout

File Purpose
default.nix Per-system entry point. Turns flake.nix inputs into packages, apps, devShells. Owns the arches table and the app × arch cross-product.
miniosv.nix Builds the unikernel image (loader.img / loader.efi / loader.elf) for one (app, arch) pair. Stages appSrc into app/, then runs the top-level Makefile.
compiler-rt.nix Provides libclang_rt.builtins.a. Native: repackages the clang resource-dir. Cross: builds from the pinned llvm-project.
llvm-libc.nix Builds llvm-libc as a no-syscall static libc.a / libm.a.
libcxx.nix Builds libc++ / libc++abi / libunwind static archives + v1 headers.
toolchain.nix Assembles the clang / lld / llvm-* tools the kernel build needs.
llvm-source.nix Pins the llvm-project sparse checkout shared by the three toolchain builds.
run.nix Shell wrapper around scripts/run.py: exports OVMF/AAVMF firmware, stages a writable boot-disk copy, boots QEMU.
aws-deploy.nix Shell wrapper around scripts/aws-deploy.py with Python + AWS deps pinned.
devshells.nix nix develop environments (default, aws, cli).
cwd-sentinel/ Placeholder source for the overridable cwd app slot (see below).

Outputs

Per system (x86_64-linux, aarch64-linux), the flake exposes:

packages.<app>-<arch>              # loader.img (build only)
packages.default                   # = packages.default-<host arch>
apps.<app>-<arch>                  # QEMU boot wrapper
apps.default                       # = apps.default-<host arch>
apps.aws-deploy-<app>-<arch>       # AWS deploy wrapper
devShells.{default,aws,cli}

Where <app> is any key from the apps attrset in flake.nix (default, cwd, and whatever a downstream flake adds) and <arch> is x86_64 or aarch64.

There is exactly one alias: packages.default and apps.default point at the in-tree app (../app/) built for the host arch, so plain nix build . and nix run . do the obvious thing. Every other target names both an app and an arch explicitly, and only things in apps.* are runnable via nix run.

Common commands

# Build an image (no run):
nix build .                           # in-tree app, host arch
nix build .#default-x86_64
nix build .#default-aarch64           # cross-compiled on any host

# Boot under QEMU (KVM when target == host, else TCG):
nix run .                             # in-tree app, host arch
nix run .#default-x86_64
nix run .#default-aarch64             # TCG on x86 host: pass -cpu cortex-a72 -c 1 for a sane boot

# Deploy on AWS (see scripts/aws-deploy.py --help):
nix run .#aws-deploy-default-x86_64 -- eu-north-1 c5.large --attach
nix run .#aws-deploy-default-aarch64  -- eu-north-1 c7g.large --attach

# Enter a dev shell (all toolchain + qemu on $PATH):
nix develop           # default
nix develop .#cli     # + bear / clang-tools / pyright / black
nix develop .#aws     # + awscli2 + python awscrt/boto3

Firmware

The QEMU wrapper always exports the right UEFI firmware — OVMF_* for x86_64 targets, AAVMF_* for aarch64 — pinned to nixpkgs. For cross runs (target arch ≠ host arch) it goes through pkgs.pkgsCross.<triple>.OVMF, which is available on cache.nixos.org so no local edk2 build is needed.

Adding a new app

flake.nix owns the app registry. Two-line change:

inputs.my-app = {
  url    = "github:me/my-app/main";  # or "path:./my-app"
  flake  = false;
};

# in outputs:
apps = {
  default = ./app;
  my-app = my-app;                   # <-- new
  cwd = cwd;
};

Each app source must be miniOSv-compliant: it needs a top-level Makefile that the kernel build includes via include app/Makefile (declaring app-objects = ...) and a C/C++ entry point extern "C" void osv_app_main(). See ../app/ for the minimum viable shape.

Rebuilding: after nix flake lock --update-input my-app, the full matrix of packages.my-app-{x86_64,aarch64} and the two matching apps.* entries appears automatically.

The registry ships no applications of its own beyond default and cwd. Ports live in their own repositories and are registered by the flake that wants them, so nothing here pins a tree it does not build.

Build options

An entry may be an attrset instead of a bare source when the application needs arguments passed to the kernel make:

apps = {
  default = ./app;
  my-app = {
    src = my-app;
    makeFlags = [ "conf_vaccel=0" ];
  };
};

makeFlags is appended to the make command line for every arch, so it reaches any variable the top-level Makefile reads (conf_*, mode=debug). Register the same source twice under different names to get both variants as separate outputs. A bare source is shorthand for makeFlags = [ ].

The cwd slot (build from your working directory)

flake.nix declares a cwd input with a sentinel default. Nothing to edit — just override at CLI time or from a downstream flake.

# From a checkout of the miniOSv repo:
nix build --override-input cwd "path:$PWD/my-app" .#cwd-x86_64

# From anywhere, against the remote flake:
nix build --override-input cwd "path:$PWD" github:seb711/miniosv#cwd-x86_64

# From another flake:
inputs.miniosv.url = "github:seb711/miniosv";
inputs.miniosv.inputs.cwd.url = "path:./my-app";

Building .#cwd-* without an override fails at build time with a message explaining exactly what to type. nix flake check still passes — the sentinel is a build-time failure, not an eval-time one, so the outputs don't disappear from nix flake show.

Adding a new target arch

One row in the arches table in default.nix:

arches = {
  x64     = { linuxName = "x86_64";  qemuName = "x86_64";  firmwarePrefix = "OVMF";  crossPkgs = pkgs.pkgsCross.gnu64; };
  aarch64 = { linuxName = "aarch64"; qemuName = "aarch64"; firmwarePrefix = "AAVMF"; crossPkgs = pkgs.pkgsCross.aarch64-multiplatform; };
  # riscv64 = { ... };
};

Everything downstream (packages.*, apps.*, apps.aws-deploy-*) picks it up. Actually building the image still requires the kernel Makefile and scripts/build-*.sh to know about the arch — that's a separate concern from the nix wrapping.

Toolchain caching

compiler-rt, llvm-libc, and libcxx are per-arch, not per-app. Every packages.<app>-<arch> for a given arch consumes the same three derivations by store hash — so switching apps or editing app sources never rebuilds them. Confirm with:

for app in default cwd; do
  echo "=== $app ==="
  nix derivation show .#$app-x86_64 | \
    jq -r '.[].inputs.drvs | keys[]' | \
    grep -E 'miniosv-(compiler-rt|llvm-libc|libcxx)'
done
# Same three .drv paths under both apps.

The three toolchain derivations also use a narrowed source (toolchainSrc in default.nix) covering only scripts/, external/llvm-libc-config/, and include/api/ — the paths their build scripts actually read. So editing README.md, flake.nix, wrappers in nix/, or any app tree does not invalidate them. Only edits to a whitelisted path trigger a rebuild.

The image derivation uses kernelSrc (the repo minus app/), so editing the in-tree app rebuilds packages.default-* only — not the other apps.

Debugging what will be built

nix flake show                          # all outputs
nix flake check --no-build              # eval only
nix path-info .#default-x86_64   # store path of the image
nix derivation show .#default-x86_64 | jq '.[].inputs.drvs | keys'
0