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'