Files
bee/iso/builder/lib/fast-path.sh
T
Mikhail ChusavitinandClaude Sonnet 5 b8c45d54c1 feat(iso): split the live medium into semantic SquashFS layers
Booting via BMC virtual CD reads the ~2.8 GB filesystem squashfs
sequentially during the live-boot toram copy; a mid-read drop of the
redirected medium loses the whole copy and fails the boot (v14). Split
the rootfs into self-contained semantic layers so a retry re-reads at
most one ~500-700 MiB layer, not everything. This is a resilience /
reduced-re-read mechanism, not a fix for the virtual-media instability.

NVIDIA variants now ship 7 layers (00-base, 05-firmware, 08-desktop,
10-nvidia-driver, 20-nvidia-platform, 30-nvidia-cuda-libs,
40-nvidia-dcgm-cuda) plus an explicit live/filesystem.module that fixes
their OverlayFS order; amd/nogpu keep a single squashfs.

- lib/squashfs-layers.sh: deterministic classifier (dpkg file ownership
  plus explicit rules for build.sh-injected files, never a path
  substring), per-layer mksquashfs, 800 MiB hard ceiling, unsquashfs -s
  plus strict extraction of every layer, merged-rootfs bootability check.
- build.sh: split the monolith after the full lb build, verify and merge,
  write the module file, delete the monolith only then; abort before ISO
  assembly on any failure. Runs the builder test suites up front.
- fast-path: force a full build for a multi-layer medium;
  fast_path_repack_squashfs hard-refuses (it would drop layers).
- iso-validation.sh: validate_iso_squashfs_layers (module vs layer set
  match, size ceiling, no lone giant squashfs) and
  validate_iso_media_integrity (xorriso -check_media).
- bee-install: honour filesystem.module order, abort on any layer failure.
- 9013-toram-retry: record the real rsync exit code (it printed a false
  rc=0) and correct the "resumes the tail" comment (rsync without
  --partial keeps only fully-copied layers). No unsafe partial resume.
- tests: test-squashfs-layers.sh plus a multi-layer guard in
  test-build-libs.sh; both run at the top of every build.
- docs: bible-local architecture and decision, iso/README, iso-build-rules.

Verified by a full nvidia build: 7 layers 622/199/256/466/37/567/562 MiB,
every validator passes, xorriso -check_media good, merged rootfs bootable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:38:14 +03:00

240 lines
10 KiB
Bash
Executable File

