Files
bee/iso/builder/lib/fast-path.sh
T
Mikhail ChusavitinandClaude Sonnet 5 d0ed1bda91 fix(iso): hash build scripts into the fast-path safety check
needs_full_build() decided the fast path was safe by hashing only config
inputs (VERSIONS, package lists, hooks, archives, auto/config, Dockerfile).
It never hashed the build logic itself, so a change to build.sh or
lib/fast-path.sh counted as a "light" file and the next build silently
reused a squashfs produced by the old code. The merged-usr /lib fix in the
previous commit only forced a full rebuild by accident (the overlay
manifest saw firmware move from lib/ to usr/lib/).

hash_heavy_config now covers every *.sh under iso/builder (build.sh, the
build-*.sh helpers, lib/*.sh) plus auto/. A change to build logic is now
as heavy as a package-list change. config/bootloaders stays excluded (the
fast path regenerates the outer ISO layer from it every time). Hash is
deterministic (LC_ALL=C sort).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2LLuid8PFhBqBPcxXxkQU
2026-09-03 15:32:33 +03:00

218 lines
8.6 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
_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() (
_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 ==="
rsync -a --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
}