Skip to content
Merged
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
16 changes: 13 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
51 changes: 51 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 5 additions & 2 deletions ROADMAP.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
4 changes: 4 additions & 0 deletions cairn/items/0023-liam-init-pid-1-in-the-microvm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
10 changes: 10 additions & 0 deletions crates/liam-init/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions crates/liam-init/src/main.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
23 changes: 23 additions & 0 deletions crates/liam-init/tests/cli.rs
Original file line number Diff line number Diff line change
@@ -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));
}
13 changes: 13 additions & 0 deletions crates/liamd/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
19 changes: 19 additions & 0 deletions crates/liamd/src/main.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
23 changes: 23 additions & 0 deletions crates/liamd/tests/cli.rs
Original file line number Diff line number Diff line change
@@ -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));
}
2 changes: 2 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -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"]
23 changes: 23 additions & 0 deletions scripts/task
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ usage: scripts/task <target>
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
Expand All @@ -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
Expand Down
Loading