copy_memtest_from_deb() {
deb="$1"
dst_boot="$2"
tmpdir="$(mktemp -d)"
dpkg-deb -x "$deb" "$tmpdir"
for f in memtest86+x64.bin memtest86+x64.efi; do
if [ -f "$tmpdir/boot/$f" ]; then
cp "$tmpdir/boot/$f" "$dst_boot/$f"
fi
done
rm -rf "$tmpdir"
}
reset_live_build_stage() {
lb_dir="$1"
stage="$2"
for root in \
"$lb_dir/.build" \
"$lb_dir/.stage" \
"$lb_dir/auto"; do
[ -d "$root" ] || continue
find "$root" -maxdepth 1 \( -name "${stage}" -o -name "${stage}.*" -o -name "*${stage}*" \) -exec rm -rf {} + 2>/dev/null || true
done
}
# State written after every successful full lb build for this variant. Keep it
# outside the rsync-managed live-build workdir so source synchronization cannot
# delete the state that decides whether the fast path is safe.
FULL_BUILD_STATE_DIR="${CACHE_ROOT}/full-build-state-${BUILD_VARIANT}"
mkdir -p "${FULL_BUILD_STATE_DIR}"
FULL_BUILD_MARKER="${FULL_BUILD_STATE_DIR}/complete"
FULL_BUILD_HASH_FILE="${FULL_BUILD_STATE_DIR}/heavy-config.sha256"
FULL_BUILD_ABI_FILE="${FULL_BUILD_STATE_DIR}/kernel-abi"
FULL_BUILD_OVERLAY_MANIFEST="${FULL_BUILD_STATE_DIR}/overlay.manifest"
# Hashes the content of every "heavy" build input: the build scripts themselves
# (build.sh, build-in-container.sh, the build-*.sh helpers, lib/*.sh) plus
# VERSIONS, package lists, hooks, archives, auto/, and the Dockerfile. If any of
# these change, the previous squashfs was produced by different logic or pins
# and the fast path is not safe — a change to build.sh or lib/fast-path.sh is as
# heavy as a package-list change, not a "light" file.
#
# Bootloader templates (config/bootloaders) are deliberately excluded: the fast
# path regenerates the complete outer ISO layer from them every time.
#
# Content-based, not mtime-based: mtimes get reset by git checkouts, rsync, and
# retried builds in ways that don't track "did this content actually change
# since the last full build", which previously let needs_full_build() silently
# take the fast path (reusing an old squashfs) with no error.
hash_heavy_config() {
(
cd "${BUILDER_DIR}"
find . -type f \
\( -name '*.sh' \
-o -path './VERSIONS' \
-o -path './Dockerfile' \
-o -path './auto/*' \
-o -path './config/package-lists/*' \
-o -path './config/hooks/*' \
-o -path './config/archives/*' \) \
-print0 2>/dev/null |
LC_ALL=C sort -z |
xargs -0 -r sha256sum
) | sha256sum | awk '{print $1}'
}
write_overlay_manifest() {
out_path="$1"
(
cd "${OVERLAY_STAGE_DIR}"
find . -mindepth 1 -printf '%y %P\n' | sort
) > "$out_path"
}
overlay_paths_were_removed() {
[ -f "${FULL_BUILD_OVERLAY_MANIFEST}" ] || return 0
current_manifest="$(mktemp)"
write_overlay_manifest "$current_manifest"
if comm -23 "${FULL_BUILD_OVERLAY_MANIFEST}" "$current_manifest" | grep -q .; then
rm -f "$current_manifest"
return 0
fi
rm -f "$current_manifest"
return 1
}
# Returns 0 if full lb build is needed, 1 if fast-path is safe.
# Fast-path is safe when only light files changed since the last full build
# (Go source, overlay scripts/configs). Heavy changes (VERSIONS, package lists,
# hooks, archives, Dockerfile, auto/config) require a full lb build.
needs_full_build() {
[ -f "${FULL_BUILD_MARKER}" ] || return 0
[ -f "${FULL_BUILD_HASH_FILE}" ] || return 0
[ -f "${FULL_BUILD_ABI_FILE}" ] || return 0
[ -f "${FULL_BUILD_OVERLAY_MANIFEST}" ] || return 0
[ -f "${BUILD_WORK_DIR}/live-image-amd64.hybrid.iso" ] || return 0
# Accept any versioned squashfs (filesystem-v*.squashfs or legacy filesystem.squashfs)
_any_sq=$(find "${BUILD_WORK_DIR}/binary/live" -maxdepth 1 \
-name 'filesystem*.squashfs' 2>/dev/null | head -1)
[ -n "$_any_sq" ] || return 0
# Multi-layer live medium: the fast path only knows how to repack a single
# squashfs and would drop every other layer. Force a full build until a
# layer-aware repack exists. Detected by the module file or >1 squashfs.
if [ -f "${BUILD_WORK_DIR}/binary/live/filesystem.module" ] || \
[ "$(find "${BUILD_WORK_DIR}/binary/live" -maxdepth 1 -name 'filesystem*.squashfs' 2>/dev/null | wc -l)" -gt 1 ]; then
echo "=== full build required: previous build produced a multi-layer squashfs medium ==="
return 0
fi
_old_abi="$(cat "${FULL_BUILD_ABI_FILE}" 2>/dev/null)"
if [ "${DEBIAN_KERNEL_ABI}" != "$_old_abi" ]; then
echo "=== full build required: kernel ABI changed (${_old_abi:-unknown} -> ${DEBIAN_KERNEL_ABI}) ==="
return 0
fi
if overlay_paths_were_removed; then
echo "=== full build required: overlay paths were removed or changed type ==="
return 0
fi
_new_hash="$(hash_heavy_config)"
_old_hash="$(cat "${FULL_BUILD_HASH_FILE}" 2>/dev/null)"
if [ "$_new_hash" != "$_old_hash" ]; then
echo "=== full build required: heavy config content changed since last full build ==="
return 0
fi
return 1
}
# Fast path: unsquash existing filesystem, rsync overlay on top, repack.
# CACHE_ROOT must have enough free space for the extracted root filesystem.
fast_path_repack_squashfs() (
# Hard stop: this path takes the first squashfs and deletes the rest. On a
# multi-layer medium that silently loses every layer but base. needs_full_build
# already forces a full build here; this guard makes the invariant explicit.
if [ -f "${BUILD_WORK_DIR}/binary/live/filesystem.module" ] || \
[ "$(find "${BUILD_WORK_DIR}/binary/live" -maxdepth 1 -name 'filesystem*.squashfs' 2>/dev/null | wc -l)" -gt 1 ]; then
echo "ERROR: fast_path_repack_squashfs refuses to run on a multi-layer squashfs medium" >&2
exit 1
fi
_old_sq=$(find "${BUILD_WORK_DIR}/binary/live" -maxdepth 1 \
-name 'filesystem*.squashfs' | sort | head -1)
_sq="${BUILD_WORK_DIR}/binary/live/${SQUASHFS_FILENAME}"
_tmp_parent="$(mktemp -d "${CACHE_ROOT}/fast-unsquash-${BUILD_VARIANT}.XXXXXX")"
_tmp="${_tmp_parent}/root"
trap 'rm -rf "$_tmp_parent"' EXIT
echo "=== fast-path: unsquash $(basename "$_old_sq") ($(du -sh "$_old_sq" | cut -f1) compressed) ==="
unsquashfs -d "$_tmp" "$_old_sq"
echo "=== fast-path: syncing overlay stage ==="
# --keep-dirlinks: when the extracted root has a symlink-to-directory (e.g.
# merged-usr /lib -> usr/lib) and the overlay stage carries a real directory
# of the same name, follow the symlink instead of replacing it with a plain
# directory. Without this rsync silently orphans everything reachable only
# through that symlink and the resulting image panics at boot.
rsync -a --keep-dirlinks --checksum "${OVERLAY_STAGE_DIR}/" "$_tmp/"
echo "=== fast-path: repacking as ${SQUASHFS_FILENAME} ==="
_sq_new="${_sq}.new"
rm -f "$_sq_new"
mksquashfs "$_tmp" "$_sq_new" -comp zstd -b 1048576 -noappend -no-progress -no-xattrs
mv "$_sq_new" "$_sq"
rm -rf "$_tmp_parent"
for _candidate in "${BUILD_WORK_DIR}/binary/live/"filesystem*.squashfs; do
[ -e "$_candidate" ] || continue
[ "$_candidate" = "$_sq" ] || rm -f "$_candidate"
done
echo "=== fast-path: squashfs repacked ($(du -sh "$_sq" | cut -f1)) ==="
)
# Fast-path: rebuild ISO replacing the squashfs via xorriso.
# Boot structure (El Torito, EFI, MBR hybrid) is replayed from the prior ISO.
recover_iso_memtest() {
lb_dir="$1"
iso_path="$2"
binary_boot="$lb_dir/binary/boot"
echo "=== attempting memtest recovery in binary tree ==="
mkdir -p "$binary_boot"
for root in \
"$lb_dir/chroot/boot" \
"/boot"; do
for f in memtest86+x64.bin memtest86+x64.efi; do
if [ ! -f "$binary_boot/$f" ] && [ -f "$root/$f" ]; then
cp "$root/$f" "$binary_boot/$f"
echo "memtest recovery: copied $f from $root"
fi
done
done
if [ ! -f "$binary_boot/memtest86+x64.bin" ] || [ ! -f "$binary_boot/memtest86+x64.efi" ]; then
for dir in \
"$lb_dir/cache/packages.binary" \
"$lb_dir/cache/packages.chroot" \
"$lb_dir/chroot/var/cache/apt/archives" \
"${BEE_CACHE_DIR:-${DIST_DIR}/cache}/lb-packages" \
"/var/cache/apt/archives"; do
[ -d "$dir" ] || continue
deb="$(find "$dir" -maxdepth 1 -type f -name 'memtest86+*.deb' 2>/dev/null | head -1)"
[ -n "$deb" ] || continue
echo "memtest recovery: extracting payload from $deb"
copy_memtest_from_deb "$deb" "$binary_boot"
break
done
fi
if [ ! -f "$binary_boot/memtest86+x64.bin" ] || [ ! -f "$binary_boot/memtest86+x64.efi" ]; then
tmpdl="$(mktemp -d)"
if (
cd "$tmpdl" && apt-get download memtest86+ >/dev/null 2>&1
); then
deb="$(find "$tmpdl" -maxdepth 1 -type f -name 'memtest86+*.deb' 2>/dev/null | head -1)"
if [ -n "$deb" ]; then
echo "memtest recovery: downloaded $deb"
copy_memtest_from_deb "$deb" "$binary_boot"
fi
fi
rm -rf "$tmpdl"
fi
enforce_live_build_bootloader_assets "$lb_dir"
reset_live_build_stage "$lb_dir" "binary_checksums"
reset_live_build_stage "$lb_dir" "binary_iso"
reset_live_build_stage "$lb_dir" "binary_zsync"
run_optional_step_sh "rebuild live-build checksums after memtest recovery" "91-lb-checksums" "lb binary_checksums 2>&1"
run_optional_step_sh "rebuild ISO after memtest recovery" "92-lb-binary-iso" "rm -f '$iso_path' && lb binary_iso 2>&1"
run_optional_step_sh "rebuild zsync after memtest recovery" "93-lb-zsync" "lb binary_zsync 2>&1"
if [ ! -f "$iso_path" ]; then
memtest_fail "ISO rebuild was skipped or failed after memtest recovery: $iso_path" "$iso_path"
fi
}