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
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,31 @@ jobs:
# beyond the build.
- run: cargo xtask container

demo:
name: the README's demo
runs-on: ubuntu-latest
needs: check
steps:
- uses: actions/checkout@v4
- run: rustup target add x86_64-unknown-linux-musl
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cpio

- name: Cache the boot artifacts
uses: actions/cache@v4
with:
path: target/busybox-x86_64
key: alpine-v3.22-x86_64-shell

- run: cargo xtask fetch --arch x86_64

# Every command the README's opening section tells a newcomer to type.
# The demo is the first thing anybody sees, so it is the worst thing to
# let rot — and M13 already found what happens to an artifact nothing
# runs. A README that tells someone to type something that no longer
# works is worse than a README with nothing in it.
- run: cargo xtask test-demo

distro:
name: a real userspace
runs-on: ubuntu-latest
Expand Down
159 changes: 159 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Cut by pushing a tag:
#
# git tag -a v0.1.0 -m "…" && git push origin v0.1.0
#
# Nothing here runs on its own. A version number is a claim about stability,
# and the person making that claim should be the one typing it.
name: Release

on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Tag to build, for a dry run against an existing tag"
required: true

permissions:
contents: write
packages: write

env:
CARGO_TERM_COLOR: always

jobs:
binaries:
name: ${{ matrix.arch }} binaries
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
steps:
- uses: actions/checkout@v4
- run: rustup target add ${{ matrix.arch }}-unknown-linux-musl
- uses: Swatinem/rust-cache@v2

- name: Build
run: |
cargo build --release --target ${{ matrix.arch }}-unknown-linux-musl \
-p oxinit -p oxctl -p oxlogd

# Statically linked, so the tarball is the whole dependency: an init
# system that needed a loader from the filesystem it is about to mount
# would be a poor one.
- name: Pack
run: |
DIR=oxinit-${{ github.ref_name }}-${{ matrix.arch }}-linux-musl
mkdir -p "$DIR"
cp target/${{ matrix.arch }}-unknown-linux-musl/release/{oxinit,oxctl,oxlogd} "$DIR/"
cp README.md LICENSE-MIT LICENSE-APACHE docs/UNIT_FORMAT.md "$DIR/"
tar czf "$DIR.tar.gz" "$DIR"
sha256sum "$DIR.tar.gz" > "$DIR.tar.gz.sha256"
file "$DIR/oxinit"

- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.arch }}
path: |
*.tar.gz
*.tar.gz.sha256

image:
name: demo image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: rustup target add x86_64-unknown-linux-musl
- uses: Swatinem/rust-cache@v2
- run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cpio

- name: Cache the boot artifacts
uses: actions/cache@v4
with:
path: target/busybox-x86_64
key: alpine-v3.22-x86_64-shell

- run: cargo xtask fetch --arch x86_64

# The image the README's first section tells people to run. Built from
# demo/, not units/ — the latter is the test suite and contains services
# that fail on purpose.
- name: Build the demo image
run: cargo xtask demo --build-only --tag ghcr.io/${{ github.repository_owner }}/oxinit:demo

- name: Log in to ghcr.io
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

- name: Push
run: |
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
docker tag ghcr.io/${{ github.repository_owner }}/oxinit:demo ghcr.io/$OWNER/oxinit:demo
docker tag ghcr.io/$OWNER/oxinit:demo ghcr.io/$OWNER/oxinit:${{ github.ref_name }}
docker push ghcr.io/$OWNER/oxinit:demo
docker push ghcr.io/$OWNER/oxinit:${{ github.ref_name }}

release:
name: publish
runs-on: ubuntu-latest
needs: [binaries, image]
if: startsWith(github.ref, 'refs/tags/')
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/download-artifact@v4
with:
merge-multiple: true

# `--generate-notes` writes the pull requests merged since the previous
# tag. The milestone titles are the changelog, which is the reason PR
# titles in this repository are written as sentences.
- name: Create the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cat > header.md <<'EOF'
**Pre-alpha.** Nothing is stable, and the unit format may change
between any two releases.

### Try it

```bash
docker run --rm --name oxinit-demo -p 8080:8080 ghcr.io/OWNER/oxinit:TAG
```

### Install

Download the tarball for your architecture below, check it, and put
the binaries somewhere on the target system. They are statically
linked against musl and need nothing else.

```bash
sha256sum -c oxinit-TAG-x86_64-linux-musl.tar.gz.sha256
tar xzf oxinit-TAG-x86_64-linux-musl.tar.gz
```

`oxinit` is the supervisor and belongs at `/sbin/init`; `oxctl` is
the client and `oxlogd` the log writer. Units go in
`/etc/oxinit/units/`, and every key is specified in
[docs/UNIT_FORMAT.md](docs/UNIT_FORMAT.md).

