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 ===" # --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 }