#!/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 <