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
5 changes: 5 additions & 0 deletions doc/source/build_system.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ dependencies plus the SO3 image tooling (``device-tree-compiler``,
build dependencies, …) and a few **cross-toolchains** that are *not* built by the
layers. The canonical, continuously-tested list is the base container recipe
``docker/Dockerfile.toolchains`` — mirror it when setting up a bare host.
For development, the same environment is available as a ready-made build
container: ``scripts/dbuild.sh --build`` then ``scripts/dbuild.sh build.sh
bsp-linux`` (or bare ``dbuild.sh`` for a shell) runs any front-end script
inside it, with the repository bind-mounted at its own path and the container
running as the calling user — see ``docker/README.md``.

The cross-toolchains that must be on ``PATH``:

Expand Down
23 changes: 23 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ All images build SO3 with the **Infrabase** (bitbake) build system. The containe
holds the whole repository at `/so3`; inside it the kernel is at `/so3/so3/so3`,
the user space at `/so3/so3/usr` and the bundled LVGL at `/so3/so3/usr/lib/lvgl`.

# Developer build container — `dbuild.sh`

For day-to-day development, use [`build-env`](./build-env) driven by
`scripts/dbuild.sh` — it replaces the older "repository baked into the image"
flow for interactive work. The image carries ONLY the environment (both Arm
64-bit toolchains including the bare-metal `aarch64-none-elf`, the 32-bit apt
toolchains, host packages); the repository stays on the host and is
bind-mounted at its **own absolute path**, so host and container builds share
one `build/tmp` interchangeably, and the container runs as the calling host
user so everything it writes stays yours.

```
./scripts/dbuild.sh --build # build the so3-build:1.0 image (once)
./scripts/dbuild.sh build.sh bsp-linux # run any front-end script inside
./scripts/dbuild.sh # interactive shell, env.sh sourced
./scripts/dbuild.sh st.sh -d # graphical QEMU from the container
```

Caller environment variables are not forwarded; pass them through the
command: `./scripts/dbuild.sh env IB_FORCE_ATTACH=1 build.sh bsp-linux`.

The images below remain the CI/perf-rig side of the house.

# Base build environment

- [`Dockerfile.toolchains`](./Dockerfile.toolchains) — Ubuntu image with the
Expand Down
135 changes: 135 additions & 0 deletions docker/build-env/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Build environment for the SO3 Infrabase build system.
#
# The image carries ONLY the host environment: cross toolchains, host
# packages, Python. It contains NO project source. The repository stays
# on the host and is bind-mounted by scripts/dbuild.sh at its own
# absolute path, so the same tree can be built from inside or outside
# the container interchangeably (bitbake stamps, the buildroot host tree,
# CMake caches and the *.attach.sha256 manifests all embed absolute paths).
#
# Build the image: scripts/dbuild.sh --build
# Use it: scripts/dbuild.sh build.sh bsp-linux
# scripts/dbuild.sh st.sh -d
#
# The build context is this directory only — never the project root,
# which holds tens of GB of build output.
#
# Copyright (c) 2026 REDS Institute - HEIG-VD

# ---------------------------------------------------------------------
# Stage 1 — fetch the Arm cross toolchains
# ---------------------------------------------------------------------

FROM ubuntu:24.04 AS toolchains

# Pinned deliberately: these are the versions the build is validated
# with. Bump them here, rebuild the image and re-validate — never by
# installing something else on a host.
#
# Only the 64-bit toolchains are downloaded. The 32-bit ones come from
# apt (see packages.txt) because Ubuntu's packages carry exactly the
# prefixes the recipes pin. The 64-bit ones do NOT: Ubuntu's
# gcc-aarch64-linux-gnu installs "aarch64-linux-gnu-", while
# IB_TOOLCHAIN:virt64/:rpi4_64 and the patched kernel Makefile pin
# "aarch64-none-linux-gnu-".

