diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2f59ae..4280ef3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,17 +38,27 @@ jobs: cache-dependency-path: site/pnpm-lock.yaml - run: ./scripts/task check + static: + name: static + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + # musl-gcc compiles mimalloc's C for the musl target. + - run: sudo apt-get install -y --no-install-recommends musl-tools + - run: ./scripts/task build:static + # The one status check branch protection requires, so adding a job never means # editing a protection rule. required: name: required if: always() - needs: [check] + needs: [check, static] runs-on: ubuntu-latest steps: - name: Every job succeeded run: | - if [ "${{ needs.check.result }}" != "success" ]; then - echo "check: ${{ needs.check.result }}" + if [ "${{ needs.check.result }}" != "success" ] || [ "${{ needs.static.result }}" != "success" ]; then + echo "check: ${{ needs.check.result }}, static: ${{ needs.static.result }}" exit 1 fi diff --git a/Cargo.lock b/Cargo.lock index cc3e59c..67bfc14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,57 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + [[package]] name = "liam" version = "0.0.0" + +[[package]] +name = "liam-init" +version = "0.0.0" + +[[package]] +name = "liamd" +version = "0.0.0" +dependencies = [ + "mimalloc", +] + +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" diff --git a/Cargo.toml b/Cargo.toml index 0b70465..02bcaaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,12 @@ rust-version = "1.98" license = "MIT OR Apache-2.0" repository = "https://github.com/oddurs/liam" authors = ["Oddur Sigurdsson"] + +# Image binaries are built once and booted many times, so build time is spent +# freely for speed and size. +[profile.release] +opt-level = 3 +lto = "fat" +codegen-units = 1 +panic = "abort" +strip = true diff --git a/ROADMAP.md b/ROADMAP.md index 09e94d1..c337d2d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -29,13 +29,12 @@ Releases ship when their gate passes, not on a date. ## v0.1 — Static sites in a microVM -`··········` 0% · 0 of 24 done +`#·········` 4% · 1 of 24 done Serve a static site from a Firecracker microVM over HTTP/1.1, from cold boot to first byte inside the boot budget, measured in CI. For sites behind a load balancer or CDN that already terminates TLS. ### committed -- [ ] `0012` Scaffold the Cargo workspace and the task seam chore · p0 · s · infra - [ ] `0013` CI: format, lint and test every pull request chore · p0 · s · infra - [ ] `0014` How fast can a stripped Linux LTS kernel boot in Firecracker? spike · p0 · m · kernel - [ ] `0015` Which io_uring runtime does liamd build on? spike · p0 · m · io @@ -63,6 +62,10 @@ Serve a static site from a Firecracker microVM over HTTP/1.1, from cold boot to - [ ] `0035` Project microsite and design system feature · p2 · m · site +### done + +- [x] `0012` Scaffold the Cargo workspace and the task seam chore · p0 · s · infra + ## v0.2 — HTTPS and containers `··········` 0% · 0 of 15 done diff --git a/cairn/items/0012-scaffold-the-cargo-workspace-and-the-task-seam.md b/cairn/items/0012-scaffold-the-cargo-workspace-and-the-task-seam.md index 2f24a5c..5a370bf 100644 --- a/cairn/items/0012-scaffold-the-cargo-workspace-and-the-task-seam.md +++ b/cairn/items/0012-scaffold-the-cargo-workspace-and-the-task-seam.md @@ -3,10 +3,12 @@ id: 12 uid: 10481a9f-b883-4165-bd4f-a3236d394e11 title: Scaffold the Cargo workspace and the task seam type: chore -status: planned +status: done milestone: v0.1 +assignee: Oddur Sigurdsson created: 2026-09-28 updated: 2026-09-28 +closed_at: 2026-09-28 priority: p0 area: infra effort: s @@ -27,7 +29,31 @@ Nothing can be built, tested or gated until the repository has its shape. Every ## Acceptance criteria -- [ ] `scripts/task check` passes on the empty workspace. -- [ ] A deliberate clippy warning makes `scripts/task lint` exit non-zero. -- [ ] `file` reports the release `liamd` and `liam-init` binaries as statically linked. -- [ ] Both license files are present, and every crate declares `MIT OR Apache-2.0`. +- [x] `scripts/task check` passes on the empty workspace. +- [x] A deliberate clippy warning makes `scripts/task lint` exit non-zero. +- [x] `file` reports the release `liamd` and `liam-init` binaries as statically linked. +- [x] Both license files are present, and every crate declares `MIT OR Apache-2.0`. + +## 2026-09-28 + +Library crates (liam-io, liam-http, liam-pack, liam-config) are not created here: empty crates are dead scaffolding, and each has an item (0017, 0018, 0019, 0020) that creates it with real code. This item builds the two image binaries the static pipeline needs. + +## 2026-09-28 + +mimalloc is the global allocator in liamd only. liam-init barely allocates, and its size budget is 300 KB; the CLI runs on developer machines. The release profile also sets strip = true for the size budget. + +## 2026-09-28 + +Static builds are a separate verb, scripts/task build:static, run by a 'static' CI job on Linux (musl-tools provides musl-gcc for mimalloc's C). It fails unless file(1) reports each binary as statically linked or static-pie linked, and prints each binary's size. It refuses on macOS rather than half-working. + +## 2026-09-28 + +Evidence. 1: scripts/task check green on macOS. 2: a planted clippy::len_zero made scripts/task lint exit 101; removing it, 0. 4: LICENSE-MIT and LICENSE-APACHE present; cargo metadata shows liam, liam-init and liamd all MIT OR Apache-2.0. + +## 2026-09-28 + +Evidence. 3: CI run 36514087437, static job: liamd 543,400 bytes and liam-init 397,880 bytes, both 'ELF 64-bit LSB pie executable, x86-64, static-pie linked, stripped'. Static-pie is fully static: there is no interpreter. + +## Result + +The workspace has liam (CLI), liamd and liam-init. The release profile is fat LTO, one codegen unit, panic=abort, stripped. scripts/task build:static builds static-pie musl binaries and CI's static job rejects anything dynamic. Baseline sizes: liamd 543 KB, liam-init 398 KB. Library crates are created by the items that fill them (0017-0020), not here. diff --git a/cairn/items/0023-liam-init-pid-1-in-the-microvm.md b/cairn/items/0023-liam-init-pid-1-in-the-microvm.md index 9d1e917..351f882 100644 --- a/cairn/items/0023-liam-init-pid-1-in-the-microvm.md +++ b/cairn/items/0023-liam-init-pid-1-in-the-microvm.md @@ -41,3 +41,7 @@ A small static Rust binary that: - [ ] A test shows an orphaned process is reaped. - [ ] Firecracker's `SendCtrlAltDel` powers the VM off cleanly. - [ ] The stripped binary is under 300 KB. + +## 2026-09-28 + +Starting point from 0012: a liam-init that only prints its version is already 397,880 bytes (x86_64 musl, static-pie, fat LTO, stripped, panic=abort). The 300 KB criterion needs more than the shared release profile: opt-level = "z" for this crate, avoiding std::fmt, or a per-package profile. Measure with scripts/task build:static. diff --git a/crates/liam-init/Cargo.toml b/crates/liam-init/Cargo.toml new file mode 100644 index 0000000..e649c22 --- /dev/null +++ b/crates/liam-init/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "liam-init" +description = "PID 1 for liam images" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish = false diff --git a/crates/liam-init/src/main.rs b/crates/liam-init/src/main.rs new file mode 100644 index 0000000..4ac4119 --- /dev/null +++ b/crates/liam-init/src/main.rs @@ -0,0 +1,16 @@ +use std::process::ExitCode; + +// No custom allocator: PID 1 barely allocates, and every byte counts against the +// size budget. +fn main() -> ExitCode { + match std::env::args().nth(1).as_deref() { + Some("--version" | "-V") => { + println!("liam-init {}", env!("CARGO_PKG_VERSION")); + ExitCode::SUCCESS + } + _ => { + eprintln!("liam-init: nothing to start yet; see ROADMAP.md"); + ExitCode::from(1) + } + } +} diff --git a/crates/liam-init/tests/cli.rs b/crates/liam-init/tests/cli.rs new file mode 100644 index 0000000..69a9a83 --- /dev/null +++ b/crates/liam-init/tests/cli.rs @@ -0,0 +1,23 @@ +use std::process::Command; + +fn run(args: &[&str]) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_liam-init")) + .args(args) + .output() + .expect("the liam-init binary runs") +} + +#[test] +fn version_prints_the_crate_version() { + let out = run(&["--version"]); + assert!(out.status.success()); + assert_eq!( + String::from_utf8_lossy(&out.stdout).trim(), + format!("liam-init {}", env!("CARGO_PKG_VERSION")) + ); +} + +#[test] +fn without_arguments_it_refuses_to_pretend() { + assert_eq!(run(&[]).status.code(), Some(1)); +} diff --git a/crates/liamd/Cargo.toml b/crates/liamd/Cargo.toml new file mode 100644 index 0000000..86390da --- /dev/null +++ b/crates/liamd/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "liamd" +description = "The liam web server" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish = false + +[dependencies] +mimalloc = "0.1.52" diff --git a/crates/liamd/src/main.rs b/crates/liamd/src/main.rs new file mode 100644 index 0000000..ebbf84e --- /dev/null +++ b/crates/liamd/src/main.rs @@ -0,0 +1,19 @@ +use std::process::ExitCode; + +// musl's allocator serialises on a global lock, which a thread-per-core server +// would contend on constantly. +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +fn main() -> ExitCode { + match std::env::args().nth(1).as_deref() { + Some("--version" | "-V") => { + println!("liamd {}", env!("CARGO_PKG_VERSION")); + ExitCode::SUCCESS + } + _ => { + eprintln!("liamd: nothing to serve yet; see ROADMAP.md"); + ExitCode::from(1) + } + } +} diff --git a/crates/liamd/tests/cli.rs b/crates/liamd/tests/cli.rs new file mode 100644 index 0000000..a269f27 --- /dev/null +++ b/crates/liamd/tests/cli.rs @@ -0,0 +1,23 @@ +use std::process::Command; + +fn run(args: &[&str]) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_liamd")) + .args(args) + .output() + .expect("the liamd binary runs") +} + +#[test] +fn version_prints_the_crate_version() { + let out = run(&["--version"]); + assert!(out.status.success()); + assert_eq!( + String::from_utf8_lossy(&out.stdout).trim(), + format!("liamd {}", env!("CARGO_PKG_VERSION")) + ); +} + +#[test] +fn without_arguments_it_refuses_to_pretend() { + assert_eq!(run(&[]).status.code(), Some(1)); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ef662bf..fcecd53 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,5 @@ [toolchain] channel = "1.98.1" components = ["clippy", "rustfmt"] +# The images are static musl binaries; scripts/task build:static needs this. +targets = ["x86_64-unknown-linux-musl"] diff --git a/scripts/task b/scripts/task index 3f8ceb4..600b8de 100755 --- a/scripts/task +++ b/scripts/task @@ -31,6 +31,7 @@ usage: scripts/task lint lint; warnings are errors test the full test suite build compile everything, including the tests + build:static release musl binaries for the image; fails unless static (Linux) check fmt:check, lint, test, build — what CI runs USAGE @@ -56,6 +57,28 @@ case "${1:-}" in cargo build --all-targets site run --silent build ;; + build:static) + # What goes into an image: release builds for musl, which must be static + # because the image has no dynamic linker. Linux only; CI runs it there. + target=x86_64-unknown-linux-musl + if [ "$(uname -s)" != Linux ]; then + printf 'scripts/task build:static: needs Linux with musl-gcc; CI runs it\n' >&2 + exit 1 + fi + cargo build --release --locked --target "$target" -p liamd -p liam-init + for bin in liamd liam-init; do + path="target/$target/release/$bin" + kind=$(file -b "$path") + case "$kind" in + *'statically linked'* | *'static-pie linked'*) ;; + *) + printf '%s is not static: %s\n' "$bin" "$kind" >&2 + exit 1 + ;; + esac + printf '%-10s %8s bytes %s\n' "$bin" "$(wc -c <"$path" | tr -d ' ')" "$kind" + done + ;; check) step 'format' "$0" fmt:check