diff --git a/fedac/native/docker-build.sh b/fedac/native/docker-build.sh index d310f8b8d..46c54bf9b 100644 --- a/fedac/native/docker-build.sh +++ b/fedac/native/docker-build.sh @@ -736,8 +736,15 @@ if [ -n "$FW_LIST" ]; then log " Built-in firmware files: $FW_LIST" fi -# Copy initramfs into kernel tree -cp "$BUILD/initramfs.cpio.lz4" "$LINUX_DIR/initramfs.cpio.lz4" +# Pack initramfs as gzip up front — the kernel's EFI stub loads it externally +# via the baked-in `initrd=\initramfs.cpio.gz` cmdline token. The .lz4 stays +# around as the canonical build artifact (smaller + faster to repack), but the +# kernel itself no longer embeds initramfs, so pure code changes skip the +# kernel link step entirely. +log " Packing initramfs.cpio.gz (external initrd)..." +lz4 -d "$BUILD/initramfs.cpio.lz4" -c 2>/dev/null | gzip -c > "$BUILD/initramfs.cpio.gz" +cp "$BUILD/initramfs.cpio.gz" "$OUT/initramfs.cpio.gz" 2>/dev/null || true +log " initramfs.cpio.gz: $(($(stat -c%s "$BUILD/initramfs.cpio.gz") / 1048576))MB" # Configure — force-disable bloated GPU drivers that olddefconfig enables cd "$LINUX_DIR" @@ -795,35 +802,15 @@ if [ ! -f arch/x86/boot/bzImage ]; then fi cp arch/x86/boot/bzImage "$BUILD/vmlinuz" cp arch/x86/boot/bzImage "$OUT/vmlinuz" 2>/dev/null || true +# Legacy alias: media-layout.sh + OTA code still look for `vmlinuz-slim`. +# It is now the exact same bytes as `vmlinuz` since there is no monolithic +# variant — the kernel's EFI stub loads initramfs externally on every path. +cp arch/x86/boot/bzImage "$BUILD/vmlinuz-slim" +cp arch/x86/boot/bzImage "$OUT/vmlinuz-slim" 2>/dev/null || true VMLINUZ_SIZE=$(stat -c%s "$BUILD/vmlinuz") SHA=$(sha256sum "$BUILD/vmlinuz" | awk '{print $1}') - -# ══════════════════════════════════════════════ -# Step 4b: Build slim kernel (no embedded initramfs) for Mac EFI boot -# ══════════════════════════════════════════════ -log "Step 4b: Building slim kernel for Mac..." -sed -i 's|^CONFIG_INITRAMFS_SOURCE=.*|CONFIG_INITRAMFS_SOURCE=""|' .config -make olddefconfig >>"$KCFG_LOG" 2>&1 || { err "Kernel olddefconfig failed before slim build"; tail -120 "$KCFG_LOG" >&2; exit 1; } -if ! command -v ccache &>/dev/null; then make clean 2>/dev/null || true; fi -rm -f usr/initramfs_data.o usr/.initramfs_data.o.cmd -if run_make_with_heartbeat "$BUILD/kernel-slim.log" make -j"${KERNEL_JOBS}" CC="${CC_USE}" bzImage; then - cp arch/x86/boot/bzImage "$BUILD/vmlinuz-slim" - cp arch/x86/boot/bzImage "$OUT/vmlinuz-slim" 2>/dev/null || true - SLIM_SIZE=$(stat -c%s "$BUILD/vmlinuz-slim") - log " Slim kernel: $((SLIM_SIZE / 1048576))MB" -else - err "Slim kernel build failed (non-fatal)" -fi -# Restore config for any subsequent builds -sed -i 's|^CONFIG_INITRAMFS_SOURCE=.*|CONFIG_INITRAMFS_SOURCE="initramfs.cpio.lz4"|' .config -make olddefconfig >>"$KCFG_LOG" 2>&1 || { err "Kernel olddefconfig failed while restoring initramfs config"; tail -120 "$KCFG_LOG" >&2; exit 1; } - -# Export initramfs as gzip (for systemd-boot on Mac) -log " Packing initramfs.cpio.gz..." -lz4 -d "$BUILD/initramfs.cpio.lz4" -c 2>/dev/null | gzip -c > "$BUILD/initramfs.cpio.gz" -cp "$BUILD/initramfs.cpio.gz" "$OUT/initramfs.cpio.gz" 2>/dev/null || true -log " initramfs.cpio.gz: $(($(stat -c%s "$BUILD/initramfs.cpio.gz") / 1048576))MB" +log " vmlinuz: $((VMLINUZ_SIZE / 1048576))MB (external initramfs)" # ══════════════════════════════════════════════ # Step 5: Generate UEFI-bootable ISO diff --git a/fedac/native/initramfs/init b/fedac/native/initramfs/init index 076acf14c..5dc996d21 100755 --- a/fedac/native/initramfs/init +++ b/fedac/native/initramfs/init @@ -161,11 +161,30 @@ if [ -d /sys/bus/pci/devices ]; then done fi echo "=== DMESG (kernel ring) ===" >> $LOG -# Capture 2s of /dev/kmsg output (busybox dmesg applet may be absent) +# Capture 6s of /dev/kmsg — WiFi PCIe probe + SOF codec machine driver both +# finish around 3-5s after userspace starts, so a 2s window misses them. ( cat /dev/kmsg >> $LOG 2>&1 ) & KMSG_PID=$! -sleep 2 +sleep 6 kill $KMSG_PID 2>/dev/null +# Snapshot devices + sound AFTER the ring-buffer dump so we see the state +# that existed when drivers had their chance to probe. +echo "=== POST-PROBE SNAPSHOT ===" >> $LOG +echo "net ifaces:" >> $LOG +ls /sys/class/net/ 2>/dev/null >> $LOG +echo "sound cards:" >> $LOG +cat /proc/asound/cards 2>/dev/null >> $LOG +echo "pci drivers:" >> $LOG +for d in /sys/bus/pci/devices/*; do + [ -d "$d" ] || continue + class=$(cat $d/class 2>/dev/null) + case "$class" in + 0x010*|0x028*|0x040*|0x0805*) + drv=$(basename $(readlink $d/driver 2>/dev/null) 2>/dev/null) + echo " $(basename $d) class=$class drv=${drv:-NONE}" >> $LOG + ;; + esac +done echo "=== CPUINFO ===" >> $LOG head -30 /proc/cpuinfo >> $LOG 2>&1 echo "=== CMDLINE ===" >> $LOG diff --git a/fedac/native/kernel/config-minimal b/fedac/native/kernel/config-minimal index 892d01e77..8446b187b 100644 --- a/fedac/native/kernel/config-minimal +++ b/fedac/native/kernel/config-minimal @@ -182,7 +182,7 @@ CONFIG_ARCH_SUPPORTS_INT128=y # CONFIG_SCHED_AUTOGROUP is not set CONFIG_RELAY=y CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="initramfs.cpio.lz4" +CONFIG_INITRAMFS_SOURCE="" CONFIG_INITRAMFS_ROOT_UID=0 CONFIG_INITRAMFS_ROOT_GID=0 CONFIG_RD_GZIP=y @@ -424,7 +424,7 @@ CONFIG_HOTPLUG_CPU=y CONFIG_LEGACY_VSYSCALL_XONLY=y # CONFIG_LEGACY_VSYSCALL_NONE is not set CONFIG_CMDLINE_BOOL=y -CONFIG_CMDLINE="console=tty0 quiet loglevel=3 vt.global_cursor_default=0 init=/init mitigations=off snd_hda_intel.power_save=0 nmi_watchdog=0 tsc=reliable no_timer_check acpi_backlight=native thinkpad_acpi.brightness_enable=1 i8042.reset i8042.nomux reboot=efi,cold" +CONFIG_CMDLINE="initrd=\\initramfs.cpio.gz console=tty0 quiet loglevel=3 vt.global_cursor_default=0 init=/init mitigations=off snd_hda_intel.power_save=0 nmi_watchdog=0 tsc=reliable no_timer_check acpi_backlight=native thinkpad_acpi.brightness_enable=1 i8042.reset i8042.nomux reboot=efi,cold" # CONFIG_CMDLINE_OVERRIDE is not set CONFIG_MODIFY_LDT_SYSCALL=y # CONFIG_STRICT_SIGALTSTACK_SIZE is not set @@ -1805,6 +1805,8 @@ CONFIG_RTW89_8852A=y CONFIG_RTW89_8852AE=y CONFIG_RTW89_8852B=y CONFIG_RTW89_8852BE=y +CONFIG_RTW89_8852BT=y +CONFIG_RTW89_8852BTE=y CONFIG_RTW89_8852C=y CONFIG_RTW89_8852CE=y CONFIG_WLAN_VENDOR_RSI=y diff --git a/fedac/native/pieces/os.mjs b/fedac/native/pieces/os.mjs index 2d15dd562..7f0cdf561 100644 --- a/fedac/native/pieces/os.mjs +++ b/fedac/native/pieces/os.mjs @@ -5,9 +5,17 @@ const OS_BASE_URL = "https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com/os/"; const OS_VERSION_URL = OS_BASE_URL + "native-notepat-latest.version"; const OS_VMLINUZ_URL = OS_BASE_URL + "native-notepat-latest.vmlinuz"; +const OS_INITRAMFS_URL = OS_BASE_URL + "native-notepat-latest.initramfs.cpio.gz"; let remoteSize = 0; // parsed from version file (line 2) -// States: idle | checking | up-to-date | available | downloading | flashing +// Kernel no longer embeds initramfs (Phase 2 — loaded externally via EFI stub +// `initrd=\initramfs.cpio.gz` from the ESP root). OTA must download BOTH the +// kernel and the initramfs and flash them atomically, otherwise the device +// boots a new kernel against a stale initramfs and code-path drift follows. +let initramfsDownloaded = false; + +// States: idle | checking | up-to-date | available +// | downloading (kernel) | downloading-initramfs | flashing // | confirm-reboot | shutting-down | error // | devices | clone-confirm | cloning let state = "idle"; @@ -635,26 +643,51 @@ function paint({ wipe, ink, box, line, write, screen, system, wifi }) { errorMsg = "timeout"; } - // Download progress + // Download progress (kernel) if (state === "downloading") { const prevProgress = progress; progress = system?.fetchBinaryProgress ?? progress; - // Telemetry on progress milestones if (progress > 0 && Math.floor(progress * 10) > Math.floor(prevProgress * 10)) { const pct = Math.round(progress * 100); - addTelemetry(`download ${pct}% (${(progress * (remoteSize || 93_000_000) / 1048576).toFixed(1)}MB)`); + addTelemetry(`kernel ${pct}% (${(progress * (remoteSize || 13_000_000) / 1048576).toFixed(1)}MB)`); + } + if (system?.fetchBinaryDone) { + if (system?.fetchBinaryOk) { + addTelemetry("kernel downloaded, fetching initramfs..."); + state = "downloading-initramfs"; + progress = 0; + initramfsDownloaded = false; + // Initramfs size isn't in the .version file today; use a generous + // default so the progress bar doesn't stall at 100% prematurely. + system?.fetchBinary?.(OS_INITRAMFS_URL, "/tmp/initramfs.cpio.gz.new", 336_000_000); + } else { + state = "error"; + errorMsg = "kernel download failed"; + } + } + } + + // Download progress (initramfs) — kicks off flash once both files are local + if (state === "downloading-initramfs") { + const prevProgress = progress; + progress = system?.fetchBinaryProgress ?? progress; + if (progress > 0 && Math.floor(progress * 10) > Math.floor(prevProgress * 10)) { + const pct = Math.round(progress * 100); + addTelemetry(`initramfs ${pct}%`); } if (system?.fetchBinaryDone) { if (system?.fetchBinaryOk) { - addTelemetry("download complete, starting flash..."); + initramfsDownloaded = true; + addTelemetry("initramfs downloaded, starting flash..."); state = "flashing"; const dev = globalThis.__osFlashDevice; addTelemetry("target: " + (dev || system?.bootDevice || "auto")); - if (dev) system?.flashUpdate?.("/tmp/vmlinuz.new", dev); - else system?.flashUpdate?.("/tmp/vmlinuz.new"); + // Four-arg flashUpdate: (kernelSrc, device, initramfsSrc) + // Device may be omitted — null lets C auto-detect the boot device. + system?.flashUpdate?.("/tmp/vmlinuz.new", dev || null, "/tmp/initramfs.cpio.gz.new"); } else { state = "error"; - errorMsg = "download failed"; + errorMsg = "initramfs download failed"; } } } diff --git a/fedac/native/scripts/flash-helper-runner.sh b/fedac/native/scripts/flash-helper-runner.sh index 5bcf7d3f7..bff25c20c 100644 --- a/fedac/native/scripts/flash-helper-runner.sh +++ b/fedac/native/scripts/flash-helper-runner.sh @@ -131,19 +131,36 @@ copy_boot_tree_to_vfat() { # (set by ac_media_stage_boot_tree in media-layout.sh). local STAGED_KERNEL="${STAGED_ROOT}/EFI/BOOT/BOOTX64.EFI" + # Kernel now loads initramfs externally via `initrd=\initramfs.cpio.gz` + # in CONFIG_CMDLINE — every boot mode except systemd-boot (which manages + # its own initrd path via loader/entries/) must ship the initramfs at + # the partition root. Staging places it there already; we just copy. + local STAGED_INITRAMFS_GZ="${STAGED_ROOT}/initramfs.cpio.gz" + local STAGED_INITRAMFS_LZ4="${STAGED_ROOT}/initramfs.cpio.lz4" + copy_external_initramfs() { + if [ -f "${STAGED_INITRAMFS_GZ}" ]; then + cp "${STAGED_INITRAMFS_GZ}" "${mountpoint}/initramfs.cpio.gz" + elif [ -f "${STAGED_INITRAMFS_LZ4}" ]; then + cp "${STAGED_INITRAMFS_LZ4}" "${mountpoint}/initramfs.cpio.lz4" + fi + } + case "${boot_mode}" in chainloader) cp "${STAGED_KERNEL}" "${mountpoint}/EFI/BOOT/BOOTX64.EFI" cp "${STAGED_KERNEL}" "${mountpoint}/EFI/BOOT/KERNEL.EFI" + copy_external_initramfs ;; kernel-only) cp "${STAGED_KERNEL}" "${mountpoint}/EFI/BOOT/KERNEL.EFI" rm -f "${mountpoint}/EFI/BOOT/BOOTX64.EFI" + copy_external_initramfs ;; kernel-direct) # Place kernel AS BOOTX64.EFI — standard UEFI fallback path. # This is discoverable by all UEFI firmware including Intel Macs. cp "${STAGED_KERNEL}" "${mountpoint}/EFI/BOOT/BOOTX64.EFI" + copy_external_initramfs ;; systemd-boot) # Universal boot: splash.efi → systemd-boot → slim kernel + initramfs. @@ -207,6 +224,13 @@ populate_mac_partition() { cp "${STAGED_KERNEL}" "${mountpoint}/System/Library/CoreServices/boot.efi" mkdir -p "${mountpoint}/EFI/BOOT" cp "${STAGED_KERNEL}" "${mountpoint}/EFI/BOOT/BOOTX64.EFI" + # External initramfs — Mac firmware exposes HFS+ via EFI SimpleFileSystem, + # so the kernel's EFI stub can load `\initramfs.cpio.gz` from the root. + if [ -f "${STAGED_ROOT}/initramfs.cpio.gz" ]; then + cp "${STAGED_ROOT}/initramfs.cpio.gz" "${mountpoint}/initramfs.cpio.gz" + elif [ -f "${STAGED_ROOT}/initramfs.cpio.lz4" ]; then + cp "${STAGED_ROOT}/initramfs.cpio.lz4" "${mountpoint}/initramfs.cpio.lz4" + fi cat > "${mountpoint}/System/Library/CoreServices/SystemVersion.plist" << 'PLIST_EOF' diff --git a/fedac/native/scripts/media-layout.sh b/fedac/native/scripts/media-layout.sh index 63e6e2e9d..f8025b76d 100644 --- a/fedac/native/scripts/media-layout.sh +++ b/fedac/native/scripts/media-layout.sh @@ -266,19 +266,24 @@ ac_media_stage_boot_tree() { cp "${kernel_path}" "${stage_root}/EFI/BOOT/BOOTIA32.EFI" cp "${config_path}" "${stage_root}/config.json" - # Stage slim kernel + initramfs for universal boot (systemd-boot mode) + # Stage slim kernel + initramfs for universal boot (systemd-boot mode). + # The kernel's baked-in cmdline has `initrd=\initramfs.cpio.gz`, so the + # gzip initramfs MUST land at the ESP root — don't relocate it under + # EFI/BOOT/ or the EFI stub will fail to find it at boot time. local kernel_dir kernel_dir="$(dirname "${kernel_path}")" local slim="${kernel_dir}/vmlinuz-slim" if [ -f "${slim}" ]; then cp "${slim}" "${stage_root}/EFI/BOOT/KERNEL-SLIM.EFI" fi - # Prefer gzip initramfs (universal), fall back to lz4 local initramfs_gz="${kernel_dir}/initramfs.cpio.gz" local initramfs_lz4="${kernel_dir}/initramfs.cpio.lz4" if [ -f "${initramfs_gz}" ]; then cp "${initramfs_gz}" "${stage_root}/initramfs.cpio.gz" elif [ -f "${initramfs_lz4}" ]; then + # Fallback: no .gz produced. Kernel cmdline still says .gz, so this + # path is only useful for systemd-boot layouts that pass their own + # initrd= in loader/entries/ac-native.conf. cp "${initramfs_lz4}" "${stage_root}/initramfs.cpio.lz4" fi } @@ -313,6 +318,13 @@ ac_media_create_fat_image() { if [ -f "${stage_root}/EFI/BOOT/BOOTIA32.EFI" ]; then mcopy -o -i "${image_path}" "${stage_root}/EFI/BOOT/BOOTIA32.EFI" ::EFI/BOOT/BOOTIA32.EFI fi + # External initramfs at ESP root — matches `initrd=\initramfs.cpio.gz` in + # CONFIG_CMDLINE so the kernel EFI stub can load it. + if [ -f "${stage_root}/initramfs.cpio.gz" ]; then + mcopy -o -i "${image_path}" "${stage_root}/initramfs.cpio.gz" ::initramfs.cpio.gz + elif [ -f "${stage_root}/initramfs.cpio.lz4" ]; then + mcopy -o -i "${image_path}" "${stage_root}/initramfs.cpio.lz4" ::initramfs.cpio.lz4 + fi } ac_media_create_efi_disk_image() { @@ -349,6 +361,12 @@ ac_media_create_efi_disk_image() { if [ -f "${stage_root}/EFI/BOOT/BOOTIA32.EFI" ]; then mcopy -o -i "${image_path}@@${esp_offset}" "${stage_root}/EFI/BOOT/BOOTIA32.EFI" ::EFI/BOOT/BOOTIA32.EFI fi + # External initramfs — see ac_media_create_fat_image for rationale. + if [ -f "${stage_root}/initramfs.cpio.gz" ]; then + mcopy -o -i "${image_path}@@${esp_offset}" "${stage_root}/initramfs.cpio.gz" ::initramfs.cpio.gz + elif [ -f "${stage_root}/initramfs.cpio.lz4" ]; then + mcopy -o -i "${image_path}@@${esp_offset}" "${stage_root}/initramfs.cpio.lz4" ::initramfs.cpio.lz4 + fi } ac_create_fat_boot_image() { diff --git a/fedac/native/src/js-bindings.c b/fedac/native/src/js-bindings.c index db38cdb8c..0faee8639 100644 --- a/fedac/native/src/js-bindings.c +++ b/fedac/native/src/js-bindings.c @@ -4241,6 +4241,44 @@ static void *flash_thread_fn(void *arg) { } } + // Optional initramfs copy — for OTA updates where the kernel is now + // slim (Phase 2 de-embed) and must load `\initramfs.cpio.gz` from the + // ESP root via its baked-in `initrd=` cmdline. Without this step, an + // os.mjs OTA would leave the new kernel paired with the PREVIOUS + // initramfs, which is a latent source of boot breakage whenever any + // file inside initramfs changes. + if (rt->flash_initramfs_src[0]) { + char initramfs_dst[512]; + snprintf(initramfs_dst, sizeof(initramfs_dst), "%s/initramfs.cpio.gz", efi_mount); + ac_log("[flash] writing initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); + flash_tlog(rt, "initramfs: %s -> %s", rt->flash_initramfs_src, initramfs_dst); + // Keep previous as .prev for rollback + char initramfs_prev[512]; + snprintf(initramfs_prev, sizeof(initramfs_prev), "%s.prev", initramfs_dst); + if (access(initramfs_dst, F_OK) == 0) { + unlink(initramfs_prev); + rename(initramfs_dst, initramfs_prev); + } + long initramfs_copied = flash_copy_file(rt->flash_initramfs_src, initramfs_dst); + flash_tlog(rt, "initramfs wrote %ld bytes", initramfs_copied); + if (initramfs_copied <= 0) { + ac_log("[flash] initramfs copy FAILED (copied=%ld) — restoring .prev", + initramfs_copied); + if (access(initramfs_prev, F_OK) == 0) { + unlink(initramfs_dst); + rename(initramfs_prev, initramfs_dst); + } + // Non-fatal to the kernel write — but flag as failure since the + // new kernel won't boot without a matching initramfs. + flash_trace_close_and_archive(); + rt->flash_phase = 4; + rt->flash_ok = 0; + rt->flash_pending = 0; + rt->flash_done = 1; + return NULL; + } + } + // Phase 2: Syncing to disk — belt-and-suspenders for vfat rt->flash_phase = 2; @@ -4428,8 +4466,12 @@ static void *flash_thread_fn(void *arg) { return NULL; } -// system.flashUpdate(srcPath[, devicePath]) -// devicePath defaults to auto-detected boot device if omitted +// system.flashUpdate(srcPath[, devicePath[, initramfsSrcPath]]) +// devicePath defaults to auto-detected boot device if omitted. +// initramfsSrcPath is optional — when provided, the flash thread also copies +// it to `/initramfs.cpio.gz` (matching the kernel's baked-in +// `initrd=\initramfs.cpio.gz` cmdline). Required for OTA updates since the +// kernel no longer embeds initramfs (Phase 2 de-embed). static JSValue js_flash_update(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { (void)this_val; if (!current_rt || argc < 1) { ac_log("[flash] flashUpdate called with no runtime/args\n"); return JS_UNDEFINED; } @@ -4442,6 +4484,7 @@ static JSValue js_flash_update(JSContext *ctx, JSValueConst this_val, int argc, strncpy(current_rt->flash_src, src, sizeof(current_rt->flash_src) - 1); current_rt->flash_src[sizeof(current_rt->flash_src) - 1] = 0; JS_FreeCString(ctx, src); + current_rt->flash_initramfs_src[0] = '\0'; // Device: use arg[1] if provided, else auto-detect if (argc >= 2 && !JS_IsUndefined(argv[1]) && !JS_IsNull(argv[1])) { const char *dev = JS_ToCString(ctx, argv[1]); @@ -4453,6 +4496,17 @@ static JSValue js_flash_update(JSContext *ctx, JSValueConst this_val, int argc, } else { detect_boot_device(current_rt->flash_device, sizeof(current_rt->flash_device)); } + // Optional initramfs source — arg[2] + if (argc >= 3 && !JS_IsUndefined(argv[2]) && !JS_IsNull(argv[2])) { + const char *init_src = JS_ToCString(ctx, argv[2]); + if (init_src) { + strncpy(current_rt->flash_initramfs_src, init_src, + sizeof(current_rt->flash_initramfs_src) - 1); + current_rt->flash_initramfs_src[sizeof(current_rt->flash_initramfs_src) - 1] = 0; + ac_log("[flash] initramfs src=%s", init_src); + JS_FreeCString(ctx, init_src); + } + } // Pass runtime as arg (current_rt is thread-local, unavailable in new thread) pthread_attr_t attr; pthread_attr_init(&attr); diff --git a/fedac/native/src/js-bindings.h b/fedac/native/src/js-bindings.h index c1dd85a26..ea79ecf0e 100644 --- a/fedac/native/src/js-bindings.h +++ b/fedac/native/src/js-bindings.h @@ -61,6 +61,7 @@ typedef struct { volatile long flash_verified_bytes; // bytes verified after readback pthread_t flash_thread; char flash_src[256]; // source vmlinuz path + char flash_initramfs_src[256]; // optional initramfs.cpio.gz source path (empty = skip) char flash_device[64]; // EFI partition device, e.g. /dev/sda1 or /dev/nvme0n1p1 char flash_dst[256]; // actual destination path written to volatile int flash_same_device; // 1 = target is same device as /mnt (NVMe-to-NVMe)