ARG AARCH64_LINUX_VER=12.3.rel1
ARG AARCH64_ELF_VER=12.3.rel1
ARG ARM_BASE=https://developer.arm.com/-/media/Files/downloads/gnu

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl xz-utils \
&& rm -rf /var/lib/apt/lists/*

RUN mkdir -p /opt/toolchains

# Linux-hosted toolchain: 64-bit kernel and U-Boot (virt64, rpi4_64).

RUN curl -fSL "${ARM_BASE}/${AARCH64_LINUX_VER}/binrel/arm-gnu-toolchain-${AARCH64_LINUX_VER}-$(uname -m)-aarch64-none-linux-gnu.tar.xz" \
| tar -xJ -C /opt/toolchains \
&& mv /opt/toolchains/arm-gnu-toolchain-*-aarch64-none-linux-gnu \
/opt/toolchains/aarch64-none-linux-gnu

# Bare-metal toolchain: the SO3 kernel and AVZ, 64-bit. Selected by
# CONFIG_CROSS_COMPILE in the SO3 defconfig, NOT by IB_TOOLCHAIN — which
# is why it is so easy to miss on a hand-built host.

RUN curl -fSL "${ARM_BASE}/${AARCH64_ELF_VER}/binrel/arm-gnu-toolchain-${AARCH64_ELF_VER}-$(uname -m)-aarch64-none-elf.tar.xz" \
| tar -xJ -C /opt/toolchains \
&& mv /opt/toolchains/arm-gnu-toolchain-*-aarch64-none-elf \
/opt/toolchains/aarch64-none-elf

# ---------------------------------------------------------------------
# Stage 2 — the build environment proper
# ---------------------------------------------------------------------

FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

# BitBake requires a UTF-8 locale.

RUN apt-get update && apt-get install -y --no-install-recommends locales \
&& localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 \
&& rm -rf /var/lib/apt/lists/*

ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8

# Host packages come from packages.txt — the single source of truth.
# Adding a dependency there is enough.
#
# The list covers building AND running QEMU: qemu/build/qemu-system-* is
# built into the bind-mounted tree but resolves its libraries inside the
# container.

COPY packages.txt /tmp/packages.txt

RUN apt-get update \
&& sed -e 's/#.*//' -e '/^[[:space:]]*$/d' /tmp/packages.txt \
| xargs apt-get install -y --no-install-recommends \
&& apt-get install -y --no-install-recommends vim nano less \
&& rm -rf /var/lib/apt/lists/* /tmp/packages.txt

# pcpp: pre-processor used by the SO3 ITS/device-tree tooling.

RUN pip3 install --break-system-packages --no-cache-dir pcpp

COPY --from=toolchains /opt/toolchains /opt/toolchains

ENV PATH=/opt/toolchains/aarch64-none-linux-gnu/bin:/opt/toolchains/aarch64-none-elf/bin:${PATH}

# The build escalates a few operations (losetup, mount, mkfs, cpio with
# device nodes) through `sudo -n`. Inside a throw-away build container a
# blanket NOPASSWD rule is the simplest correct answer, and it makes the
# host-side tty_tickets problem (scripts/common/setup_sudo.sh) moot.
#
# `Defaults !authenticate` is NOT redundant with NOPASSWD. The front-end
# scripts open their session with `sudo -v`, which validates the user
# rather than a specific command: sudo then requires authentication as
# soon as ANY applicable rule asks for it — and the build user also
# matches Ubuntu's `%sudo ALL=(ALL:ALL) ALL`. Without this line,
# `sudo -n true` succeeds while `sudo -v` fails with "a terminal is
# required to read the password", and every `build.sh bsp-linux*` /
# `deploy.sh` dies at sudo_session_start.

RUN printf 'Defaults !authenticate\nALL ALL=(ALL) NOPASSWD:ALL\n' \
> /etc/sudoers.d/build-env \
&& chmod 0440 /etc/sudoers.d/build-env

# The tree is bind-mounted and owned by the host user; keep git quiet if
# the ids ever fail to line up.

RUN git config --system --add safe.directory '*'

COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

# rc file for `dbuild.sh` with no command: an interactive shell with
# env.sh already sourced (see the file for why it must be sourced there
# and not in a parent).

COPY bashrc /etc/so3-build.bashrc

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["bash"]
62 changes: 62 additions & 0 deletions docker/build-env/bashrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Interactive shell inside the SO3 build container.
#
# Sourcing env.sh HERE (rather than in a parent process) matters: it
# defines shell functions — the cd/pushd/popd wrappers and
# ib_autoswitch_* — which only exist in the shell that sourced them.
# A parent that sources env.sh then execs bash passes on the exported
# variables but loses the functions.
#
# Copyright (c) 2026 REDS Institute - HEIG-VD

