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
83 changes: 83 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version: "1.26"
go-version-file: go.mod

- run: make

- uses: actions/upload-artifact@v4
with:
name: init-binaries
path: internal/initrd/embed/init-*

vet:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version: "1.26"
go-version-file: go.mod

- uses: actions/download-artifact@v7
with:
name: init-binaries
path: internal/initrd/embed/

- run: go vet ./...

test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version: "1.26"
go-version-file: go.mod

- uses: actions/download-artifact@v7
with:
name: init-binaries
path: internal/initrd/embed/

- run: go test ./...

gosec:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-go@v6
with:
go-version: "1.26"
go-version-file: go.mod

- uses: actions/download-artifact@v7
with:
name: init-binaries
path: internal/initrd/embed/

- name: Install gosec
run: go install github.com/securego/gosec/v2/cmd/gosec@v2.23.0

- name: Run Gosec Security Scanner
run: gosec ./...
13 changes: 11 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@ go.work.sum
# Build artifacts (wisp Makefile output)
build/

# Embedded init binary (cross-compiled by Makefile, not committed)
cmd/wisp/embed/init-arm64
# Embedded init binaries (cross-compiled by Makefile, not committed)
internal/initrd/embed/init-arm64
internal/initrd/embed/init-riscv64
internal/initrd/embed/init-amd64

# Default wisp output directory
wisp-output/

# env file
.env

# Editor/IDE
# .idea/
# .vscode/


