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>
240 lines
10 KiB
Bash
Executable File
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
|
|
}
|