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
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,65 @@ on:
env:
CARGO_TERM_COLOR: always

permissions:
contents: read

jobs:
# runs first: builds the write-head binaries and leaves them as artifacts
# for later fls jobs (add `needs: write-head` + actions/download-artifact
# in a job that wants them)
write-head:
name: Write head (fls-wh)
runs-on: ubuntu-latest
Comment thread
coderabbitai[bot] marked this conversation as resolved.
steps:
- uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

# build the QNX cross binary in the fedora container (remote/ has its
# own Containerfile with the cross toolchain)
- name: Build in fedora container (podman)
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq podman
podman build -t fls-wh-builder remote/
podman run --rm -v "$GITHUB_WORKSPACE":/src -w /src fls-wh-builder \
make -C remote LD=aarch64-linux-gnu-ld STUB_LD=aarch64-linux-gnu-ld

# the QNX binary can't run on a linux runner, so verify the artifact:
# aarch64 ELF, QNX interpreter, correct SONAME and NEEDED entries
- name: Verify artifact
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq binutils-aarch64-linux-gnu
file remote/flswh-aarch64-qnx7 | grep -q 'ARM aarch64'
file remote/flswh-aarch64-qnx7 | grep -q '/usr/lib/ldqnx-64.so.2'
aarch64-linux-gnu-readelf -d remote/flswh-aarch64-qnx7 | grep -q 'NEEDED.*libc.so.5'
aarch64-linux-gnu-readelf -d remote/stubs/libc.min.so_stub.so | grep -q 'SONAME.*libc.so.5'

# runtime test: build the same source natively and drive it with a
# framed command script against a temp file; assert records + bytes
- name: Native test
run: python3 remote/test.py

# build the native (runner) binary too, kept for later fls testing
- name: Build native
run: make -C remote native

- name: Upload flswh-aarch64-qnx7
uses: actions/upload-artifact@v4
with:
name: flswh-aarch64-qnx7
path: remote/flswh-aarch64-qnx7
if-no-files-found: error

- name: Upload flswh-x86_64-linux
uses: actions/upload-artifact@v4
with:
name: flswh-x86_64-linux
path: remote/flswh-x86_64-linux
if-no-files-found: error

lint:
name: Lint
runs-on: ubuntu-latest
Expand Down Expand Up @@ -152,3 +210,5 @@ jobs:
path: ${{ matrix.artifact_name }}
if-no-files-found: error



13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Makefile for fls - ARM64 static builds

.PHONY: all build clean test arm64-static lint fmt clippy check help
.PHONY: all build clean test arm64-static remote-wh remote-wh-native lint fmt clippy check help

# Default target
all: build
Expand All @@ -24,6 +24,14 @@ arm64-static:
all-targets:
./build.sh

# Build the remote write head (fls/remote) for QNX aarch64
remote-wh:
$(MAKE) -C remote

# Build the remote write head for the host (native)
remote-wh-native:
$(MAKE) -C remote native

# Run tests
test:
cargo test
Expand Down Expand Up @@ -55,6 +63,7 @@ check:
clean:
cargo clean
rm -f fls-*
$(MAKE) -C remote clean

