Add install-to-disk.sh, and get the GPU firmware into the initramfs
Some checks failed
build image / build (push) Has been cancelled

Validated by running it on the machine, twice, and inspecting the result.

Two properties of this laptop rule out the obvious approach, and the script
exists mainly to encode them. Its internal UFS reports 4096-byte logical
sectors, so the image — built with 512-byte geometry — cannot be dd'd onto it;
the GPT header and every partition offset would land in the wrong place. And
there are no EFI runtime variables, so efibootmgr cannot register a boot entry
and GRUB has to sit at the removable-media path where the firmware looks
unprompted.

Three things the validation runs caught that review would not have:

rsync is not in the image. I had put it in the build container and never in
the package list, so the first run died at the copy. It now falls back to tar
(--xattrs-include='*', or SELinux labels are silently dropped and the result
does not boot), and rsync is in base.pkgs for the progress output.

Copying a live root makes tar exit non-zero — files change underneath it, and
this machine's clock is wrong besides, so every mtime looks like it is in the
future. With pipefail that aborted the install after the root filesystem and
before /boot, leaving a half-installed disk that looked plausible. Warning-level
exits are now tolerated and only a fatal exit 2 stops the run.

systemd-machine-id-setup keeps an existing valid id, and one had just been
copied off the stick, so the installed system was a clone. The file is removed
first now.

Also: msm_dpu probes ~6s in, while the initramfs is still root, and asks for
qcom/a630_sqe.fw before the real filesystem carrying it is reachable. It never
retries. Adding the Adreno firmware to the initramfs is a few tens of
kilobytes. c630-firmware does the same for the DSP blobs once they exist, since
dracut rejects install_items globs that match nothing.

chrony, because the RTC reads 1970 and nothing was correcting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS
This commit is contained in:
2026-07-27 19:23:29 +03:00
parent 482c5d9c9a
commit f0b2d7846a
6 changed files with 334 additions and 31 deletions

254
build/install-to-disk.sh Executable file
View File

