vibing alpine

This commit is contained in:
Adeeb Shihadeh
2025-11-12 16:51:08 -08:00
parent 99e9e3e6f5
commit ad00c4a1e9
5 changed files with 371 additions and 0 deletions
+107
View File
@@ -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, dont 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-<ver>-aarch64.tar.gz`.
- Use new Alpine docker build to produce a `system-alpine.img`.
- Keep all outputs in `output/alpine/` so it doesnt 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:<version>` 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-<ver>-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: Alpines `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 Alpines `/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.
+82
View File
@@ -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"]
+137
View File
@@ -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"
+20
View File
@@ -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 <<EOF
Alpine splash image ready at: $IMAGE_PATH
Manual smoke test until automation exists:
1. Flash the image to a spare comma 3/3X or boot it in your preferred ARM64 VM.
2. Let the device boot normally. OpenRC should launch /usr/comma/magic.py automatically.
3. Verify the comma logo (bg.jpg) appears and the backlight turns on.
4. If it does not, capture /var/log/magic.log from the device and file an issue.
EOF
+25
View File
@@ -0,0 +1,25 @@
#!/sbin/openrc-run
description="Render the comma boot logo with magic.py"
command="/usr/bin/python3"
command_args="/usr/comma/magic.py"
command_user="comma"
command_group="comma"
command_background="true"
pidfile="/run/magic/magic.pid"
start_stop_daemon_args="--make-pidfile --stdout /var/log/magic.log --stderr /var/log/magic.log"
supervisor="supervise-daemon"
respawn_delay="2"
respawn_max="0"
depend() {
need localmount
use net
after modules
}
start_pre() {
checkpath --directory --mode 0755 --owner ${command_user}:${command_group} /var/log
checkpath --file --mode 0644 --owner ${command_user}:${command_group} /var/log/magic.log
checkpath --directory --mode 0755 --owner ${command_user}:${command_group} /var/tmp/weston
}