From 1ca254da153bdfbf52f275a522691c98b5dce572 Mon Sep 17 00:00:00 2001 From: Yury MonZon Date: Wed, 24 Jun 2026 11:54:44 +0200 Subject: [PATCH 1/4] Add BTRFS support --- Dockerfile | 1 + README.md | 2 +- build-images.sh | 10 ---------- debian-rk3576-img.yaml | 11 ++++++++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index a48e1ac..3da2156 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ RUN apt-get install -y \ flex \ parted \ fdisk \ + btrfs-progs \ python3-dev \ python3-libfdt \ python3-setuptools \ diff --git a/README.md b/README.md index d27a3c6..b3cab4f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build-images.sh b/build-images.sh index 7f864b1..2c92db3 100755 --- a/build-images.sh +++ b/build-images.sh @@ -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 diff --git a/debian-rk3576-img.yaml b/debian-rk3576-img.yaml index 2e2aff3..8f73e06 100644 --- a/debian-rk3576-img.yaml +++ b/debian-rk3576-img.yaml @@ -21,14 +21,18 @@ actions: start: 32KiB end: 64MiB - name: root - fs: ext4 + fs: btrfs start: 64MiB end: -1MiB - flags: [ boot ] mountpoints: - mountpoint: / partition: root - options: [ x-systemd.growfs ] + options: [ compress=zstd, x-systemd.growfs ] + + - action: run + description: Enable zstd compression on the image mount + chroot: false + command: mount -o remount,compress=zstd "$IMAGEMNTDIR" - action: filesystem-deploy append-kernel-cmdline: {{ $cmdline }} @@ -43,3 +47,4 @@ 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 + From d93e455e03d8a1a12fe5702ccf13d4d4f63c3c4d Mon Sep 17 00:00:00 2001 From: Yury MonZon Date: Tue, 30 Jun 2026 21:41:15 +0200 Subject: [PATCH 2/4] Add btrfs subvolume scheme, boot entries and tooling Define the subvolume layout (@, @home, @var-log, @var-cache, @snapshots, boot) with per-subvolume mount options, keep /boot uncompressed so U-Boot can read the kernel/dtb/initrd, and factor out root device retrieval into a helper. Generate the BLS boot entries, deriving the devicetreedir/overlay prefix from each entry's root subvolume so U-Boot resolves the in-entry paths correctly, with FDT_SUBVOL_PREFIX as an override. Install btrfs-progs and btrfs-compsize. --- debian-rk3576-img.yaml | 93 +++++++++++++++++-- debian-rk3576-ospack.yaml | 3 + .../kernel/install.d/90-loaderentry.install | 48 +++++++--- 3 files changed, 121 insertions(+), 23 deletions(-) diff --git a/debian-rk3576-img.yaml b/debian-rk3576-img.yaml index 8f73e06..46e510e 100644 --- a/debian-rk3576-img.yaml +++ b/debian-rk3576-img.yaml @@ -1,16 +1,23 @@ {{ $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" }} +{{- /* 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 }} @@ -22,20 +29,74 @@ actions: end: 64MiB - name: root fs: btrfs + fslabel: {{ $fslabel }} + fsck: false start: 64MiB end: -1MiB mountpoints: - mountpoint: / partition: root - options: [ compress=zstd, 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 @ -> 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-default @) or U-Boot won't see this sibling /boot. - action: run - description: Enable zstd compression on the image mount + description: Create subvolumes and mount the layout chroot: false - command: mount -o remount,compress=zstd "$IMAGEMNTDIR" + command: | + set -eu + ROOTPART="$({{ $rootdev }})" + + # debos has the top level (subvolid=5) mounted here; create subvolumes on it. + for sv in @ boot @home @snapshots @var-log @var-cache; do + btrfs subvolume create "$IMAGEMNTDIR/$sv" + done + umount "$IMAGEMNTDIR" + + # Mount @ at the mount point debos owns, then nest the rest under it. + mount -t btrfs -o "{{ $btrfsopts }},subvol=@" "$ROOTPART" "$IMAGEMNTDIR" + mkdir -p "$IMAGEMNTDIR/boot" \ + "$IMAGEMNTDIR/home" \ + "$IMAGEMNTDIR/.snapshots" \ + "$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" + # @snapshots is mounted only for the build (to write the Factory Image); it's + # not in fstab, so the .snapshots mountpoint is removed from @ before detach. + mount -t btrfs -o "{{ $btrfsopts }},subvol=@snapshots" "$ROOTPART" "$IMAGEMNTDIR/.snapshots" + 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=subvol=@. - action: filesystem-deploy - append-kernel-cmdline: {{ $cmdline }} + append-kernel-cmdline: rootflags=subvol=@ {{ $cmdline }} + + # Add fstab entries for the non-root subvolumes (debos only generated the root + # line). The FSUUID is read back at build time. @snapshots is intentionally + # omitted -- it holds the Factory Image and is mounted on demand for rollback, + # never auto-mounted at runtime. + - 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" </usr/lib/... -- where that +# root's DTBs live (the kernel package installs them in every root's /usr/lib). The prefix is +# derived per entry from its own rootflags=subvol= (each profile boots a different root). The +# staged kernel/initrd live under /boot (the boot subvolume) and are never prefixed. +# Set FDT_SUBVOL_PREFIX to override the derivation (e.g. "" if the root is not a subvolume). +fdt_prefix() { # $1 = options string -> "/" (or $FDT_SUBVOL_PREFIX if set) + if [ "${FDT_SUBVOL_PREFIX+x}" = x ]; then printf '%s' "$FDT_SUBVOL_PREFIX"; return 0; fi + _sv=$(printf '%s' "$1" | tr ' ' '\n' | sed -n 's/^rootflags=subvol=//p' | tail -n1) + [ -n "$_sv" ] && printf '/%s' "$_sv" + return 0 +} + log() { [ "${KERNEL_INSTALL_VERBOSE:-0}" -gt 0 ] && echo "flipper-bls: $*" >&2; return 0; } die() { echo "flipper-bls: $*" >&2; exit 1; } trim() { printf '%s' "$1" | sed 's/^ *//; s/ *$//'; } @@ -53,8 +66,12 @@ if [ "$COMMAND" = "remove" ]; then fi [ "$COMMAND" = "add" ] || exit 0 -# --- locate the boot mount, so in-entry paths are relative to the boot partition -- -[ -n "$BOOT_MNT" ] || BOOT_MNT="$(stat -c %m "$BOOT_ROOT")" +# In-entry paths are relative to the partition U-Boot actually reads. On Flipper One +# that is always the root partition — /boot is a directory/subvolume on it, never a +# separate boot partition U-Boot mounts — so paths must keep the /boot prefix, i.e. +# BOOT_MNT=/. (Override BOOT_MNT in the environment if /boot ever becomes its own +# partition, which would make boot_rel strip the mountpoint again.) +[ -n "$BOOT_MNT" ] || BOOT_MNT="/" # absolute path under BOOT_ROOT -> path as it must appear inside a BLS entry (relative # to the boot partition; identical to the absolute path when /boot is on the rootfs). @@ -130,7 +147,7 @@ fi # devicetreedir (U-Boot fdtdir extension) — referenced in place, opt-in via a # /etc/kernel/devicetreedir file containing a true boolean. Only when no single # devicetree was configured. -DEVICETREEDIR_SRC=""; DEVICETREEDIR_ENTRY="" +DEVICETREEDIR_SRC=""; DEVICETREEDIR_REL="" if [ -z "$DEVICETREE" ] && [ -f "$CONF_ROOT/devicetreedir" ]; then _v=""; read -r _v <"$CONF_ROOT/devicetreedir" || : case "$_v" in @@ -143,7 +160,7 @@ if [ -z "$DEVICETREE" ] && [ -f "$CONF_ROOT/devicetreedir" ]; then [ -n "$DEVICETREEDIR_SRC" ] && break done [ -n "$DEVICETREEDIR_SRC" ] || die "no device-tree directory for $KERNEL_VERSION" - DEVICETREEDIR_ENTRY="$(boot_rel "$DEVICETREEDIR_SRC")" + DEVICETREEDIR_REL="$(boot_rel "$DEVICETREEDIR_SRC")" # /usr/lib/...; prefixed per entry ;; esac fi @@ -180,6 +197,8 @@ done # write_entry FILE TITLE OPTIONS FDTOVERLAYS write_entry() { _f="$1"; _title="$2"; _opts="$3"; _fdtov="$4" + # devicetreedir is prefixed with THIS entry's root subvol (U-Boot reads subvolid 5) + _dtdir=""; [ -n "$DEVICETREEDIR_REL" ] && _dtdir="$(fdt_prefix "$_opts")$DEVICETREEDIR_REL" { echo "# Boot Loader Specification type#1 entry (Flipper One)" echo "title $_title" @@ -188,9 +207,9 @@ write_entry() { [ -n "$SORT_KEY" ] && echo "sort-key $SORT_KEY" [ -n "$_opts" ] && echo "options $_opts" echo "linux $KERNEL_ENTRY" - [ -n "$DEVICETREE_ENTRY" ] && echo "devicetree $DEVICETREE_ENTRY" - [ -n "$DEVICETREEDIR_ENTRY" ] && echo "devicetreedir $DEVICETREEDIR_ENTRY" - [ -n "$_fdtov" ] && echo "devicetree-overlay $_fdtov" + [ -n "$DEVICETREE_ENTRY" ] && echo "devicetree $DEVICETREE_ENTRY" + [ -n "$_dtdir" ] && echo "devicetreedir $_dtdir" + [ -n "$_fdtov" ] && echo "devicetree-overlay $_fdtov" for _i in $INITRD_ENTRIES; do echo "initrd $_i"; done } >"$_f" return 0 # don't let a trailing empty conditional's status abort `set -e` @@ -198,11 +217,12 @@ write_entry() { # Echo the space-separated paths of the named DT overlays (flat, next to the DTBs). # Returns non-zero with no output if any one is missing. -overlay_paths() { +overlay_paths() { # $1 = subvol prefix (e.g. /@Desktop); remaining args = overlay names + _pfx="$1"; shift _paths="" for _n in "$@"; do - [ -f "$OVERLAY_DIR/$_n.dtbo" ] || return 1 - _paths="$_paths${_paths:+ }$OVERLAY_DIR/$_n.dtbo" + [ -f "$OVERLAY_DIR/$_n.dtbo" ] || return 1 # check the real on-disk path + _paths="$_paths${_paths:+ }$_pfx$OVERLAY_DIR/$_n.dtbo" done printf '%s' "$_paths" } @@ -225,10 +245,10 @@ make_title() { fi } -# Base entry == the [DEFAULT] profile. -write_entry "$ENTRY_STEM.conf" "$(make_title '[DEFAULT]')" "$BASE_OPTS" "" +# Base entry == the [DESKTOP] profile. +write_entry "$ENTRY_STEM.conf" "$(make_title '[DESKTOP]')" "$BASE_OPTS" "" -[ -f "$PROFILES" ] || { log "$PROFILES not found, only the [DEFAULT] entry written"; exit 0; } +[ -f "$PROFILES" ] || { log "$PROFILES not found, only the [DESKTOP] entry written"; exit 0; } while IFS='|' read -r tok suf extra gate dtbos legend; do tok="$(echo "$tok" | tr -d '[:space:]')" @@ -257,7 +277,7 @@ while IFS='|' read -r tok suf extra gate dtbos legend; do required=0 case "$dtbos" in '!'*) required=1; dtbos="$(trim "${dtbos#\!}")" ;; esac - paths="$(overlay_paths $dtbos)" || paths="" + paths="$(overlay_paths "$(fdt_prefix "$opts")" $dtbos)" || paths="" if [ -z "$paths" ] && [ "$required" = 1 ]; then log "skip $tok (overlays not found for $KERNEL_VERSION)" continue From 19951cbdf6aaa97db6d862b3f5ec5ec5cafd0fd5 Mon Sep 17 00:00:00 2001 From: Yury MonZon Date: Tue, 30 Jun 2026 21:43:01 +0200 Subject: [PATCH 3/4] Disable CoW on the systemd journal directory journald's append-heavy writes fragment badly under btrfs copy-on-write. Pre-create the journal directory with NODATACOW while it is still empty so the journal files written on first boot inherit it. --- debian-rk3576-img.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/debian-rk3576-img.yaml b/debian-rk3576-img.yaml index 46e510e..8360f55 100644 --- a/debian-rk3576-img.yaml +++ b/debian-rk3576-img.yaml @@ -98,6 +98,17 @@ actions: 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 source: {{ $kerneldir }} From 21f3108ab009ca49aac1aba5e51ba7520a4f26a2 Mon Sep 17 00:00:00 2001 From: Yury MonZon Date: Tue, 30 Jun 2026 21:46:06 +0200 Subject: [PATCH 4/4] Add read-only Factory Image snapshot of root Once the rootfs and kernel are fully deployed, take a read-only snapshot of @ into .snapshots/@_Factory-Image for rollback. It captures @ only; the nested subvolumes (boot/home/var-*) are separate and excluded. --- debian-rk3576-img.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/debian-rk3576-img.yaml b/debian-rk3576-img.yaml index 8360f55..f728285 100644 --- a/debian-rk3576-img.yaml +++ b/debian-rk3576-img.yaml @@ -120,6 +120,14 @@ actions: chroot: true command: find /var/cache/linux-kernel -name linux-image-\*.deb -a ! -name \*-dbg_\* -print0 | xargs -0 apt install -y + # Read-only "Factory Image" snapshot of @, taken once the rootfs and kernel are + # fully deployed. It captures @ only -- the nested subvols (boot/home/var-*) are + # separate and excluded. + - action: run + description: Create the read-only Factory Image snapshot of @ + chroot: false + command: btrfs subvolume snapshot -r "$IMAGEMNTDIR" "$IMAGEMNTDIR/.snapshots/@_Factory-Image" + # Unmount the nested subvolumes (deepest first), leaving @ mounted at # $IMAGEMNTDIR. debos' image-partition Cleanup unmounts @ and detaches the loop; # without this it would fail with EBUSY on the still-mounted children.