Stage the Windows ISO files with rsync before copying them into the image

Copy the ISO without install.wim to a staging directory, split install.wim
into it, and fill the image with one recursive mcopy instead of a mmd and
mcopy per directory. rsync --chmod keeps the staged directories writable so
that cleanup can remove them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GuofangandClaude Sonnet 5.5 committed 2026-10-19 13:26:24 +08:00
1 parent dd29fd92a7
commit d9707fd57b
1 file changed
+28 -35
+28 -35
View File
@@ -15,7 +15,7 @@
#
# Dependencies
# macOS (Homebrew) wimlib dosfstools gptfdisk mtools
# Debian wimtools dosfstools gdisk mtools udisks2
# Debian wimtools dosfstools gdisk mtools rsync udisks2
#
# Usage: build-windows-install-disk.sh [--force] ISO [OUTPUT.img]
# OUTPUT defaults to the ISO path with ".img" in place of ".iso".
@@ -46,8 +46,8 @@ die() {
exit 1
}
# The build has four steps; step prints a numbered heading for each of them.
total_steps=4
# The build has five steps; step prints a numbered heading for each of them.
total_steps=5
step_number=0
step() {
step_number=$((step_number + 1))
@@ -88,7 +88,7 @@ for dir in ${brew_prefix:+"$brew_prefix/sbin"} /opt/homebrew/sbin /usr/local/sbi
[[ -d "$dir" ]] && PATH="$PATH:$dir"
done
tools=(wimlib-imagex mkfs.fat sgdisk mcopy mmd)
tools=(wimlib-imagex mkfs.fat sgdisk mcopy rsync)
[[ "$os" == Darwin ]] && tools+=(hdiutil)
missing=()
for tool in "${tools[@]}"; do
@@ -98,7 +98,7 @@ if ((${#missing[@]} > 0)); then
if [[ "$os" == Darwin ]]; then
hint="brew install wimlib dosfstools gptfdisk mtools"
else
hint="apt install wimtools dosfstools gdisk mtools"
hint="apt install wimtools dosfstools gdisk mtools rsync"
fi
die "missing tools: ${missing[*]} ($hint)"
fi
@@ -128,6 +128,8 @@ cleanup() {
udisksctl unmount -b "$loop_device" >/dev/null 2>&1 || true
udisksctl loop-delete -b "$loop_device" >/dev/null 2>&1 || true ;;
esac
# Staged directories may be read-only when the run was cancelled early.
chmod -R u+rwX "$work_dir" >/dev/null 2>&1 || true
rm -rf "$work_dir"
}
trap cleanup EXIT
@@ -202,20 +204,27 @@ install_wim="$mount_dir/sources/install.wim"
[[ -d "$mount_dir/efi/boot" ]] || die "efi/boot was not found in the ISO; is it a UEFI Windows ISO?"
note "install.wim: $(stat "${size_args[@]}" "$install_wim") bytes"
# The staging directory holds everything that goes into the image: the ISO files
# without install.wim, and the install.swm parts in sources/.
stage_dir="$work_dir/stage"
mkdir "$stage_dir"
step "Staging the ISO files without install.wim"
# The UDF mount is read-only, so its directories are too; --chmod keeps the copies
# writable, otherwise cleanup cannot remove them.
run rsync -rt --chmod=u+rwX --exclude=/sources/install.wim "$mount_dir/" "$stage_dir/"
run mkdir -p "$stage_dir/sources"
# Parts must stay below the 4 GiB FAT32 file limit; wimlib takes MiB.
step "Splitting install.wim into install.swm parts (FAT32 files must stay below 4 GiB)"
split_dir="$work_dir/split"
mkdir "$split_dir"
run wimlib-imagex split "$install_wim" "$split_dir/install.swm" 3800 >/dev/null
for part in "$split_dir"/install*.swm; do
run wimlib-imagex split "$install_wim" "$stage_dir/sources/install.swm" 3800 >/dev/null
for part in "$stage_dir"/sources/install*.swm; do
note "$(basename "$part"): $(stat "${size_args[@]}" "$part") bytes"
done
# Size the image: everything but install.wim, the split parts, and 5% plus 64 MiB
# of room for FAT32 metadata and partition alignment.
# Size the image: the staged files, and 5% plus 64 MiB of room for FAT32
# metadata and partition alignment.
content_bytes=$(
{ find "$mount_dir" -type f ! -path "$install_wim" -exec stat "${size_args[@]}" {} +
find "$split_dir" -type f -exec stat "${size_args[@]}" {} +; } |
find "$stage_dir" -type f -exec stat "${size_args[@]}" {} + |
awk '{ total += $1 } END { printf "%d", total }'
)
mib=$((1024 * 1024))
@@ -240,29 +249,13 @@ mtools_image="$image@@$((2048 * 512))"
note "partition: $sectors sectors, formatted as FAT32 (label WINPE)"
export MTOOLS_SKIP_CHECK=1
copy_dir() { # copy_dir SOURCE_DIR TARGET_DIR_IN_IMAGE
local source="$1" target="$2" files=() entry
while IFS= read -r -d '' entry; do
files+=("$entry")
done < <(find "$source" -maxdepth 1 -type f ! -path "$install_wim" -print0)
if ((${#files[@]} > 0)); then
note "\$ mcopy -Q -m -i $mtools_image <${#files[@]} files from ${source#"$mount_dir"} > ::$target/"
mcopy -Q -m -i "$mtools_image" "${files[@]}" "::$target/"
fi
}
step "Copying the installation files into the image"
copy_dir "$mount_dir" ""
dir_count=0
while IFS= read -r -d '' dir; do
relative="${dir#"$mount_dir"/}"
dir_count=$((dir_count + 1))
run mmd -i "$mtools_image" "::$relative"
copy_dir "$dir" "$relative"
done < <(find "$mount_dir" -mindepth 1 -type d -print0 | sort -z)
note "copied $dir_count directories"
note "copying the install*.swm parts to sources/"
run mcopy -Q -m -i "$mtools_image" "$split_dir"/install*.swm "::sources/"
entries=()
while IFS= read -r -d '' entry; do
entries+=("$entry")
done < <(find "$stage_dir" -mindepth 1 -maxdepth 1 -print0)
note "\$ mcopy -s -Q -m -i $mtools_image <${#entries[@]} entries from the staging directory> ::/"
mcopy -s -Q -m -i "$mtools_image" "${entries[@]}" "::/"
partial_output=""
printf 'Built Windows 11 installation disk: %s\n' "$output"