Add install-flex-emmc.sh for a proper ChromeOS Flex install on eMMC
The earlier raw dd of the Flex recovery image left the installer layout (2MB ROOT-B placeholder, fixed 6GB STATE), so Flex booted as live media and couldn't A/B update. The script runs the image's own chromeos-install against the eMMC from neon, chrooted into the image in a private mount namespace with all other disks masked. Handles the cros_efi cmdline requirement, udisks auto-mount interference, and unreliable exit status. Tested end to end on hp-chell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9c3b8e48ee
commit
93f4eca05c
@@ -138,13 +138,40 @@ sudo sh -c 'unzip -p "<path to .zip>" <path to .bin inside the zip> \
|
||||
| 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
|
||||
|
||||
Executable
+180
@@ -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 <eMMC> --payload_image <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 <flex .bin.zip or .bin> [dst, default /dev/mmcblk0]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SRC=${1:?usage: $0 <flex .bin.zip or .bin> [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)."
|
||||
Reference in New Issue
Block a user