@@ -0,0 +1,254 @@
#!/usr/bin/bash
#
# Copy the running system onto the C630's internal storage.
#
# Run this from the USB-booted image, on the laptop itself:
#
# sudo ./install-to-disk.sh --dry-run # show the plan, touch nothing
# sudo ./install-to-disk.sh # do it, after confirmation
#
# Why this rather than dd'ing the image at the internal drive:
#
# * The internal UFS reports 4096-byte logical sectors. The disk image is
# built with 512-byte sector geometry, so its GPT header and every
# partition offset would land in the wrong place. Partitioning has to
# happen natively, on the device, at its own sector size.
#
# * There are no EFI variables on this machine — efibootmgr reports "EFI
# variables are not supported on this system" — so nothing can register a
# boot entry. GRUB has to sit at the removable-media path,
# EFI/BOOT/BOOTAA64.EFI, where the firmware looks without being told.
set -euo pipefail
TARGET=/dev/sda
ESP_SIZE=1GiB
BOOT_SIZE=1GiB
ASSUME_YES=0
DRY_RUN=0
usage() {
cat <<EOF
Usage: sudo $0 [options]
--target DEV Disk to install onto (default: ${TARGET})
--esp-size SIZE EFI system partition (default: ${ESP_SIZE})
--boot-size SIZE /boot partition (default: ${BOOT_SIZE})
--dry-run Print the plan and exit without touching anything
--yes Skip the confirmation prompt
-h, --help This message
Everything on the target disk is destroyed.
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--target) TARGET="$2"; shift 2 ;;
--esp-size) ESP_SIZE="$2"; shift 2 ;;
--boot-size) BOOT_SIZE="$2"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;;
--yes) ASSUME_YES=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
die() { echo "error: $*" >&2; exit 1; }
log() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
[ "$(id -u)" -eq 0 ] || die "must run as root"
[ -b "$TARGET" ] || die "$TARGET is not a block device"
# nvme0n1 -> nvme0n1p1, sda -> sda1
partdev() { case "$TARGET" in *[0-9]) echo "${TARGET}p$1" ;; *) echo "${TARGET}$1" ;; esac; }
ESP_PART=$(partdev 1); BOOT_PART=$(partdev 2); ROOT_PART=$(partdev 3)
# --- refuse to eat the system we are running from ------------------------
RUNNING_ROOT="$(findmnt -no SOURCE /)"
RUNNING_DISK="/dev/$(lsblk -no pkname "$RUNNING_ROOT" 2>/dev/null || true)"
if [ "$RUNNING_DISK" = "$TARGET" ]; then
die "$TARGET holds the running root filesystem ($RUNNING_ROOT). Boot the USB image and try again."
fi
while read -r mnt; do
[ -n "$mnt" ] && die "$TARGET has a mounted partition at $mnt — unmount it first"
done < <(lsblk -nro MOUNTPOINT "$TARGET" | grep -v '^$' || true)
SIZE_H="$(lsblk -dno SIZE "$TARGET")"
SECTOR="$(blockdev --getss "$TARGET")"
cat <<EOF
Target ${TARGET} (${SIZE_H}, ${SECTOR}-byte logical sectors)
Running from ${RUNNING_ROOT}
${ESP_PART} ${ESP_SIZE} fat32 ESP -> /boot/efi
${BOOT_PART} ${BOOT_SIZE} ext4 boot -> /boot
${ROOT_PART} rest ext4 fedora -> /
EOF
if [ "$DRY_RUN" = 1 ]; then
echo "dry run — nothing written"
exit 0
fi
if [ "$ASSUME_YES" != 1 ]; then
echo "This destroys everything on ${TARGET}."
read -rp "Type the target device to confirm: " reply
[ "$reply" = "$TARGET" ] || die "not confirmed"
fi
MNT=$(mktemp -d /tmp/c630-install.XXXXXX)
cleanup() { umount -R "$MNT" 2>/dev/null || true; rmdir "$MNT" 2>/dev/null || true; }
trap cleanup EXIT
# ---------------------------------------------------------------------------
log "Partitioning ${TARGET}"
# ---------------------------------------------------------------------------
# sgdisk works in the device's own sector size, which is the entire point of
# doing this here rather than in the image.
wipefs -a "$TARGET" >/dev/null
sgdisk --zap-all "$TARGET" >/dev/null
sgdisk \
--new "1:0:+${ESP_SIZE}" --typecode 1:ef00 --change-name 1:ESP \
--new "2:0:+${BOOT_SIZE}" --typecode 2:8300 --change-name 2:boot \
--new "3:0:0" --typecode 3:8300 --change-name 3:root \
"$TARGET" >/dev/null
partprobe "$TARGET" 2>/dev/null || true
udevadm settle
sgdisk --print "$TARGET"
# ---------------------------------------------------------------------------
log "Creating filesystems"
# ---------------------------------------------------------------------------
# Let mkfs.vfat take the device's 4096-byte sectors rather than forcing 512 —
# this firmware booted Windows off this disk, so it reads it natively.
mkfs.vfat -F 32 -n ESP "$ESP_PART" >/dev/null
# orphan_file and metadata_csum_seed are recent ext4 features some GRUB builds
# cannot read, and GRUB has to read /boot.
mkfs.ext4 -q -F -O ^orphan_file,^metadata_csum_seed -L boot "$BOOT_PART"
mkfs.ext4 -q -F -O ^orphan_file,^metadata_csum_seed -L fedora "$ROOT_PART"
ROOT_UUID=$(blkid -s UUID -o value "$ROOT_PART")
BOOT_UUID=$(blkid -s UUID -o value "$BOOT_PART")
ESP_UUID=$(blkid -s UUID -o value "$ESP_PART")
echo "root=${ROOT_UUID} boot=${BOOT_UUID} esp=${ESP_UUID}"
mount "$ROOT_PART" "$MNT"
mkdir -p "$MNT/boot"
mount "$BOOT_PART" "$MNT/boot"
mkdir -p "$MNT/boot/efi"
mount "$ESP_PART" "$MNT/boot/efi"
# Copy one filesystem's worth of tree, hard links, ACLs and xattrs intact.
# rsync is not in the minimal image and tar is, so tar is the fallback rather
# than the exception. --xattrs-include='*' matters: without it tar drops
# security.selinux, and an unlabelled root does not boot.
copy_tree() {
local src="$1" dst="$2"
if command -v rsync >/dev/null; then
# 24 is "some files vanished while transferring", which is normal when
# copying a filesystem that is in use.
rsync -aHAX -x --info=progress2 "$src/" "$dst/" || [ $? -eq 24 ]
return
fi
# This copies a *live* root, so files legitimately change underneath us and
# tar exits 1 to say so. The clock is also often wrong on this machine
# (no working RTC), which makes every mtime look like it is in the future.
# Neither is a reason to abandon the install — but exit 2 is.
local st
set +e
tar --create --file - --one-file-system --numeric-owner \
--warning=no-timestamp --warning=no-file-changed --warning=no-file-removed \
--xattrs --xattrs-include='*' --acls --selinux -C "$src" . \
| tar --extract --file - --numeric-owner \
--warning=no-timestamp \
--xattrs --xattrs-include='*' --acls --selinux -C "$dst"
st=("${PIPESTATUS[@]}")
set -e
[ "${st[0]}" -le 1 ] || die "reading ${src} failed (tar exit ${st[0]})"
[ "${st[1]}" -le 1 ] || die "writing ${dst} failed (tar exit ${st[1]})"
}
# ---------------------------------------------------------------------------
log "Copying the root filesystem"
# ---------------------------------------------------------------------------
# One filesystem only, so /boot, /boot/efi and the pseudo-filesystems are all
# skipped here and dealt with separately (or not at all).
copy_tree / "$MNT"
log "Copying /boot"
copy_tree /boot "$MNT/boot"
log "Copying the ESP"
# vfat carries no ownership, permissions or xattrs, so do not ask for any.
cp -r /boot/efi/. "$MNT/boot/efi/"
# ---------------------------------------------------------------------------
log "Pointing the new system at itself"
# ---------------------------------------------------------------------------
cat > "$MNT/etc/fstab" <<EOF
UUID=${ROOT_UUID} / ext4 defaults 1 1
UUID=${BOOT_UUID} /boot ext4 defaults 1 2
UUID=${ESP_UUID} /boot/efi vfat umask=0077,shortname=winnt 0 2
EOF
# Every place the old root UUID appears has to become the new one, or the
# installed system quietly boots the USB stick's root — or nothing at all.
OLD_ROOT_UUID=$(blkid -s UUID -o value "$RUNNING_ROOT")
OLD_BOOT_UUID=$(findmnt -no UUID /boot 2>/dev/null || true)
for f in "$MNT"/boot/loader/entries/*.conf; do
[ -e "$f" ] || continue
sed -i "s/${OLD_ROOT_UUID}/${ROOT_UUID}/g" "$f"
echo " $(basename "$f")"
done
[ -f "$MNT/etc/kernel/cmdline" ] &&
sed -i "s/${OLD_ROOT_UUID}/${ROOT_UUID}/g" "$MNT/etc/kernel/cmdline"
if [ -n "$OLD_BOOT_UUID" ]; then
sed -i "s/${OLD_BOOT_UUID}/${BOOT_UUID}/g" "$MNT/boot/grub2/grub.cfg"
for g in "$MNT"/boot/efi/EFI/*/grub.cfg; do
[ -e "$g" ] && sed -i "s/${OLD_BOOT_UUID}/${BOOT_UUID}/g" "$g"
done
fi
# No EFI variables on this machine, so the firmware will only find a bootloader
# at the removable-media path. Make sure one is there.
mkdir -p "$MNT/boot/efi/EFI/BOOT"
if [ -f "$MNT/boot/efi/EFI/fedora/grubaa64.efi" ]; then
cp "$MNT/boot/efi/EFI/fedora/grubaa64.efi" "$MNT/boot/efi/EFI/BOOT/BOOTAA64.EFI"
fi
# A fresh identity, so the installed system is not a clone of the stick — two
# machines sharing one machine-id confuses DHCP leases and journal collection.
# The removal matters: systemd-machine-id-setup keeps an existing valid id, and
# one was just copied off the stick, so without this it is a no-op.
#
# Writing a real id rather than leaving the file empty also keeps systemd from
# treating the next boot as a first boot and prompting on the console for
# locale and passwords — which, on a machine reached over ssh, would look
# exactly like a hang.
rm -f "$MNT/etc/machine-id"
systemd-machine-id-setup --root="$MNT" >/dev/null 2>&1 ||
uuidgen | tr -d - > "$MNT/etc/machine-id"
# The root partition already fills the disk, so there is nothing to grow.
mkdir -p "$MNT/var/lib/c630" && touch "$MNT/var/lib/c630/growfs-done"
sync
log "Done"
cat <<EOF
Installed to ${TARGET}. Reboot and pick the internal drive from the boot menu
(Fn+F12), or remove the USB stick.
root ${ROOT_UUID}
boot ${BOOT_UUID}
esp ${ESP_UUID}
EOF