update: let kernel and DTB fixes flow through pacman via flash-sync

Until now anything living on the FAT boot partition (kernel Image and
the device trees) was reflash-only, breaking the promise that every
update can arrive either by rewriting the image or through pacman and
the ES update menu. Close the gap with three pieces:

- The linux package now ships the kernel image in the rootfs under
  /usr/share/bootloader/Image next to the device_trees it already
  installs, so a pacman upgrade of linux delivers both.
- archr-flash-sync copies Image to /flash/KERNEL (refreshing the
  KERNEL.md5 sidecar) and device_trees/*.dtb to /flash/dtbs, but only
  the files whose sha256 actually changed, each written as a verified
  .new temp in the same FAT directory and renamed over the original so
  the torn-write window is the rename alone. /flash/overlays (the
  user's panel overlay), extlinux, boot scripts and u-boot are never
  touched; u-boot/TPL live in raw sectors where a torn write bricks
  the card, so the bootloader intentionally stays image-flash only.
- An alpm PostTransaction hook (HookDir /etc/pacman.d/hooks) fires the
  sync whenever a transaction touches the bootloader payload.

gen-pacman-repo now prunes the .image build-plumbing directory from
packages, which would otherwise dump a duplicate raw kernel at /.image
on every update.

Hardware test pending: run archr-flash-sync by hand over SSH and check
the device still boots with the synced KERNEL and dtbs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Douglas Teles
2026-07-07 20:10:49 -03:00
parent 56cd1626d6
commit 6faa39c570
5 changed files with 141 additions and 1 deletions
+6
View File
@@ -146,6 +146,12 @@ EOF
cp ${PKG_DIR}/sources/scripts/* ${INSTALL}/usr/bin
chmod 0755 ${INSTALL}/usr/bin/* 2>/dev/null ||:
# alpm hooks (pacman.conf points HookDir at /etc/pacman.d/hooks): the
# flash-sync hook keeps the FAT boot partition in step with kernel or
# DTB updates delivered through pacman.
mkdir -p ${INSTALL}/etc/pacman.d/hooks
cp ${PKG_DIR}/sources/pacman-hooks/*.hook ${INSTALL}/etc/pacman.d/hooks
### Install man pages
if [ -d "${PKG_DIR}/sources/man" ]; then
for page in ${PKG_DIR}/sources/man/*.[1-9]; do
@@ -0,0 +1,15 @@
# Sync the kernel and device trees delivered by pacman into the FAT
# boot partition (/flash), so kernel-side fixes reach users through the
# normal update flow. See /usr/bin/archr-flash-sync for the safety
# model; u-boot/TPL stays image-flash only.
[Trigger]
Operation = Install
Operation = Upgrade
Type = Path
Target = usr/share/bootloader/Image
Target = usr/share/bootloader/device_trees/*
[Action]
Description = Syncing kernel and device trees to the boot partition...
When = PostTransaction
Exec = /usr/bin/archr-flash-sync
@@ -0,0 +1,111 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
# Copyright (C) 2026-present ArchR (https://github.com/archr-linux/Arch-R)
#
# archr-flash-sync: bring the boot partition (FAT, /flash) in line with
# the kernel and device trees shipped in the rootfs. This is what lets
# kernel and DTB fixes flow through `pacman -Syu` / the ES update menu
# instead of demanding a reflash: the linux package delivers the new
# files under /usr/share/bootloader and an alpm PostTransaction hook
# runs this script to copy the changed ones over.
#
# Safety model: each target is written as <name>.new in the same FAT
# directory, synced, read back and hash-verified, then renamed over the
# old file. The window where a power cut leaves a torn file is the
# rename itself, the smallest FAT allows. Anything user-owned on the
# FAT is never touched: /flash/overlays (the user's mipi-panel.dtbo),
# extlinux/, boot.scr, boot.ini.
#
# u-boot/TPL (idbloader) stays OUT of scope on purpose: it lives in raw
# sectors before the first partition and a torn write there bricks the
# card with no recovery besides a reflash. Bootloader updates remain
# image-flash only.
SRC="/usr/share/bootloader"
BOOT="/flash"
log() { echo "flash-sync: $*"; }
if [ ! -d "${BOOT}" ] || ! grep -q " ${BOOT} " /proc/mounts; then
log "no boot partition mounted at ${BOOT}, nothing to do"
exit 0
fi
if ! mount -o remount,rw "${BOOT}" 2>/dev/null; then
log "WARN: could not remount ${BOOT} read-write, skipping"
exit 0
fi
finish() {
sync
mount -o remount,ro "${BOOT}" 2>/dev/null
}
trap finish EXIT
CHANGED=0
FAILED=0
# sync_file <src> <dst>: copy only when the content differs, via a
# verified temp file in the destination directory.
sync_file() {
s="$1"
d="$2"
[ -f "${s}" ] || return 0
src_sum=$(sha256sum "${s}" | cut -d' ' -f1)
if [ -f "${d}" ]; then
dst_sum=$(sha256sum "${d}" | cut -d' ' -f1)
[ "${src_sum}" = "${dst_sum}" ] && return 0
fi
tmp="${d}.new"
if ! cp "${s}" "${tmp}" 2>/dev/null; then
log "ERROR: writing ${tmp} failed (FAT full?)"
rm -f "${tmp}" 2>/dev/null
FAILED=1
return 1
fi
sync
new_sum=$(sha256sum "${tmp}" | cut -d' ' -f1)
if [ "${new_sum}" != "${src_sum}" ]; then
log "ERROR: verify of ${tmp} failed, keeping the old file"
rm -f "${tmp}" 2>/dev/null
FAILED=1
return 1
fi
mv -f "${tmp}" "${d}"
sync
log "updated ${d}"
CHANGED=1
return 0
}
# Kernel: /usr/share/bootloader/Image -> /flash/KERNEL. Refresh the
# sidecar md5 too (legacy .update flow reads it with the target/KERNEL
# name baked in).
if [ -f "${SRC}/Image" ]; then
if sync_file "${SRC}/Image" "${BOOT}/KERNEL" && [ "${CHANGED}" = "1" ]; then
md5=$(md5sum "${BOOT}/KERNEL" | cut -d' ' -f1)
printf '%s target/KERNEL\n' "${md5}" > "${BOOT}/KERNEL.md5" 2>/dev/null
fi
else
log "no ${SRC}/Image in this rootfs (older build), kernel left alone"
fi
# Device trees: only into the dtbs/ dir the image layout uses. The
# user's panel overlay lives in /flash/overlays and is never touched.
if [ -d "${SRC}/device_trees" ] && [ -d "${BOOT}/dtbs" ]; then
for dtb in "${SRC}/device_trees/"*.dtb; do
[ -f "${dtb}" ] || continue
sync_file "${dtb}" "${BOOT}/dtbs/$(basename "${dtb}")"
done
fi
if [ "${FAILED}" = "1" ]; then
log "finished WITH ERRORS, the untouched files remain bootable"
elif [ "${CHANGED}" = "1" ]; then
log "boot partition updated, changes take effect on the next boot"
else
log "boot partition already up to date"
fi
exit 0
+5
View File
@@ -378,6 +378,11 @@ makeinstall_target() {
if [ "${BOOTLOADER}" = "u-boot" ]; then
mkdir -p ${INSTALL}/usr/share/bootloader
# Ship the kernel image in the rootfs as well: pacman carries it to
# updated systems and archr-flash-sync copies it over /flash/KERNEL
# (the FAT copy u-boot actually boots). Fresh images stay in sync by
# construction; this closes the update-without-reflash path.
cp -p arch/${TARGET_KERNEL_ARCH}/boot/${KERNEL_TARGET} ${INSTALL}/usr/share/bootloader/
for dtb in arch/${TARGET_KERNEL_ARCH}/boot/dts/**/*.dtb; do
if [ -f ${dtb} ]; then
if [ "${DEVICE}" = "H700" -o "${DEVICE}" = "RK3326" -o "${DEVICE}" = "RK3399" -o "${DEVICE}" = "RK3566" -o "${DEVICE}" = "RK3588" ]; then
+4 -1
View File
@@ -139,7 +139,10 @@ EOF
filelist="$(mktemp)"
{
echo ".PKGINFO"
find . -mindepth 1 -maxdepth 1 -not -name '.PKGINFO' -printf '%P\n'
# .image is build-system plumbing (raw kernel + symbols the
# image script consumes); shipping it would dump ~25MB at
# /.image on every kernel update for nothing.
find . -mindepth 1 -maxdepth 1 -not -name '.PKGINFO' -not -name '.image' -printf '%P\n'
} > "${filelist}"
tar --zstd -cf "${outfile}" --owner=0 --group=0 -T "${filelist}"
rm -f "${filelist}"