# Install dependencies for ARM64 cross-compilation
install-deps:
Expand All @@ -72,6 +81,8 @@ help:
@echo " build - Build for native target"
@echo " arm64-static - Build for ARM64 static (musl)"
@echo " all-targets - Build for native + ARM64 static"
@echo " remote-wh - Build the remote write head for QNX aarch64"
@echo " remote-wh-native - Build the remote write head for the host"
@echo ""
@echo "Testing and Quality:"
@echo " test - Run tests"
Expand Down
3 changes: 3 additions & 0 deletions remote/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.o
flswh-*
stubs/*.so_stub.so*
12 changes: 12 additions & 0 deletions remote/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM fedora:latest

# gcc: native compiler (runs the native test; system headers)
# gcc-aarch64-linux-gnu: cross compiler (provides aarch64-linux-gnu-gcc)
# binutils-aarch64-linux-gnu: GNU ld (provides aarch64-linux-gnu-ld)
# python3: runs stubs/so_stub.py; make: the build itself
RUN dnf install -y gcc gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu python3 make \
&& dnf clean all

# The Makefile and so_stub.py hardcode the QNX-style tool name; map it
# to Fedora's. (ld.lld is already named correctly by the lld package.)
RUN ln -s /usr/bin/aarch64-linux-gnu-gcc /usr/local/bin/aarch64-elf-gcc
76 changes: 76 additions & 0 deletions remote/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# fls-wh — a block-device write head, built for a QNX aarch64 target.
#
# Built from:
# - this source (fls-wh.c, start.S)
# - standard C/POSIX headers (include/)
# - a minimal JSON symbol manifest (stubs/*.min.json) <- source of truth
# - open tooling: aarch64-elf-gcc (GCC) + ld.lld (LLVM) + stubs/so_stub.py
#
# The stub .so files are build artifacts (gitignored). At runtime, the
# target's dynamic linker resolves the SONAME (libc.so.5) to the library
# present on the system.
#
# Container build: `make container-build` builds the Containerfile with
# podman and runs the whole build inside it (fedora + cross toolchain).
#
# `make help` prints this file's `##`-annotated targets, grouped by
# `##@` section headers.

CC := aarch64-elf-gcc
LD := ld.lld
CFLAGS := -I include
LDFLAGS := -dynamic-linker /usr/lib/ldqnx-64.so.2
STUBDIR := stubs

CONTAINER ?= podman
IMAGE ?= fls-wh-builder

# host arch/os for the native binary name (flswh-<arch>-<os>)
UNAME_M := $(shell uname -m)
UNAME_S := $(shell uname -s)
NATIVE_ARCH := $(if $(filter arm64,$(UNAME_M)),aarch64,$(UNAME_M))
NATIVE_OS := $(if $(filter Darwin,$(UNAME_S)),darwin,$(if $(filter Linux,$(UNAME_S)),linux,$(UNAME_S)))
NATIVE_BIN := flswh-$(NATIVE_ARCH)-$(NATIVE_OS)

.PHONY: all clean stubs native container container-build test help

##@ build
all: flswh-aarch64-qnx7 ## build fls-wh for QNX aarch64 (default)

flswh-aarch64-qnx7: fls-wh.o start.o stubs ## link the QNX aarch64 binary
$(LD) -o $@ fls-wh.o start.o -L$(STUBDIR) -l:libc.min.so_stub.so $(LDFLAGS)

native: $(NATIVE_BIN) ## build fls-wh for the host (system compiler, no stubs)

$(NATIVE_BIN): fls-wh.c
cc -O2 $< -o $@

# build the minimal stub .so from the minimal JSON (artifacts, gitignored)
stubs: ## build the stub .so files from the JSON manifests
python3 stubs/so_stub.py $(STUBDIR)/libc.min.json -o $(STUBDIR)/libc.min.so_stub.so

fls-wh.o: fls-wh.c
$(CC) $(CFLAGS) -c $< -o $@

start.o: start.S
$(CC) -c $< -o $@

clean: ## remove all build artifacts
rm -f flswh-* fls-wh.o start.o $(STUBDIR)/*.so_stub.so*

##@ test
test: ## native test: build with the system compiler, run, assert
python3 test.py

##@ container (podman)
container: ## build the fedora container image
$(CONTAINER) build -t $(IMAGE) .

container-build: container ## build fls-wh inside the container (GNU ld)
$(CONTAINER) run --rm -v $(PWD):/src -w /src $(IMAGE) make \
LD=aarch64-linux-gnu-ld STUB_LD=aarch64-linux-gnu-ld

help: ## show this help
@awk 'BEGIN {FS = ":.*##"} \
/^##@/ { printf "\n\033[1;33m%s\033[0m\n", substr($$0, 5) } \
/^[a-zA-Z0-9_-]+:.*##/ { printf " \033[36m%-18s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
63 changes: 63 additions & 0 deletions remote/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# fls-wh — the remote write head

`fls-wh` is a small C program that runs on a remote system (QNX today;
extensible to others, e.g. a U-Boot command talking over the network) and
writes framed commands to a raw block device (e.g. `/dev/emmc0`). It is the
remote counterpart of the local writer in `src/fls/block_writer.rs` (which
uses `O_DIRECT` on Linux/macOS). QNX has no `O_DIRECT`, so durability comes
from an explicit `SYNC` (`fdatasync`; `fsync` on macOS, which has no
`fdatasync`).

## Usage

```
./flswh-<arch>-<os> <device>
```

Opens `<device>` (`O_RDWR`), reports its size (`READY`), then reads framed
commands from stdin and writes them to the device, emitting framed records to
stdout. Intended to be driven by a remote client (e.g. over SSH). QUIT or
EOF exits.
Device I/O errors are reported (`ERR`), after which the write head exits;
the client treats any `ERR` as fatal.

## Protocol

Frame (both directions): `[magic:4][opcode:1][payload_len:u32 LE][payload]`,
little-endian throughout. The magic (`FLSW`) is a sync header: the reader
scans for it to resync after any corruption.

| | opcode | | opcode |
|---|---|---|---|
| **Requests** | `DATA 0x01` `SEEK 0x03` `SYNC 0x04` `QUIT 0x05` `READ 0x06` `ZERO 0x09` `SIZE 0x0a` | **Responses** | `READY 0x81` `OK 0x82` `PROG 0x83` `ERR 0x84` `DONE 0x85` `READ_DATA 0x86` `SIZE 0x89` |
| **Reserved** | `AUTH 0x07` `HELLO 0x08` | | `AUTH_RESULT 0x87` `HELLO 0x88` |

The `READY` payload is `version:u8 + size:u64` (total device bytes, measured
at open); the protocol version is currently 1. `DATA` is
`size:u32 + content + crc:u32` (CRC32-IEEE, capped at 8 MiB); the payload is
staged and CRC-validated before the device is written, so a corrupt chunk
leaves no bytes behind.
`READ` is `len:u64` (reads from the current offset, advances it, capped at
1 MiB) and returns `READ_DATA` with the bytes. `ZERO` is `len:u64` — writes
that many zeros from the current offset and advances it, sending only the 8
length bytes over the wire (for filling regions without transferring bytes).

`SIZE` has no request payload and returns `SIZE 0x89` with `size:u64` (the
same total device bytes measured at open), without changing the cursor. If
the size could not be determined, it returns `ERR` with the original errno;
the startup `READY` uses zero for unknown size.
Before writing an image, the client can use `READY` or `SIZE` to check
`image_size <= device_size - write_offset` after checking that
`write_offset <= device_size`. For sparse images, compare the expanded size.

## Build

Binaries are named `flswh-<arch>-<os>` (matching the `fls` artifact scheme):

- **QNX aarch64** (cross): `make` (or `make -C remote` from the repo root) → `flswh-aarch64-qnx7`. Needs an aarch64 cross gcc toolchain (the Makefile's `aarch64-elf-gcc` + `ld.lld` names map to any gcc cross toolchain; `make container-build` uses Fedora's in a container).
- **Native** (host, incl. macOS): `make native` → `flswh-<arch>-<os>` (e.g. `flswh-aarch64-darwin`, `flswh-x86_64-linux`). Uses the system compiler and system headers, no stubs.

## Test

`python3 test.py` — builds the source with the system compiler, drives it with
a framed command script against a temp file, and asserts the records and bytes.
Loading
Loading