# gosec output
results.json
39 changes: 22 additions & 17 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,24 +43,29 @@ wisp is written in Go. The only external dependency is `golang.org/x/sys/unix`
```text
wisp/
├── cmd/
│ ├── wisp/ # CLI entrypoint (host tool)
│ │ ├── main.go
│ │ ├── boards/ # Board profile JSONs (embedded via go:embed)
│ │ │ ├── pi5.json
│ │ │ └── qemu.json
│ │ └── embed/ # Pre-built init binary (embedded via go:embed)
│ │ └── init-arm64 # Cross-compiled by Makefile, not committed
│ ├── init/ # Init binary (cross-compiled, runs on target)
│ └── mkinitrd/ # Standalone initrd assembler (Makefile workflow)
│ ├── wisp/ # CLI entrypoint (thin wiring layer)
│ │ └── main.go
│ └── init/ # Init binary (cross-compiled, runs on target)
├── internal/
│ ├── board/ # Board profile types and loader
│ ├── initrd/ # Initrd assembly (cpio archive creation)
│ ├── board/ # Board profile types, loader, and embedded profiles
│ │ └── boards/ # Board profile JSONs (embedded via go:embed)
│ │ ├── pi5.json
│ │ ├── qemu.json
│ │ └── raspi3b.json
│ ├── initrd/ # Initrd assembly, init binary embedding, build logic
│ │ ├── initrd.go # Low-level cpio archive writer
│ │ ├── initbin.go # Embedded init binaries + arch lookup
│ │ ├── build.go # High-level Build() assembling complete initrd
│ │ └── embed/ # Pre-built init binaries (cross-compiled by Makefile)
│ │ ├── init-arm64
│ │ ├── init-riscv64
│ │ └── init-amd64
│ ├── image/ # Disk image assembly (FAT32 boot partition)
│ ├── fetch/ # Asset fetching and caching (kernels, firmware)
│ └── validate/ # Binary validation (static ELF, correct arch)
├── testdata/
│ └── helloworld/ # Test HTTP server for QEMU integration tests
├── Makefile # Two-step build: init binary → wisp CLI
├── Makefile # Two-step build: init binaries → wisp CLI
├── DESIGN.md # Architecture and design decisions
├── README.md
├── CLAUDE.md # This file
Expand Down Expand Up @@ -93,8 +98,8 @@ wisp validate --target pi5 --binary ./myservice
# Build everything (cross-compile init, build wisp CLI, build test binary)
make

# Quick QEMU test using the Makefile workflow (bypasses wisp CLI)
make qemu
# Quick QEMU test
wisp run --target qemu --binary ./build/helloworld --ip 10.0.2.15/24 --gateway 10.0.2.2

# Run all tests
go test ./...
Expand All @@ -105,8 +110,8 @@ make clean

The Makefile performs a two-step build:

1. Cross-compile `cmd/init` → `cmd/wisp/embed/init-arm64`
2. Build `cmd/wisp` (which embeds the init binary and board profiles via
1. Cross-compile `cmd/init` → `internal/initrd/embed/init-{arm64,riscv64,amd64}`
2. Build `cmd/wisp` (which embeds the init binaries and board profiles via
`go:embed`)

## Key constraints
Expand All @@ -125,7 +130,7 @@ The Makefile performs a two-step build:

## Board profiles

Board profiles are JSON files in `cmd/wisp/boards/` that define everything
Board profiles are JSON files in `internal/board/boards/` that define everything
board-specific: firmware URLs, DTB filename, kernel package source, required
modules, network interface name, architecture. Embedded into the wisp binary
via `go:embed` and parsed with `board.Parse()`.
Expand Down
15 changes: 12 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ adds scope to v1 unnecessarily.

## Target board profiles

Each supported board has a JSON profile in `cmd/wisp/boards/` that defines everything
Each supported board has a JSON profile in `internal/board/boards/` that defines everything
board-specific. Profiles are embedded into the wisp binary via `go:embed` and parsed with
`board.Parse()`. Adding a new board means adding a new JSON file — no code changes
required (ideally).
Expand All @@ -209,8 +209,9 @@ Fields in a board profile:
- **`boot_config`** — Content of `config.txt` written to the boot partition.
- **`cmdline`** — Kernel command line written to `cmdline.txt`.
- **`modules`** — List of kernel modules with `path` (in APK) and `name` (output filename).
- **`qemu`** — QEMU emulation config: `binary`, `machine`, `cpu`, `memory`, `accel`, `net_dev`, `extra`. Present only for QEMU-bootable targets.

Example (`cmd/wisp/boards/qemu.json`):
Example (`internal/board/boards/qemu.json`):

```json
{
Expand All @@ -229,7 +230,15 @@ Example (`cmd/wisp/boards/qemu.json`):
{ "path": "kernel/net/core/failover.ko.gz", "name": "failover.ko" },
{ "path": "kernel/drivers/net/net_failover.ko.gz", "name": "net_failover.ko" },
{ "path": "kernel/drivers/net/virtio_net.ko.gz", "name": "virtio_net.ko" }
]
],
"qemu": {
"binary": "qemu-system-aarch64",
"machine": "virt",
"cpu": "host",
"memory": "512M",
"accel": "hvf",
"net_dev": "virtio-net-pci"
}
}
```

Expand Down
120 changes: 21 additions & 99 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,121 +1,43 @@
BUILD_DIR := build
MODULES_DIR := $(BUILD_DIR)/modules
CONF_DIR := $(BUILD_DIR)/conf
EMBED_DIR := cmd/wisp/embed
EMBED_DIR := internal/initrd/embed

# Cross-compilation for aarch64 Linux (used for init and service binaries)
CROSS_ENV := GOOS=linux GOARCH=arm64 CGO_ENABLED=0
.PHONY: all clean wisp kill

# Alpine kernel for QEMU (v3.23, kernel 6.18 LTS)
KERNEL_PKG := linux-virt-6.18.13-r0.apk
KERNEL_URL := https://dl-cdn.alpinelinux.org/alpine/v3.23/main/aarch64/$(KERNEL_PKG)
KERNEL_VER := 6.18.13-0-virt

# Kernel modules needed for QEMU virtio networking (load order matters).
# virtio and virtio_pci are built-in; these three are modules.
QEMU_MODULES := \
lib/modules/$(KERNEL_VER)/kernel/net/core/failover.ko.gz \
lib/modules/$(KERNEL_VER)/kernel/drivers/net/net_failover.ko.gz \
lib/modules/$(KERNEL_VER)/kernel/drivers/net/virtio_net.ko.gz

# QEMU settings
QEMU_PORT := 18080

.PHONY: all clean initrd kernel qemu kill wisp

# Two-step build: cross-compile init binary, then build wisp CLI (which embeds it).
# Two-step build: cross-compile init binaries, then build wisp CLI (which embeds them).
all: wisp $(BUILD_DIR)/helloworld

# Step 1: Cross-compile the init binary into the embed directory.
# Step 1: Cross-compile init binaries for all supported architectures.
$(EMBED_DIR)/init-arm64: cmd/init/main.go
@mkdir -p $(EMBED_DIR)
$(CROSS_ENV) go build -o $@ ./cmd/init/
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o $@ ./cmd/init/

$(EMBED_DIR)/init-riscv64: cmd/init/main.go
@mkdir -p $(EMBED_DIR)
GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 go build -o $@ ./cmd/init/

$(EMBED_DIR)/init-amd64: cmd/init/main.go
@mkdir -p $(EMBED_DIR)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o $@ ./cmd/init/

# Step 2: Build the wisp CLI (embeds init binary and board profiles).
INIT_BINARIES := $(EMBED_DIR)/init-arm64 $(EMBED_DIR)/init-riscv64 $(EMBED_DIR)/init-amd64

# Step 2: Build the wisp CLI (embeds init binaries and board profiles).
wisp: $(BUILD_DIR)/wisp
$(BUILD_DIR)/wisp: $(EMBED_DIR)/init-arm64 cmd/wisp/main.go
$(BUILD_DIR)/wisp: $(INIT_BINARIES) cmd/wisp/main.go
@mkdir -p $(BUILD_DIR)
go build -o $@ ./cmd/wisp/

# Cross-compile the init binary to build/ (for manual Makefile-based workflow)
$(BUILD_DIR)/init: cmd/init/main.go
@mkdir -p $(BUILD_DIR)
$(CROSS_ENV) go build -o $@ ./cmd/init/

# Cross-compile the test helloworld binary for aarch64
$(BUILD_DIR)/helloworld: testdata/helloworld/main.go
@mkdir -p $(BUILD_DIR)
$(CROSS_ENV) go build -o $@ ./testdata/helloworld/

# Build mkinitrd for the host (not cross-compiled)
$(BUILD_DIR)/mkinitrd: cmd/mkinitrd/main.go internal/initrd/initrd.go
@mkdir -p $(BUILD_DIR)
go build -o $@ ./cmd/mkinitrd/

# Download Alpine virt kernel and extract modules (cached)
kernel: $(BUILD_DIR)/vmlinuz
$(BUILD_DIR)/vmlinuz:
@mkdir -p $(BUILD_DIR)
curl -sL -o $(BUILD_DIR)/$(KERNEL_PKG) "$(KERNEL_URL)"
cd $(BUILD_DIR) && tar -xzf $(KERNEL_PKG) boot/vmlinuz-virt $(QEMU_MODULES) 2>/dev/null
mv $(BUILD_DIR)/boot/vmlinuz-virt $@
@mkdir -p $(MODULES_DIR)
for mod in $(QEMU_MODULES); do \
gunzip -c $(BUILD_DIR)/$$mod > $(MODULES_DIR)/$$(basename $$mod .gz); \
done
rm -rf $(BUILD_DIR)/boot $(BUILD_DIR)/lib $(BUILD_DIR)/$(KERNEL_PKG)
@echo "kernel: $@"
@ls -1 $(MODULES_DIR)/

# Generate config files for QEMU
$(CONF_DIR)/wisp.conf:
@mkdir -p $(CONF_DIR)
printf 'IFACE=eth0\nADDR=10.0.2.15/24\nGW=10.0.2.2\n' > $@

$(CONF_DIR)/resolv.conf:
@mkdir -p $(CONF_DIR)
printf 'nameserver 10.0.2.3\n' > $@

$(CONF_DIR)/modules:
@mkdir -p $(CONF_DIR)
printf 'failover.ko\nnet_failover.ko\nvirtio_net.ko\n' > $@

# Assemble the initrd using our Go cpio writer (no shelling out to cpio)
initrd: $(BUILD_DIR)/initrd.img
$(BUILD_DIR)/initrd.img: $(BUILD_DIR)/mkinitrd $(BUILD_DIR)/init $(BUILD_DIR)/helloworld $(BUILD_DIR)/vmlinuz $(CONF_DIR)/wisp.conf $(CONF_DIR)/resolv.conf $(CONF_DIR)/modules
$(BUILD_DIR)/mkinitrd \
-o $@ \
-init $(BUILD_DIR)/init \
-service $(BUILD_DIR)/helloworld \
-conf $(CONF_DIR)/wisp.conf \
-resolv $(CONF_DIR)/resolv.conf \
-modules-list $(CONF_DIR)/modules \
-modules-dir $(MODULES_DIR)

# Build everything and boot in QEMU (manual Makefile workflow)
qemu: initrd kernel
@echo "Booting QEMU (port forward: localhost:$(QEMU_PORT) -> guest:8080)"
@echo "Test: curl http://localhost:$(QEMU_PORT)/"
@echo "Quit: Ctrl-A X"
@echo "---"
qemu-system-aarch64 \
-machine virt \
-accel hvf \
-cpu host \
-m 512M \
-kernel $(BUILD_DIR)/vmlinuz \
-initrd $(BUILD_DIR)/initrd.img \
-append "rdinit=/init console=ttyAMA0 net.ifnames=0 quiet" \
-nographic \
-netdev user,id=net0,hostfwd=tcp::$(QEMU_PORT)-:8080 \
-device virtio-net-pci,netdev=net0
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o $@ ./testdata/helloworld/

# Kill any running QEMU instances (by name and by port)
# Kill any running QEMU instances
kill:
@pkill -f qemu-system-aarch64 2>/dev/null || true
@lsof -ti :$(QEMU_PORT) 2>/dev/null | xargs kill -9 2>/dev/null || true
@lsof -ti :18080 2>/dev/null | xargs kill -9 2>/dev/null || true
@echo "killed"

clean:
rm -rf $(BUILD_DIR)
rm -f $(EMBED_DIR)/init-arm64
rm -f $(EMBED_DIR)/init-arm64 $(EMBED_DIR)/init-riscv64 $(EMBED_DIR)/init-amd64
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,6 @@ make
This cross-compiles the init binary for the target architecture, then builds
the wisp CLI which embeds it.

Quick QEMU test without the wisp CLI (Makefile workflow):

```sh
make qemu
```

Run tests:

```sh
Expand Down
Loading
Loading