From f0b2d7846aa7f126e0bf7d32a6aed5cd20fb1b31 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Mon, 27 Jul 2026 19:23:29 +0300 Subject: [PATCH] Add install-to-disk.sh, and get the GPU firmware into the initramfs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS --- README.md | 19 +- build/install-to-disk.sh | 254 +++++++++++++++++++++++++ config/packages/base.pkgs | 8 + docs/install.md | 59 +++--- overlay/etc/dracut.conf.d/10-c630.conf | 13 ++ overlay/usr/local/sbin/c630-firmware | 12 +- 6 files changed, 334 insertions(+), 31 deletions(-) create mode 100755 build/install-to-disk.sh diff --git a/README.md b/README.md index ff1a6fd..2a07a22 100644 --- a/README.md +++ b/README.md @@ -131,10 +131,23 @@ Confirmed on hardware, from a USB stick: subsystem, WiFi and IPA all appear as platform devices, so `DEVICE_CMDLINE` and the DTB are right - The framebuffer console works (`simple-framebuffer`, 240x67) -- The root filesystem mounts and systemd starts +- The root filesystem mounts, systemd starts, and it reaches a login prompt +- Networking works over a USB WiFi dongle, and sshd is reachable +- `build/install-to-disk.sh` copies it onto the internal UFS -Not yet confirmed: reaching a login prompt, and anything past it. SELinux is -shipped permissive — see below. +Not yet confirmed: booting from the internal drive rather than USB. + +Onboard WiFi does not appear at all, and audio, sensors, video decode and +accelerated graphics are all absent — every one of them waiting on the +model-signed firmware described in [docs/firmware.md](docs/firmware.md). On +this machine Windows has been wiped, so those blobs are gone; the kernel names +each missing file explicitly in `dmesg`. + +The internal drive needs `build/install-to-disk.sh` rather than `dd`: its UFS +uses 4096-byte logical sectors, which the 512-byte image geometry cannot be +written onto directly. See [docs/install.md](docs/install.md). + +SELinux is shipped permissive — see below. ### SELinux diff --git a/build/install-to-disk.sh b/build/install-to-disk.sh new file mode 100755 index 0000000..544a420 --- /dev/null +++ b/build/install-to-disk.sh @@ -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 <&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 < /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" </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 </dev/null || compgen -G "${FW_DIR}/*.elf" >/dev/null; then + printf 'install_items+=" %s/*.mbn %s/*.elf "\n' "$FW_DIR" "$FW_DIR" \ + > /etc/dracut.conf.d/20-c630-extracted-firmware.conf + echo "Extracted firmware will be included in the initramfs." +fi + +echo "Regenerating the initramfs..." dracut --force --regenerate-all cat <<'EOF'