#!/bin/bash # install-flex-emmc.sh # # Properly install ChromeOS Flex onto hp-chell's internal eMMC from the # running KDE neon system (which boots from the microSD card in the built-in # slot; its reader is USB-attached internally, so it shows as sda). # # Why not just dd the image: the Flex recovery .bin is an *installer* # image -- ROOT-B is a 2MB placeholder and STATE is a fixed ~6GB with # the rest of the disk unallocated. Flex's is_running_from_installer # compares the ROOT-A/ROOT-B sizes, so a raw dd boots as "live USB" # (Install / Try it first) and can't do A/B updates. This script instead # runs the image's own chromeos-install (what the "Install ChromeOS Flex" # button runs) against the eMMC, which lays out full-size A/B roots and a # STATE partition filling the disk. # # How: loop-mount the image, chroot into its ROOT-A, and run # chromeos-install --dst --payload_image ... # inside a private mount namespace where every block device except the # destination (and loop devices) is masked with /dev/null -- so the neon # disk is unreachable from inside the chroot. # # Afterwards, updates the "ChromeOS Flex" GRUB chainload entry in # /etc/grub.d/40_custom with the new EFI-SYSTEM filesystem UUID. # # WIPES THE DESTINATION DEVICE ENTIRELY. # # Usage: sudo ./install-flex-emmc.sh [dst, default /dev/mmcblk0] set -euo pipefail SRC=${1:?usage: $0 [dst device]} DST=${2:-/dev/mmcblk0} WORK=${WORK:-/var/tmp/flex-install} ROOTMNT=/mnt/flexchroot LOG=/var/log/flex-install.log if [ "$(id -u)" -ne 0 ]; then echo "must run as root (sudo $0 ...)" >&2 exit 1 fi [ -b "$DST" ] || { echo "$DST is not a block device" >&2; exit 1; } dst_name=$(basename "$DST") # Refuse if the destination holds the running system. root_src=$(findmnt -no SOURCE /) root_disk=$(lsblk -no PKNAME "$root_src" | head -1) if [ "$root_disk" = "$dst_name" ]; then echo "refusing: $DST holds the running root filesystem ($root_src)" >&2 exit 1 fi echo "==> destination: $DST" lsblk -o NAME,SIZE,TYPE,TRAN,PARTLABEL,MOUNTPOINTS "$DST" echo "==> running system root is on /dev/$root_disk (will be masked)" read -r -p "ERASE $DST and install ChromeOS Flex? type 'yes': " ans [ "$ans" = yes ] || { echo aborted; exit 1; } # Keep udisks (the Plasma desktop) from auto-mounting the new partitions # mid-install -- chromeos-install's sfdisk step refuses a busy disk. UDEV_RULE=/run/udev/rules.d/99-flex-install-ignore.rules mkdir -p /run/udev/rules.d LOOP= cleanup() { if [ -n "$LOOP" ]; then losetup -d "$LOOP" 2>/dev/null || true; fi rm -f "$UDEV_RULE" udevadm control --reload } trap cleanup EXIT echo "KERNEL==\"${dst_name}*\", ENV{UDISKS_IGNORE}=\"1\"" > "$UDEV_RULE" udevadm control --reload udevadm trigger --settle "$DST" # Unmount anything mounted from the destination. for p in $(lsblk -lno PATH "$DST" | tail -n +2); do for m in $(findmnt -rno TARGET "$p" || true); do umount "$m"; done done # Get a raw .bin. case "$SRC" in *.zip) mkdir -p "$WORK" bin_name=$(unzip -Z1 "$SRC" | grep '\.bin$' | head -1) IMG="$WORK/$bin_name" if [ ! -s "$IMG" ]; then echo "==> extracting $bin_name to $WORK" unzip -o -q "$SRC" "$bin_name" -d "$WORK" fi ;; *) IMG=$SRC ;; esac IMG=$(realpath "$IMG") echo "==> attaching image read-only" LOOP=$(losetup -r -P -f --show "$IMG") mkdir -p "$ROOTMNT" efibootmgr > /var/log/flex-install-efiboot-before.txt 2>&1 || true echo "==> running chromeos-install (log: $LOG); this takes a while on eMMC" # Everything below runs in a private mount namespace; all mounts vanish # when it exits. unshare -m --propagation private bash -euo pipefail -c ' LOOP=$1; R=$2; IMG=$3; DSTNAME=$4 mount -o ro,noload "${LOOP}p3" "$R" mount -t tmpfs tmpfs "$R/tmp" mount -t tmpfs tmpfs "$R/run" mount -t tmpfs tmpfs "$R/var" # postinst touches .install_completed on the *installer'"'"'s* stateful # partition; give it somewhere writable (discarded afterwards). mount -t tmpfs tmpfs "$R/mnt/stateful_partition" mount --bind /dev "$R/dev" mount -t devpts devpts "$R/dev/pts" 2>/dev/null || true mount -t proc proc "$R/proc" mount --bind /sys "$R/sys" # chromeos_postinst picks the bootloader setup from a cros_XXX flag on # the kernel cmdline ("No recognized cros_XXX bios option" otherwise); # neon obviously has none, so fake cros_efi. echo "$(cat /proc/cmdline) cros_efi" > "$R/tmp/cmdline" mount --bind "$R/tmp/cmdline" "$R/proc/cmdline" # chromeos-postinst re-chroots into the freshly written root with a plain # (non-recursive) bind of /proc, which drops that overmount. Wrap the # image'"'"'s chroot (a coreutils shim) to re-apply it in the target root. cat > "$R/tmp/chroot-wrap" <<"WRAP" #!/bin/sh for a in "$@"; do case "$a" in -*) ;; *) d=$a; break ;; esac; done if [ -n "$d" ] && [ -f "$d/proc/cmdline" ] && ! grep -q cros_efi "$d/proc/cmdline"; then mount --bind /tmp/cmdline "$d/proc/cmdline" fi exec /usr/bin/coreutils --coreutils-prog=chroot "$@" WRAP chmod 755 "$R/tmp/chroot-wrap" mount --bind "$R/tmp/chroot-wrap" "$R/bin/chroot" # Mask every disk (and its partitions) except the destination. for n in "$R"/dev/sd* "$R"/dev/nvme* "$R"/dev/mmcblk* "$R"/dev/vd*; do [ -b "$n" ] || continue case "$(basename "$n")" in "$DSTNAME"|"$DSTNAME"p[0-9]*) ;; *) mount --bind /dev/null "$n" ;; esac done mount -t tmpfs tmpfs "$R/dev/disk" mount -t tmpfs tmpfs "$R/dev/block" touch "$R/tmp/payload.bin" mount --bind "$IMG" "$R/tmp/payload.bin" chroot "$R" env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin HOME=/tmp TERM=dumb \ chromeos-install --dst "/dev/$DSTNAME" --payload_image /tmp/payload.bin \ --skip_dst_removable --yes --target_bios efi --debug ' _ "$LOOP" "$ROOTMNT" "$IMG" "$dst_name" > "$LOG" 2>&1 || true # chromeos-install can exit 0 even when its inner chromeos-install.sh # failed, so check the log rather than trusting the exit status. (It also # logs a few harmless "ERROR cros_installer" lines -- metrics etc. -- even # on success, so key off its final result lines instead.) if grep -q '^Error: ' "$LOG" || ! grep -q "^Installation to '/dev/$dst_name' complete" "$LOG"; then echo "chromeos-install FAILED; see $LOG. Tail:" >&2 tail -30 "$LOG" >&2 exit 1 fi echo "==> chromeos-install succeeded" partprobe "$DST" 2>/dev/null || true udevadm settle echo "==> new layout:" lsblk -o NAME,SIZE,FSTYPE,PARTLABEL "$DST" # Point the GRUB chainload entry at the new EFI-SYSTEM partition. esp=$(lsblk -lno PATH,PARTLABEL "$DST" | awk '$2=="EFI-SYSTEM"{print $1}') esp_uuid=$(blkid -s UUID -o value "$esp") custom=/etc/grub.d/40_custom if grep -q 'ChromeOS Flex' "$custom" 2>/dev/null; then echo "==> updating $custom to EFI-SYSTEM UUID $esp_uuid" sed -i "/ChromeOS Flex/,/^}/ s/--fs-uuid --set=root [0-9A-Fa-f-]*/--fs-uuid --set=root $esp_uuid/" "$custom" update-grub else echo "==> no ChromeOS Flex entry in $custom; EFI-SYSTEM UUID is $esp_uuid" fi echo "==> UEFI boot entries (compare with /var/log/flex-install-efiboot-before.txt):" efibootmgr echo "==> done. First Flex boot should go straight to setup (no Install/Try screen)."