diff --git a/CLAUDE.md b/CLAUDE.md index f5de196..da5c861 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -138,13 +138,40 @@ sudo sh -c 'unzip -p "" \ | dd of=/dev/mmcblk0 bs=4M status=progress conv=fsync' ``` -Unzipped image is ~8.9GB; eMMC is 29GB, plenty of room (ChromeOS auto- -expands the stateful partition to fill the rest on first real boot). -eMMC write speed degraded over the transfer (~130MB/s at the start down +Unzipped image is ~8.9GB; eMMC is 29GB. (Original note here claimed +ChromeOS auto-expands the stateful partition on first boot — **wrong**, +see the 2026-09-27 reinstall below.) eMMC write speed degraded over the transfer (~130MB/s at the start down to ~9MB/s by the end, total ~18 min for 8.9GB) — looks like the eMMC's SLC write cache exhausting partway through; expected/normal for eMMC of this class, not a fault. +**2026-09-27: reinstalled properly with `install-flex-emmc.sh`.** The +raw `dd` above left the *installer* image layout: ROOT-B a 2MB +placeholder, STATE a fixed 6GB, ~20.7GB unallocated. Flex's +`/usr/sbin/is_running_from_installer` compares ROOT-A/ROOT-B sizes, so +it booted as live media ("Install ChromeOS Flex / Try it first") and +couldn't do A/B updates. There is no flag to flip; the fix is running +the image's own `chromeos-install` (what the Install button runs) +against the eMMC. `install-flex-emmc.sh` (in this repo) does that from +neon: loop-attaches the `.bin`, chroots into its ROOT-A inside a private +mount namespace with every other disk (incl. neon's `sda`) masked by +`/dev/null`, and runs `chromeos-install --dst /dev/mmcblk0 +--payload_image ... --target_bios efi`. Gotchas it handles: +- `chromeos_postinst` needs a `cros_efi` flag on the kernel cmdline; + faked via a bind over `/proc/cmdline`, plus a wrapper on the image's + `chroot` because postinst re-chroots with a non-recursive `/proc` bind. +- Plasma/udisks auto-mounts new eMMC partitions mid-install, making + `sfdisk` refuse a busy disk — a temporary `UDISKS_IGNORE` udev rule + in `/run/udev/rules.d` prevents it. +- `chromeos-install` can exit 0 on failure and logs harmless `ERROR` + lines on success — judge by its final `Installation to ... complete.` + line. Log: `/var/log/flex-install.log`. +- Whole run is only ~2.5 min (partitions copied at eMMC speed). +Result: ROOT-A/ROOT-B 4GB each, STATE 20.6GB (fills disk), KERN-A +`successful=1`. The ESP is copied from the image so its FS UUID stays +`CBE4-703E` (script still re-points `40_custom` in case it changes); +UEFI boot entries/order untouched. + **Boot selection** — both OSes are reachable two ways: 1. **From GRUB** (neon's own boot menu, visible for 5s per `GRUB_TIMEOUT`): a custom entry "ChromeOS Flex (internal eMMC)" was diff --git a/install-flex-emmc.sh b/install-flex-emmc.sh new file mode 100755 index 0000000..84ec56d --- /dev/null +++ b/install-flex-emmc.sh @@ -0,0 +1,180 @@ +#!/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 an external USB/SD card). +# +# 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)."