Verified on x86_64 and aarch64, under QEMU, in a container, and
against a real Alpine userspace. See [ROADMAP.md](ROADMAP.md) for
what each milestone was checked against.

---
EOF

OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
sed -i "s|OWNER|$OWNER|g; s|TAG|${{ github.ref_name }}|g" header.md

gh release create "${{ github.ref_name }}" \
--title "${{ github.ref_name }}" \
--notes-file header.md \
--generate-notes \
--prerelease \
*.tar.gz *.tar.gz.sha256
3 changes: 2 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,8 @@ tested without a kernel should not be in it.
3. Boots it:

```bash
qemu-system-x86_64 -kernel bzImage -initrd oxinit.cpio.gz -nographic -append "console=ttyS0"
qemu-system-x86_64 -kernel target/vmlinuz-x86_64 \
-initrd target/oxinit-x86_64.cpio.gz -nographic -append "console=ttyS0"
```

Edit to boot is a few seconds. `-nographic` puts the guest's serial console on
Expand Down
101 changes: 47 additions & 54 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,49 +22,38 @@ rustup target add aarch64-unknown-linux-musl
MSRV is stable minus two releases. No nightly features are used.

**Tools.** `qemu-system-x86_64`, `qemu-system-aarch64` and `cpio`. Docker or
podman as well, if you are running `cargo xtask container`.

**Boot artifacts.** One command per architecture:

```bash
cargo xtask fetch --arch x86_64
cargo xtask fetch --arch aarch64
```

That downloads a kernel and a statically linked busybox from Alpine into
`target/`, where `xtask` looks for them by name. `--kernel` and `--shell`
override, and so do `$OXINIT_KERNEL_<ARCH>` and `$OXINIT_SHELL_<ARCH>`, if you
would rather use a kernel you already have.
podman as well, for `cargo xtask container`, `test-distro` and `test-demo`.

```bash
# Debian/Ubuntu
sudo apt install qemu-system-x86 cpio
sudo apt install qemu-system-x86 qemu-system-arm cpio

# Fedora
sudo dnf install qemu-system-x86 cpio
sudo dnf install qemu-system-x86 qemu-system-aarch64 cpio

# Arch
sudo pacman -S qemu-system-x86 cpio
sudo pacman -S qemu-system-x86 qemu-system-aarch64 cpio
```

**Kernel.** You need a bootable `bzImage`. Any distribution kernel with cgroup
v2 and devtmpfs works; on most Linux systems the installed one is in `/boot`.
**Boot artifacts.** A kernel and a statically linked shell per architecture.
One command each:

```bash
cargo xtask boot --kernel /boot/vmlinuz-$(uname -r)
cargo xtask fetch --arch x86_64
cargo xtask fetch --arch aarch64
```

Or set it once:
That downloads them from Alpine into `target/`, where `xtask` looks for them by
name — `vmlinuz-<arch>` and `busybox-<arch>`. To use a kernel you already have
instead, `--kernel` and `--shell` override, and so do `$OXINIT_KERNEL_<ARCH>`
and `$OXINIT_SHELL_<ARCH>`. On a Linux host the installed kernel usually works:

```bash
export OXINIT_KERNEL=/path/to/bzImage
cargo xtask boot --kernel /boot/vmlinuz-$(uname -r)
```

`xtask` looks for `--kernel`, then `OXINIT_KERNEL`, then a `bzImage` in the
repository root.

The kernel must have `CONFIG_DEVTMPFS`, `CONFIG_CGROUPS` with the v2 hierarchy,
and `CONFIG_BLK_DEV_INITRD`. Kernel 5.14 or later, for `cgroup.kill`.
Any kernel needs `CONFIG_DEVTMPFS`, `CONFIG_CGROUPS` with the v2 hierarchy, and
`CONFIG_BLK_DEV_INITRD`. Version 5.14 or later, for `cgroup.kill`.

## Development loop

Expand All @@ -76,21 +65,18 @@ This builds a static `oxinit` for `x86_64-unknown-linux-musl`, packs it as
`/init` into a cpio initramfs, and boots:

```bash
qemu-system-x86_64 -kernel bzImage -initrd oxinit.cpio.gz -nographic -append "console=ttyS0"
qemu-system-x86_64 -kernel target/vmlinuz-x86_64 \
-initrd target/oxinit-x86_64.cpio.gz -nographic -append "console=ttyS0"
```

Edit to boot takes a few seconds. `-nographic` puts the guest serial console on
your terminal. `Ctrl-A X` exits QEMU. `Ctrl-C` goes to the guest, not to QEMU.

The image holds only `/init` unless you give it a shell:

```bash
cargo xtask boot --shell /path/to/busybox
```

It must be statically linked, since the image has no libraries. Without it the
boot still exercises the mounts, console, signalfd, and reap loop, but oxinit
has nothing to supervise and logs a spawn failure for `/bin/sh`.
The image holds only `/init` unless there is a shell to put in it, which is
what `cargo xtask fetch` provides. It must be statically linked, since the
image has no libraries. Without one the boot still exercises the mounts, the
console, the signalfd and the reap loop, but oxinit has nothing to supervise
and logs a spawn failure for `/bin/sh`.

Use this loop rather than reasoning about whether the boot path works. Early
boot has too many ways to fail silently.
Expand All @@ -103,16 +89,14 @@ oxinit-graph` runs on any host with a Rust toolchain, and that is where most of

Booting from macOS needs two extra pieces:

- **A cross linker.** `rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-musl` installs the
standard library but not something that can link it. Use
[`cargo-zigbuild`](https://github.com/rust-cross/cargo-zigbuild), which needs
only `zig`, or a `musl-cross` toolchain from Homebrew with the linker
configured in `.cargo/config.toml`.
- **Patience on Apple Silicon.** `qemu-system-x86_64` there is full emulation,
not virtualization, so the boot is a few seconds rather than under one. It
works. Building for `aarch64-unknown-linux-musl` and booting
`qemu-system-aarch64` is faster but is not the tested path.
- **A cross linker.** `rustup target add` installs the standard library but not
something that can link it. `.cargo/config.toml` in this repository points
both musl targets at `rust-lld`, which ships with every rustup toolchain, so
this is already handled — but that is why the file exists.
- **Patience for the foreign architecture.** Emulating the one your host is not
has no hardware acceleration behind it, and the whole boot runs through
QEMU's JIT. Both are tested — `cargo xtask test-boot --arch all` — and the
slower of the two takes about ninety seconds rather than about thirty.

A Linux VM with a distribution kernel is the shorter route if you are doing
kernel-facing work.
Expand All @@ -137,12 +121,14 @@ cargo test -p oxinit-timer # M8: schedules and the calendar
cargo xtask test-boot # M5: boots QEMU, asserts on serial
cargo xtask test-boot --arch all # M9: x86_64 and aarch64, in turn
cargo xtask container # M6: runs it in Docker, same
cargo xtask test-demo # M16: every command the README gives
cargo xtask test-distro # M10: a real Alpine userspace
```

`oxinit-unit` and `oxinit-graph` will have no Linux dependencies and will run
anywhere. Parser and graph logic belongs there, with tests, rather than in
`oxinit`.
The library crates have no Linux dependencies and run anywhere. Parser, graph,
restart policy, log format and calendar arithmetic belong there, with tests,
rather than in `oxinit` — which refuses to compile off Linux and so cannot be
tested on a host at all.

`test-boot` boots with a timeout and matches expected lines in the serial log. A
test that hangs fails on the timeout instead of blocking the run.
Expand All @@ -152,6 +138,10 @@ packs the same staged root filesystem as a `FROM scratch` image, runs it, stops
it the way an operator would, and asserts on the log and the exit code.
`--privileged` adds a writable cgroupfs and the assertions that need one.

`test-demo` runs every command the README's opening section tells a newcomer to
type, against the image built from `demo/`. That README is the first thing
anybody reads, and this is what stops it quietly ceasing to be true.

## Code policy

Full rules are in [CLAUDE.md](CLAUDE.md). The ones that get PRs rejected:
Expand All @@ -165,11 +155,14 @@ in PID 1 is a kernel panic.
**No async in `crates/oxinit`.** One thread, one `epoll` loop. Other crates are
unconstrained.

**Unsafe is quarantined.** Syscalls go through `rustix`. Anything `rustix` does
not cover lives in `oxinit::sys::raw`, with a `// SAFETY:` comment per block
stating the invariant and why it holds at that call site. A PR adding `unsafe`
elsewhere will be asked to move it. A PR adding `unsafe` without a `// SAFETY:`
comment will be asked to write one.
**Unsafe is quarantined, and the build enforces it.** Syscalls go through
`rustix`. Anything `rustix` does not cover lives in `oxinit::sys::raw`, with a
`// SAFETY:` comment per block stating the invariant and why it holds at that
call site. The `oxinit` crate denies `unsafe_code` with that one module
relaxing it, and every crate that should contain none carries `forbid`, so a PR
adding `unsafe` elsewhere does not compile rather than being asked to move it.
A PR adding one to `sys::raw` without a `// SAFETY:` comment will be asked to
write one.

**Dependencies need a reason.** The dependency tree of PID 1 is part of its
attack surface. State the reason in the commit message.
Expand Down
Loading
Loading