# NOT sourcing /etc/bash.bashrc here: Debian/Ubuntu bash already reads it for
# every interactive shell, --rcfile only replaces ~/.bashrc. Sourcing it again
# ran its sudo hint — and anything else it prints — twice on entry.

# Colours and the usual ls shorthands. On a normal Ubuntu login these come from
# the user's ~/.bashrc, which `bash --rcfile` deliberately bypasses in favour of
# this file — so without the block below the container shell is the one place
# where `ls` comes out monochrome. Sourcing ~/.bashrc instead would drag in its
# PS1 and undo the [so3-build] prompt set at the bottom.

if [ -x /usr/bin/dircolors ]; then
if [ -r "$HOME/.dircolors" ]; then
eval "$(dircolors -b "$HOME/.dircolors")"
else
eval "$(dircolors -b)"
fi
alias ls='ls --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
fi

alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'

if [ -n "$IB_TREE" ] && [ -f "$IB_TREE/env.sh" ]; then
cd "$IB_TREE" && . ./env.sh >/dev/null 2>&1
fi

# Return to the directory the user invoked dbuild.sh from.

[ -n "$IB_CWD" ] && cd "$IB_CWD"

# Make it obvious at a glance that this shell is inside the container —
# the tree looks identical to the host one, same absolute paths.

PS1='\[\033[01;33m\][so3-build]\[\033[00m\] \w \$ '

cat <<'EOF'

SO3 build container — the repository is bind-mounted from the host and
you are running as the host user, so anything you produce stays yours.
env.sh is already sourced.

build.sh -l list recipes
build.sh bsp-linux build the Linux BSP (bsp-so3 for SO3)
deploy.sh bsp-linux deploy (FIT + rootfs + sdcard)
st.sh / st.sh -d run in QEMU (headless / graphical)
exit leave the container

EOF
56 changes: 56 additions & 0 deletions docker/build-env/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/sh

# Container entrypoint: run the build as the HOST user, not as root.
#
# Everything the recipes write lands in the bind-mounted repository —
# source trees attached in place, build/tmp, filesystem images. If the
# container ran as root, all of it would come back root-owned on the
# host, reintroducing exactly the ownership churn the unprivileged
# bitbake model was designed to remove.
#
# The UID/GID cannot be baked into the image: they differ per host
# (1000 for a developer laptop, 996 for gitlab-runner on the CI box).
# So the entrypoint starts as root, materialises a matching account,
# then drops to it.
#
# Copyright (c) 2026 REDS Institute - HEIG-VD

set -e

HOST_UID=${HOST_UID:-1000}
HOST_GID=${HOST_GID:-1000}

# Reuse an existing group/user with that id when the base image already
# ships one (ubuntu:24.04 has 'ubuntu' at 1000) — useradd would fail on
# a duplicate id otherwise.

group_name=$(getent group "$HOST_GID" | cut -d: -f1)
if [ -z "$group_name" ]; then
group_name=builder
groupadd -g "$HOST_GID" "$group_name"
fi

user_name=$(getent passwd "$HOST_UID" | cut -d: -f1)
if [ -z "$user_name" ]; then
user_name=builder
useradd -m -u "$HOST_UID" -g "$HOST_GID" -s /bin/bash "$user_name"
fi

usermod -aG sudo "$user_name" 2>/dev/null || true

# A writable HOME matters: pip caches and ccache land there (the Sense
# HAT emulator venv lives in build/tmp, inside the bind-mounted tree).

user_home=$(getent passwd "$HOST_UID" | cut -d: -f6)
if [ ! -d "$user_home" ]; then
mkdir -p "$user_home"
fi
chown "$HOST_UID:$HOST_GID" "$user_home"

export HOME="$user_home"
export USER="$user_name"

# setpriv (util-linux) rather than su/sudo: no extra process in the
# signal path, so Ctrl-C reaches the build and exit codes propagate.

exec setpriv --reuid="$HOST_UID" --regid="$HOST_GID" --init-groups "$@"
Loading
Loading