From ad00c4a1e9638ed32cf4498a9ea7439bc16ce4d7 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 12 Nov 2025 16:51:08 -0800 Subject: [PATCH] vibing alpine --- ALPINE_MIGRATION.md | 107 ++++++++++++++++++++++++++++ Dockerfile.alpine | 82 +++++++++++++++++++++ build_alpine.sh | 137 ++++++++++++++++++++++++++++++++++++ test_alpine_magic.sh | 20 ++++++ userspace/alpine/magic.init | 25 +++++++ 5 files changed, 371 insertions(+) create mode 100644 ALPINE_MIGRATION.md create mode 100644 Dockerfile.alpine create mode 100755 build_alpine.sh create mode 100755 test_alpine_magic.sh create mode 100644 userspace/alpine/magic.init diff --git a/ALPINE_MIGRATION.md b/ALPINE_MIGRATION.md new file mode 100644 index 0000000..9d316d7 --- /dev/null +++ b/ALPINE_MIGRATION.md @@ -0,0 +1,107 @@ +# Alpine Migration Plan (Draft 1) + +## What We Have Today +- `build_system.sh` pulls `ubuntu-base-24.04.3-base-arm64.tar.gz`, loads it into `Dockerfile.agnos`, and bakes the final `system.img`. +- `Dockerfile.agnos` and `userspace/*.sh` assume Ubuntu/Debian tooling: `apt-fast`, `dpkg`, `.deb` blobs in `userspace/debs`, and systemd units in `userspace/files`. +- `Dockerfile.builder` is an Ubuntu 20.04 helper image that mounts the workspace and provides build-essential tools. +- Services are managed exclusively with systemd (`userspace/services.sh`), and dozens of `*.service`/`*.timer` files are copied into `/lib/systemd/system`. + +The goal: swap the Ubuntu base for Alpine while keeping device functionality (openpilot, hardware services, flashing) unchanged. + +## Guiding Principles +1. **Parallelize, don’t destabilize.** Leave the Ubuntu build path untouched; all Alpine work happens in new `build_alpine.sh`, `Dockerfile.alpine`, etc., until we prove parity. +2. **Change one layer at a time.** Migrate builder tooling before touching the rootfs so failures are easy to bisect. +3. **Stay reproducible.** Every replacement must be scripted (no manual `apk add` on a live device). +4. **Prefer rebuilding from source.** Debian-only `.deb` artifacts (qtwayland5, modemmanager, etc.) should become source builds or Alpine `apk`s. +5. **Keep service semantics.** Whether we keep systemd on Alpine or port to OpenRC must be decided up front and applied consistently. + +## Phase A – Minimal Alpine Bring-up (Parallel Path) +Goal: produce a new `build_alpine.sh` that builds a tiny Alpine-based rootfs, flashes separately, boots to userspace, and runs `usr/comma/magic.py` to show the logo. Existing Ubuntu images/scripts remain the default. + +1. [x] Create `Dockerfile.alpine` that starts from the Alpine minirootfs and installs just enough to boot OpenRC and launch the logo service. +2. [x] Clone `build_system.sh` into `build_alpine.sh`. Adjust it to: + - Download `alpine-minirootfs--aarch64.tar.gz`. + - Use new Alpine docker build to produce a `system-alpine.img`. + - Keep all outputs in `output/alpine/` so it doesn’t collide with Ubuntu artifacts. +3. [ ] Inside the Alpine image: + - [x] Configure networking basics, hostname, and ensure `/usr/comma` is copied from the existing repository. + - [x] Install just enough packages (`python3`, `py3-pip`, DRM/mesa bits) plus pip-install `pyray` for `magic.py`. + - [x] Create an OpenRC service (`/etc/init.d/magic`) that launches `/usr/comma/magic.py` on boot and streams logs to `/var/log/magic.log`. + - [ ] Validate on real hardware (or QEMU once GPU path is mocked) that the logo shows and backlight powers on. +4. [x] Add a simple smoke test stub (`test_alpine_magic.sh`) describing how to exercise the image until automation exists. +5. [ ] Document how to invoke the new flow in `README.md` (later) but keep it optional until feature-complete. + +## Phase 0 – Recon & Prep +1. [ ] Pin an Alpine release (3.20 or edge) that ships `aarch64` and `armv7` repos; record mirror URLs. +2. [ ] List every Ubuntu package we install (`userspace/base_setup.sh`, `openpilot_dependencies.sh`, `install_extras.sh`, etc.) and mark whether Alpine has an equivalent `apk`, needs a community repo, or must be built from source. +3. [ ] Inventory `.deb` blobs in `userspace/debs`, `userspace/qtwayland`, compiler stages, and note which ones rely on glibc symbols. +4. [ ] Decide init strategy: + - Option A: keep systemd (build it from source on Alpine + run in PID1). + - Option B: switch to OpenRC (rewrite service units + helper scripts). + Document the choice because it impacts almost every script. +5. [ ] Confirm musl vs glibc requirements. If any binary *must* stay glibc-linked (e.g., Qualcomm blobs), plan to add the `gcompat`/`alpine-pkg-glibc` shim or run those pieces inside a glibc sysroot. + +## Phase 1 – Alpine Builder Container (new `Dockerfile.builder.alpine`) +1. [ ] Copy `Dockerfile.builder` to `Dockerfile.builder.alpine`; base the new file on `alpine:` while leaving the Ubuntu original intact. +2. [ ] Swap `apt-get` for `apk add --no-cache` and install Alpine equivalents (`build-base`, `clang`, `openssl`, `ccache`, `android-tools`, `py3` packages, etc.). +3. [ ] Ensure `python2` requirement is gone or solved (Alpine only ships `python3`; if python2 is still needed, vendor it from source). +4. [ ] Re-implement the user-mapping logic using BusyBox `addgroup/adduser` syntax. +5. [ ] Verify `ccache` symlink setup still works and that `docker buildx --load` succeeds when `build_alpine.sh` references the new Dockerfile. + +## Phase 2 – Alpine Rootfs Source (`build_alpine.sh`) +1. [ ] Copy `build_system.sh` to `build_alpine.sh` so the Ubuntu path remains unchanged. +2. [ ] Replace Ubuntu download variables with Alpine ones (use `alpine-minirootfs--aarch64.tar.gz`) and update the SHA. +3. [ ] Wire `build_alpine.sh` to call the Alpine Dockerfile and emit outputs under `build/alpine/` + `output/alpine/`. +4. [ ] Drop `debconf`, `dpkg`, and `apt` assumptions as soon as the Alpine tarball is extracted; Alpine already has `apk-tools`. +5. [ ] Confirm `qemu-user-static` still handles musl binaries when running on x86_64 hosts. +6. [ ] Validate that `img2simg` + ext4 creation stays the same (filesystem layer is independent of distro). + +## Phase 3 – Userspace & Package Installation (Alpine variants live beside Ubuntu) +1. **Agnos compiler stages** + - [ ] Duplicate `Dockerfile.agnos` or split it into named targets so the Alpine build stages live alongside the Ubuntu ones without altering them. + - [ ] Change all Alpine stages to `FROM alpine`. Install build deps via `apk` (`alpine-sdk`, `cmake`, `ninja`, etc.). + - [ ] Audit `compile-*.sh` scripts for `apt-get`, `ldconfig`, `/usr/lib/aarch64-linux-gnu` assumptions; rewrite paths for Alpine (`/usr/lib`, `/lib`). + - [ ] Replace `checkinstall`-generated `.deb` outputs with either plain tarballs or ad-hoc `.apk` packages (use `abuild` or `apkbuild` templates). +2. **Base setup (`userspace/base_setup.sh`)** + - [ ] Add `userspace/base_setup_alpine.sh` (leave the Ubuntu script alone). Use `/etc/apk/repositories` plus `apk update && apk add`. + - [ ] Recreate required system users/groups using BusyBox tools. + - [ ] Re-map package names (e.g., `build-essential` → `build-base`, `network-manager` → `NetworkManager` from the community repo, `iptables-persistent` → `iptables` + manual save). + - [ ] Handle 32-bit deps: Alpine’s `aarch64` repo does **not** support mixing `armhf` packages. Decide between cross-compiling needed 32-bit libs from source or hosting a parallel `armv7` sysroot mounted under `/lib32`. + - [ ] Replace `locale-gen`/`update-locale` with Alpine equivalents (`/etc/profile.d/locale.sh`, `glibc-i18n` if using glibc shim, or `musl-locales`). +3. **Openpilot deps (`userspace/openpilot_dependencies.sh`, `openpilot_python_dependencies.sh`)** + - [ ] Provide Alpine siblings of these scripts and translate each dependency to `apk` packages or source builds. For tools missing on Alpine, extend the compiler stages. + - [ ] Ensure `uv` install script runs on musl (needs `build-base`, `curl`, `python3`). +4. **Hardware setup & proprietary debs** + - [ ] For each `.deb` in `userspace/debs`, extract it (`dpkg-deb -x`) and repackage the payload into the Alpine rootfs manually or via custom `apk`s. + - [ ] Verify Qualcomm binaries only depend on glibc symbols that exist in your shim (or keep a glibc chroot mounted under `/usr/glibc`). + - [ ] Replace `apt install libjson-c2` hack with either an Alpine package or a source build pinned to the required ABI. +5. **Service management** + - [ ] If staying with systemd: build systemd against musl (supported as of v253) and ensure `pam`, `udev`, and `resolved` pieces still work. Double-check `systemctl` invocations in `userspace/services.sh`. + - [ ] If moving to OpenRC: convert every `.service`, `.timer`, `.path` file into OpenRC services; rewrite `userspace/services.sh` to call `rc-update`. Confirm replacements for `systemd-tmpfiles`, `journald`, and `networkd` (likely use `busybox-ntpd`, `rsyslog`, and `NetworkManager`). +6. **Filesystem layout differences** + - [ ] Alpine does not use `/lib/systemd/system` or `/usr/lib/aarch64-linux-gnu`; adjust copy paths only in the Alpine Dockerfile variant. + - [ ] Revisit `readonly_setup.sh`: drop `apt` cache cleanup, ensure `/etc/localtime` logic matches Alpine’s `/etc/TZ`/`/etc/timezone` expectations. + +## Phase 4 – Image Assembly & Flash Scripts +1. [ ] Introduce `load_alpine*.sh` and `flash_alpine*.sh` companions instead of rewriting the Ubuntu scripts; point them at the new image names. +2. [ ] Ensure the Alpine image writes its own `/VERSION` metadata (maybe `VERSION_ALPINE` or embed in `VERSION`). +3. [ ] Re-run size optimization (Alpine is smaller; re-tune `ROOTFS_IMAGE_SIZE` specifically for the Alpine build). +4. [ ] Verify `readonly_setup.sh` + `mv /var /usr/default` still behave; Alpine may ship busybox `mv` without `-T`, so test carefully. + +## Phase 5 – Validation +1. [ ] First milestone: boot the Alpine image and verify `/usr/comma/magic.py` auto-runs and shows the logo. +2. [ ] Full milestone: boot the new image on comma 3/3X; confirm kernel + modem + UI stack. +3. [ ] Run the existing `TESTING.md` checklist plus: + - `apk` database integrity (`apk info -vv | head`). + - Services status via the new init system. + - Openpilot runtime smoke test. +4. [ ] Exercise flashing (`flash_alpine*.sh`) end-to-end on at least one device. +5. [ ] Document any remaining Ubuntu assumptions and either fix or log issues for follow-up. + +## Open Questions & Risks +- **glibc-only blobs:** If Qualcomm or Weston hacks require glibc, we need either a glibc compatibility layer or to keep those pieces in a Debian chroot. +- **Systemd vs OpenRC:** Porting dozens of custom services may dwarf other work; validate effort before committing. +- **Multi-arch libraries:** Alpine currently lacks an easy way to install `armhf` packages alongside `aarch64`. Plan for source builds or rethink the need for 32-bit libs. +- **CI coverage:** GitHub Actions runners may not have `apk` tooling; ensure CI images are updated before merging. + +> Next iteration: once we lock the init strategy and package availability, we can expand each checkbox into concrete scripts/commands. diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100644 index 0000000..a57c412 --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,82 @@ +# check=error=true + +ARG ALPINE_VERSION=3.20.8 + +# ############### # +# ### Minimal ### # +# ### Alpine ### # +# ############### # +FROM scratch AS agnos-alpine + +ARG ALPINE_BASE_IMAGE +ARG USERNAME=comma + +ADD ${ALPINE_BASE_IMAGE} / + +SHELL ["/bin/sh", "-c"] +ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PYTHONUNBUFFERED=1 + +# Base packages just for the magic logo proof-of-concept +RUN set -eux; \ + apk update; \ + apk add --no-cache \ + bash \ + ca-certificates \ + coreutils \ + curl \ + eudev \ + libdrm \ + libinput \ + mesa-dri-gallium \ + mesa-egl \ + mesa-gbm \ + mesa-gl \ + musl-locales \ + openrc \ + python3 \ + py3-pillow \ + py3-pip \ + py3-psutil \ + py3-setuptools \ + shadow \ + sudo \ + ttf-dejavu + +# python deps that require a compiler (kept isolated so we can delete the toolchain afterwards) +RUN set -eux; \ + apk add --no-cache --virtual .magic-build-deps \ + build-base \ + linux-headers \ + python3-dev; \ + pip3 install --no-cache-dir --break-system-packages pyray; \ + apk del .magic-build-deps + +# Create comma user +RUN set -eux; \ + addgroup -g 1000 "${USERNAME}" || true; \ + adduser -D -G "${USERNAME}" -u 1000 -s /bin/bash -h "/home/${USERNAME}" "${USERNAME}" || true; \ + echo "${USERNAME}:${USERNAME}" | chpasswd; \ + adduser "${USERNAME}" video; \ + adduser "${USERNAME}" input; \ + adduser "${USERNAME}" dialout; \ + mkdir -p /data /var/tmp/weston; \ + chown "${USERNAME}:${USERNAME}" /data /var/tmp/weston /home/${USERNAME} + +# Copy minimal comma payload (logo assets + helper scripts) +COPY userspace/usr/comma /usr/comma +RUN chown -R "${USERNAME}:${USERNAME}" /usr/comma && \ + chmod +x /usr/comma/*.sh || true + +# Install OpenRC service that drives magic.py +COPY userspace/alpine/magic.init /etc/init.d/magic +RUN chmod 0755 /etc/init.d/magic && \ + rc-update add magic default + +# Basic runtime directories +RUN mkdir -p /run/magic && \ + chown "${USERNAME}:${USERNAME}" /run/magic + +# Default entrypoint: boot OpenRC like a normal system +CMD ["/sbin/init"] diff --git a/build_alpine.sh b/build_alpine.sh new file mode 100755 index 0000000..58d551a --- /dev/null +++ b/build_alpine.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +set -euo pipefail + +ALPINE_VERSION="${ALPINE_VERSION:-3.20.8}" +ALPINE_SERIES="$(echo "$ALPINE_VERSION" | awk -F. '{printf "%s.%s", $1, $2}')" +ALPINE_BASE_URL="https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_SERIES}/releases/aarch64" +ALPINE_FILE="alpine-minirootfs-${ALPINE_VERSION}-aarch64.tar.gz" +ALPINE_FILE_CHECKSUM="${ALPINE_FILE_CHECKSUM:-6d0e15d9f9f5c5003c4692337dffebe9475cab7d8a0390f109f6999fbb28745f}" + +# Ensure we are inside the repo root +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$DIR" + +BUILD_DIR="$DIR/build/alpine" +OUTPUT_DIR="$DIR/output/alpine" + +ROOTFS_DIR="$BUILD_DIR/rootfs" +ROOTFS_IMAGE="$BUILD_DIR/system-alpine.img" +OUT_IMAGE="$OUTPUT_DIR/system-alpine.img" + +ROOTFS_IMAGE_SIZE="${ROOTFS_IMAGE_SIZE:-2048M}" + +mkdir -p "$BUILD_DIR" "$OUTPUT_DIR" + +# Download Alpine minirootfs if missing +if [ ! -f "$ALPINE_FILE" ]; then + echo "Downloading Alpine minirootfs: $ALPINE_FILE" + if ! curl -C - -o "$ALPINE_FILE" "$ALPINE_BASE_URL/$ALPINE_FILE" --silent --remote-time --fail; then + echo "Download failed, please check Alpine releases: $ALPINE_BASE_URL" + exit 1 + fi +fi + +# Verify checksum +if [ "$(shasum -a 256 "$ALPINE_FILE" | awk '{print $1}')" != "$ALPINE_FILE_CHECKSUM" ]; then + echo "Checksum mismatch, please check Alpine releases: $ALPINE_BASE_URL" + exit 1 +fi + +# Register qemu user emulation if needed +if [ "$(uname -m)" = "x86_64" ]; then + echo "Registering qemu-user-static" + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes > /dev/null +fi + +export DOCKER_BUILDKIT=1 + +echo "Checking Dockerfile.alpine" +docker buildx build -f Dockerfile.alpine --check "$DIR" \ + --build-arg ALPINE_VERSION="$ALPINE_VERSION" \ + --build-arg ALPINE_BASE_IMAGE="$ALPINE_FILE" + +echo "Building Alpine system image" +BUILD_CMD="docker buildx build --load" +if [ -n "${NS:-}" ]; then + BUILD_CMD="nsc build --load" +fi +$BUILD_CMD -f Dockerfile.alpine -t agnos-alpine "$DIR" \ + --build-arg ALPINE_VERSION="$ALPINE_VERSION" \ + --build-arg ALPINE_BASE_IMAGE="$ALPINE_FILE" \ + --platform=linux/arm64 + +echo "Creating agnos-alpine container" +CONTAINER_ID=$(docker container create --entrypoint /bin/sh agnos-alpine:latest) + +echo "Checking meta-builder Dockerfile" +docker buildx build --load -f Dockerfile.builder --check "$DIR" \ + --build-arg UNAME="$(id -nu)" \ + --build-arg UID="$(id -u)" \ + --build-arg GID="$(id -g)" + +echo "Building meta-builder" +docker buildx build --load -f Dockerfile.builder -t agnos-meta-builder "$DIR" \ + --build-arg UNAME="$(id -nu)" \ + --build-arg UID="$(id -u)" \ + --build-arg GID="$(id -g)" + +echo "Starting meta-builder container" +MOUNT_CONTAINER_ID=$(docker run -d --privileged -v "$DIR:$DIR" agnos-meta-builder) + +cleanup() { + echo "Cleaning up containers:" + docker container rm -f "$CONTAINER_ID" "$MOUNT_CONTAINER_ID" > /dev/null +} +trap cleanup EXIT + +exec_as_user() { + docker exec -u "$(id -nu)" "$MOUNT_CONTAINER_ID" "$@" +} + +exec_as_root() { + docker exec "$MOUNT_CONTAINER_ID" "$@" +} + +echo "Creating sparse filesystem" +exec_as_user mkdir -p "$BUILD_DIR" +exec_as_user fallocate -l "$ROOTFS_IMAGE_SIZE" "$ROOTFS_IMAGE" +exec_as_user mkfs.ext4 "$ROOTFS_IMAGE" > /dev/null + +echo "Mounting filesystem" +exec_as_root mkdir -p "$ROOTFS_DIR" +exec_as_root mount "$ROOTFS_IMAGE" "$ROOTFS_DIR" + +cleanup_with_umount() { + exec_as_root umount -l "$ROOTFS_DIR" > /dev/null 2>&1 || true + cleanup +} +trap cleanup_with_umount EXIT + +echo "Extracting container filesystem" +docker container export -o "$BUILD_DIR/filesystem.tar" "$CONTAINER_ID" +exec_as_root tar -xf "$BUILD_DIR/filesystem.tar" -C "$ROOTFS_DIR" > /dev/null + +echo "Removing container markers" +exec_as_root rm -f "$ROOTFS_DIR/.dockerenv" + +echo "Configuring hostname and networking files" +set_network_stuff() { + cd "$ROOTFS_DIR" + HOST=comma + ln -sf /proc/sys/kernel/hostname etc/hostname + echo "127.0.0.1 localhost.localdomain localhost" > etc/hosts + echo "127.0.0.1 $HOST" >> etc/hosts + ln -sf /run/network/resolv.conf etc/resolv.conf + DATETIME=$(date '+%Y-%m-%dT%H:%M:%S') + printf "%s\n%s\n" "$GIT_HASH" "$DATETIME" > BUILD +} +GIT_HASH=${GIT_HASH:-$(git --git-dir="$DIR/.git" rev-parse HEAD)} +exec_as_root bash -c "set -e; export ROOTFS_DIR=\"$ROOTFS_DIR\" GIT_HASH=\"$GIT_HASH\"; $(declare -f set_network_stuff); set_network_stuff" + +echo "Unmounting filesystem" +exec_as_root umount -l "$ROOTFS_DIR" + +echo "Sparsifying image" +exec_as_user img2simg "$ROOTFS_IMAGE" "$OUT_IMAGE" + +echo "Alpine image written to $OUT_IMAGE" diff --git a/test_alpine_magic.sh b/test_alpine_magic.sh new file mode 100755 index 0000000..c2ffb2f --- /dev/null +++ b/test_alpine_magic.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_PATH="${1:-output/alpine/system-alpine.img}" + +if [ ! -f "$IMAGE_PATH" ]; then + echo "Missing $IMAGE_PATH" + echo "Build it first with ./build_alpine.sh" + exit 1 +fi + +cat <