Skip to content
Draft
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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ RUN apt-get install -y \
flex \
parted \
fdisk \
btrfs-progs \
python3-dev \
python3-libfdt \
python3-setuptools \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ sudo apt install git build-essential crossbuild-essential-arm64 bc bison flex \
For assembling disk images — debos, bmaptool, and zeekstd all need to be installed from source:

```bash
sudo apt install golang pipx pigz cargo parted fdisk mmdebstrap \
sudo apt install golang pipx pigz cargo parted fdisk btrfs-progs mmdebstrap \
systemd-resolved systemd-container qemu-user-binfmt \
libglib2.0-dev libostree-dev fakemachine

Expand Down
10 changes: 0 additions & 10 deletions build-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,10 @@ trap cleanup EXIT

bmaptool copy "$IMG_OUT"/debian-rootfs.img.zst "$IMG_OUT"/debian-rootfs.img
sync "$IMG_OUT"/debian-rootfs.img
TMP=`mktemp`
chmod 644 "$TMP"

# Boot entries are pure BLS in /boot/loader/entries, read directly by U-Boot's
# 'bls' bootmeth (no extlinux.conf). Nothing to rewrite here for the boot menu.

debugfs -R "cat /usr/lib/os-release" "$IMG_OUT"/debian-rootfs.img > "$TMP"
echo "BUILD_ID=$BUILD_ID" >> "$TMP"
debugfs -w -R "rm /usr/lib/os-release" "$IMG_OUT"/debian-rootfs.img
debugfs -w -R "write $TMP /usr/lib/os-release" "$IMG_OUT"/debian-rootfs.img
sync "$IMG_OUT"/debian-rootfs.img

rm -f "$TMP"

for s in 512 4096; do
echo "Creating images for $s-byte sector size"
truncate -s "$IMGSIZE" "$TMPDIR"/debian-"$s"-nobootloader-"$BUILD_ID".img
Expand Down
13 changes: 12 additions & 1 deletion build-rootfs-img.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@

set -e

# Capture Git information
[ -n "${GIT_HASH}" ] || GIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
[ -n "${GIT_BRANCH}" ] || GIT_BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git describe --tags --exact-match 2>/dev/null || echo "detached")
[ -n "${GIT_MSG}" ] || GIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null | sed 's/[\"()]/\\&/g; s/'"'"'/\\&/g' || echo "unknown")
[ -n "${GIT_INFO}" ] || GIT_INFO="${GIT_BRANCH}@${GIT_HASH}: ${GIT_MSG}"
GIT_INFO=$(echo "$GIT_INFO" | tr -dc '[:alnum:][:space:]')

# Build identifier: CI sets BUILD_ID in the environment (e.g. "build-1419"); otherwise
# fall back to a UTC timestamp, same default/format as build-images.sh.
: "${BUILD_ID:=$(date -u '+%Y%m%d-%H%M')}"

if [ -c /dev/kvm -a -w /dev/kvm ]; then
# Have virtualization support, can use fakemachine (default, fast, safe)
DEBOS="debos -c $(nproc) -m 6Gb"
Expand All @@ -29,7 +40,7 @@ mkdir -p "$IMG_OUT"/linux_tmp
cp -r "$LINUX_OUT"/* "$IMG_OUT"/linux_tmp

echo "Creating the root FS image"
$DEBOS --artifactdir="$IMG_OUT" -t imagesize:"$IMGSIZE" -t kerneldir:linux_tmp debian-rk3576-img.yaml
$DEBOS --artifactdir="$IMG_OUT" -t imagesize:"$IMGSIZE" -t kerneldir:linux_tmp -t gitinfo:"$GIT_INFO" -t buildid:"$BUILD_ID" debian-rk3576-img.yaml
sync "$IMG_OUT"/debian-nobootloader.img

owner=$(stat -c %u "$IMG_OUT"/debian-nobootloader.img)
Expand Down
281 changes: 273 additions & 8 deletions debian-rk3576-img.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
{{ $sectorsize := or .sectorsize 512 }}
{{ $imagesize := or .imagesize "4GiB" }}
{{ $kerneldir := or .kerneldir "prebuilt/linux" }}
{{ $cmdline := or .cmdline "audit=0 console=tty1 console=ttyS0,1500000n8" }}
{{ $imagesize := or .imagesize "4GiB" }}
{{ $kerneldir := or .kerneldir "prebuilt/linux" }}
{{ $cmdline := or .cmdline "audit=0 console=tty1 console=ttyS0,1500000n8" }}
{{ $fslabel := or .fslabel "flipperos" }}
{{ $gitinfo := or .gitinfo "unknown" }}
{{ $buildid := or .buildid "unknown" }}
{{- /* fs-wide btrfs mount options. ssd is FORCED (loopback/eMMC isn't reliably
detected as non-rotational). */ -}}
{{ $btrfsopts := or .btrfsopts "compress=zstd,noatime,ssd,discard=async" }}
{{ $rootdev := "findmnt -fn --nofsroot -o SOURCE --target $IMAGEMNTDIR" }}

architecture: arm64
sectorsize: {{ $sectorsize }}

actions:
- action: unpack
description: Load a tarball of the root filesystem
file: debian-ospack.tar.gz

# debos formats btrfs (default flags, random FSUUID), mounts the top level at
# $IMAGEMNTDIR, and records the standard root fstab line + root=UUID.
# fsck:false -> passno 0 (no boot-time fsck for btrfs).
- action: image-partition
imagename: debian-nobootloader.img
imagesize: {{ $imagesize }}
Expand All @@ -21,17 +30,85 @@ actions:
start: 32KiB
end: 64MiB
- name: root
fs: ext4
fs: btrfs
fslabel: {{ $fslabel }}
fsck: false
start: 64MiB
end: -1MiB
flags: [ boot ]
mountpoints:
- mountpoint: /
partition: root
options: [ x-systemd.growfs ]
options: [ compress=zstd, noatime, ssd, discard=async, x-systemd.growfs ]

# Subvolume layout (no reformat). /boot is a TOP-LEVEL subvolume named "boot"
# (not "@boot") so it sits outside the per-profile root subvolumes -> a single shared
# /boot that root snapshots never capture, and so U-Boot, which reads the default
# (top-level) subvolume, resolves /boot/extlinux/... straight into it. Keep the default
# subvolume as the top level (do NOT set a default subvolume) or U-Boot won't see /boot.
- action: run
description: Create subvolumes and mount the layout
chroot: false
command: |
set -eu
ROOTPART="$({{ $rootdev }})"

# debos has the top level (subvolid=5) mounted here; create subvolumes on it.
# @Minimal is the common base / build target; @snapshots holds runtime snapshots.
for sv in @Minimal boot @home @snapshots @var-log @var-cache; do
btrfs subvolume create "$IMAGEMNTDIR/$sv"
done
umount "$IMAGEMNTDIR"

# Mount @Minimal at the mount point debos owns, then nest the rest under it.
mount -t btrfs -o "{{ $btrfsopts }},subvol=@Minimal" "$ROOTPART" "$IMAGEMNTDIR"
mkdir -p "$IMAGEMNTDIR/boot" \
"$IMAGEMNTDIR/home" \
"$IMAGEMNTDIR/var/log" \
"$IMAGEMNTDIR/var/cache"
mount -t btrfs -o "{{ $btrfsopts }},subvol=boot" "$ROOTPART" "$IMAGEMNTDIR/boot"
# Keep /boot uncompressed so U-Boot can read the kernel/dtb/initrd regardless
# of whether its btrfs driver was built with zstd support. The property is
# stored on the subvolume and inherited by files added later (kernel updates).
btrfs property set "$IMAGEMNTDIR/boot" compression none
mount -t btrfs -o "{{ $btrfsopts }},subvol=@home" "$ROOTPART" "$IMAGEMNTDIR/home"
mount -t btrfs -o "{{ $btrfsopts }},subvol=@var-log" "$ROOTPART" "$IMAGEMNTDIR/var/log"
mount -t btrfs -o "{{ $btrfsopts }},subvol=@var-cache" "$ROOTPART" "$IMAGEMNTDIR/var/cache"

# debos writes the standard root line into /etc/fstab and root=UUID into the cmdline;
# the root subvolume is selected by the appended rootflags. @Desktop is the default root
# (the desktop); the other profiles' BLS entries append rootflags=subvol=@<profile>
# and the last rootflags= on the line wins. (Deploy still happens into @Minimal.)
- action: filesystem-deploy
append-kernel-cmdline: {{ $cmdline }}
append-kernel-cmdline: rootflags=subvol=@Desktop {{ $cmdline }}

# Add fstab entries for the non-root subvolumes (debos only generated the root
# line). The FSUUID is read back at build time. These (/boot, /home, /var/log,
# /var/cache) are shared by every profile root. @snapshots is intentionally omitted
# -- it holds runtime snapshots, managed on demand, never auto-mounted.
- action: run
description: Append subvolume entries to /etc/fstab
chroot: false
command: |
set -eu
ROOTPART="$({{ $rootdev }})"
FSUUID="$(blkid -o value -s UUID -p -c none "$ROOTPART")"
cat >> "$IMAGEMNTDIR/etc/fstab" <<EOF
UUID=$FSUUID /boot btrfs {{ $btrfsopts }},subvol=boot 0 0
UUID=$FSUUID /home btrfs {{ $btrfsopts }},subvol=@home 0 0
UUID=$FSUUID /var/log btrfs {{ $btrfsopts }},subvol=@var-log 0 0
UUID=$FSUUID /var/cache btrfs {{ $btrfsopts }},subvol=@var-cache 0 0
EOF

# journald's append-heavy writes fragment badly under btrfs copy-on-write. Pre-create
# the journal dir with NODATACOW (chattr +C) while it's still empty, so the journal
# files written on first boot inherit it. (+C also implies no compression.)
- action: run
description: Disable CoW on the systemd journal directory
chroot: false
command: |
set -eu
mkdir -p "$IMAGEMNTDIR/var/log/journal"
chattr +C "$IMAGEMNTDIR/var/log/journal"

- action: overlay
description: Copy Linux kernel packages into the target
Expand All @@ -43,3 +120,191 @@ actions:
description: Install Linux kernel packages inside the target
chroot: true
command: find /var/cache/linux-kernel -name linux-image-\*.deb -a ! -name \*-dbg_\* -print0 | xargs -0 apt install -y

# Default session for every profile: the no-DE multi-user target, where the cog-seat1
# kiosk runs (and @TV's kodi, enabled under multi-user). Only @Desktop overrides this to
# graphical.target (for sddm); @Minimal/@Router/@TV all stay on multi-user.
- action: run
description: Set the default boot target to multi-user (only @Desktop overrides)
chroot: true
command: systemctl set-default multi-user.target

# The ospack tarball already carries a BUILD_GIT (the ospack build's git state), and the
# ospack is cached/reused across image builds. Replace it with THIS image build's git
# state so the stamp reflects the actual image. Stamped into @Minimal before it is snapshotted
# to @Minimal_stock, so every profile inherits it via COW. Both are consumed by the MOTD/banner.
- action: run
description: Save GIT info
chroot: true
command: |
# /etc/os-release is normally a symlink to /usr/lib/os-release; resolve it so
# sed edits the real file in place instead of replacing the symlink.
osr="$(readlink -f /etc/os-release)"
sed -i '/^BUILD_GIT=/d; /^BUILD_ID=/d' "$osr"
echo 'BUILD_GIT="{{ $gitinfo }}"' >> "$osr"
echo 'BUILD_ID={{ $buildid }}' >> "$osr"

# ===========================================================================
# Stock (RO) bases + per-profile bootable roots.
#
# @Minimal_stock is the RO golden image of the common base. Each profile is a
# WRITABLE snapshot of it (COW: shares the base, only profile packages add new extents),
# mounted at $IMAGEMNTDIR so standard debos `apt`/`overlay` actions install into it;
# it is then locked RO and the writable @<profile> bootable root is snapshotted from it.
# BLS selects a profile via rootflags=subvol=@<profile>. The kernel lives in
# @Minimal_stock (installed above), so every profile inherits it via COW. /boot,
# @home and @var-* are separate top-level subvols shared by all profiles.
#
# KDE and TV-Media-Box install their packages (apt) + config (overlay); only @Desktop changes
# its default target (to graphical, for sddm) -- TV/Router/Minimal stay on multi-user.
# Router applies only its config overlay (NM AP/WAN + ip-forwarding); no packages needed.
# ===========================================================================

- action: run
description: Lock @Minimal_stock (RO), re-create @Minimal from it, free the mountpoint
chroot: false
command: |
set -eu
sync
ROOTPART="$({{ $rootdev }})" # @Minimal is still mounted at $IMAGEMNTDIR
# Mount the btrfs top level at a fixed path and KEEP it mounted for the whole profile
# sequence: it is our stable handle for `findmnt --target` (device lookup then works in
# every debos mode, whether or not $IMAGEMNTDIR is mounted) and where the subvols live.
TOP="${IMAGEMNTDIR%/}.toplevel"
mkdir -p "$TOP"
mount -t btrfs -o subvolid=5 "$ROOTPART" "$TOP"
btrfs subvolume snapshot -r "$TOP/@Minimal" "$TOP/@Minimal_stock"
# release @Minimal and its nested submounts so we can replace it and profile
# factories can mount here
findmnt -rno TARGET --submounts "$IMAGEMNTDIR" \
| awk '{ print length, $0 }' | sort -rn | cut -d" " -f2- \
| while read -r mp; do umount "$mp"; done
# Re-create @Minimal as a writable child of the golden base (parent = @Minimal_stock)
# so it matches the other profiles -- which are snapshots of their _stock -- instead
# of being the build origin with no parent.
btrfs subvolume delete "$TOP/@Minimal"
btrfs subvolume snapshot "$TOP/@Minimal_stock" "$TOP/@Minimal"

# --- @Desktop ------------------------------------------------------------------
- action: run
description: Snapshot writable @Desktop_stock and mount it
chroot: false
command: |
set -eu
TOP="${IMAGEMNTDIR%/}.toplevel"
ROOTPART="$(findmnt -fn --nofsroot -o SOURCE --target "$TOP")"
btrfs subvolume snapshot "$TOP/@Minimal_stock" "$TOP/@Desktop_stock"
mount -t btrfs -o "{{ $btrfsopts }},subvol=@Desktop_stock" "$ROOTPART" "$IMAGEMNTDIR"
- action: apt
description: KDE desktop packages
recommends: false # image is minbase/no-recommends; deps are listed explicitly
packages:
- bluedevil
- blueman
- breeze-gtk-theme
- kde-config-gtk-style
- kde-config-screenlocker
- kde-spectacle
- kde-standard
- konsole
- kscreen
- mpv
- plasma-discover
- plasma-nm
- plasma-systemmonitor
- powerdevil
- sddm
- sddm-theme-breeze
- systemsettings
- action: overlay
description: KDE profile config (sddm autologin, plasma, wallpaper)
source: overlays/profile-kde
destination: /
- action: run
description: Make the KDE desktop the default session on @Desktop
chroot: true
command: systemctl set-default graphical.target
- action: run
description: Lock @Desktop_stock RO and spawn writable @Desktop
chroot: false
command: |
set -eu
TOP="${IMAGEMNTDIR%/}.toplevel"
umount "$IMAGEMNTDIR"
btrfs property set "$TOP/@Desktop_stock" ro true
btrfs subvolume snapshot "$TOP/@Desktop_stock" "$TOP/@Desktop"

# --- @TV-Media-Box ---------------------------------------------------------
- action: run
description: Snapshot writable @TV-Media-Box_stock and mount it
chroot: false
command: |
set -eu
TOP="${IMAGEMNTDIR%/}.toplevel"
ROOTPART="$(findmnt -fn --nofsroot -o SOURCE --target "$TOP")"
btrfs subvolume snapshot "$TOP/@Minimal_stock" "$TOP/@TV-Media-Box_stock"
mount -t btrfs -o "{{ $btrfsopts }},subvol=@TV-Media-Box_stock" "$ROOTPART" "$IMAGEMNTDIR"
- action: apt
description: TV media box packages
recommends: false
packages:
- cage
- kodi
- kodi-eventclients-kodi-send
- action: overlay
description: TV media box profile config (kodi, enabled under multi-user)
source: overlays/profile-tv-media-box
destination: /
- action: run
description: Lock @TV-Media-Box_stock RO and spawn writable @TV-Media-Box
chroot: false
command: |
set -eu
TOP="${IMAGEMNTDIR%/}.toplevel"
umount "$IMAGEMNTDIR"
btrfs property set "$TOP/@TV-Media-Box_stock" ro true
btrfs subvolume snapshot "$TOP/@TV-Media-Box_stock" "$TOP/@TV-Media-Box"

# --- @Router ---------------------------------------------------------------
- action: run
description: Snapshot writable @Router_stock and mount it
chroot: false
command: |
set -eu
TOP="${IMAGEMNTDIR%/}.toplevel"
ROOTPART="$(findmnt -fn --nofsroot -o SOURCE --target "$TOP")"
btrfs subvolume snapshot "$TOP/@Minimal_stock" "$TOP/@Router_stock"
mount -t btrfs -o "{{ $btrfsopts }},subvol=@Router_stock" "$ROOTPART" "$IMAGEMNTDIR"
# @Router needs no packages beyond Minimal (NetworkManager + dnsmasq-base + wpa_supplicant
# are common); it just adds the AP/WAN connections + ip-forwarding via the overlay. Its
# default session stays multi-user (inherited); NM autoconnect brings the router up.
- action: overlay
description: Router profile config (NM AP/WAN connections, ip-forwarding)
source: overlays/profile-router
destination: /
- action: run
description: Fix NetworkManager connection permissions on @Router
chroot: true
command: chmod 600 /etc/NetworkManager/system-connections/*.nmconnection
- action: run
description: Lock @Router_stock RO and spawn writable @Router
chroot: false
command: |
set -eu
TOP="${IMAGEMNTDIR%/}.toplevel"
umount "$IMAGEMNTDIR"
btrfs property set "$TOP/@Router_stock" ro true
btrfs subvolume snapshot "$TOP/@Router_stock" "$TOP/@Router"

# Unmount the top level and remount @Minimal at the mountpoint debos owns, so its
# image-partition Cleanup can unmount it and detach the loop. (The profile builds left
# $IMAGEMNTDIR unmounted, and the lingering top-level mount would hold the loop busy.)
- action: run
description: Unmount the top level and remount @Minimal for debos cleanup
chroot: false
command: |
set -eu
TOP="${IMAGEMNTDIR%/}.toplevel"
ROOTPART="$(findmnt -fn --nofsroot -o SOURCE --target "$TOP")"
umount "$TOP"; rmdir "$TOP"
mount -t btrfs -o "{{ $btrfsopts }},subvol=@Minimal" "$ROOTPART" "$IMAGEMNTDIR"
Loading