Files
psxrecomp/tools/bundle_mingw_dlls.sh
T
Alex Vanderveen 155e269ba5 tools: DirectWrite/Direct2D/WIC are system DLLs, not payload
recomp-ui's Win32 emoji and font path imports DWrite.dll, d2d1.dll and the
WIC/D3D family. None was in the system list, so the bundler went looking
for them in the MinGW sysroot, did not find them (they only exist on
Windows) and failed the packaging step — measured on the SNES side, where
the same recomp-ui pulled the same imports and killed the release job.

Brings the list to parity with snesrecomp's bundle_runtime_libs.sh, whose
entries this one was missing as well (KERNELBASE, GDIPLUS, SHCORE, DWMAPI,
UXTHEME, HID, XINPUT*, AVRT, MF*, EXT-MS-*, and the rest).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DYwgPRbwbXz8FhpVzDM7tA
2026-09-06 04:04:37 -04:00

356 lines
9.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Bundle imported non-system DLLs next to Windows PE executables.
#
# Title packagers (BPE/MotK setup zips) call this for Windows PEs that still
# import non-system DLLs (typically the MSYS2-built setup host + SDL2).
# Walks the exe *and* each copied DLL (zlib1.dll → libssp-0.dll, etc.).
# Emitters built with llvm-mingw + PSXRECOMP_STATIC_CLI should import none of
# the GCC runtimes.
#
# Usage:
# bundle_mingw_dlls.sh [options] --exe <path> [--dest <dir>] [--label <name>] ...
#
# Options:
# --runtime-bin DIR Preferred DLL search directory (repeatable)
# --search-dir DIR Extra DLL search directory (repeatable)
# --exe PATH PE to inspect (repeatable; --dest/--label apply to it)
# --dest DIR Copy DLLs here (default: dirname of the preceding --exe)
# --label NAME Log label for the preceding --exe
# --require DLL After bundling, DLL must exist in every --dest that
# imported it (repeatable)
# --soft-missing Warn instead of exit 1 when objdump is unavailable
#
# Exit 1 if an imported DLL cannot be found, or a --require check fails.
set -euo pipefail
SOFT_MISSING=0
RUNTIME_BINS=()
SEARCH_DIRS=()
REQUIRE=()
# Parallel arrays for targets.
EXE_PATHS=()
DEST_DIRS=()
LABELS=()
pending_dest=""
pending_label=""
usage() {
sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'
exit 2
}
flush_pending_meta() {
# Apply trailing --dest/--label to the last --exe.
local n="${#EXE_PATHS[@]}"
if [[ "$n" -eq 0 ]]; then
return 0
fi
local i=$((n - 1))
if [[ -n "${pending_dest}" ]]; then
DEST_DIRS[$i]="${pending_dest}"
pending_dest=""
fi
if [[ -n "${pending_label}" ]]; then
LABELS[$i]="${pending_label}"
pending_label=""
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage ;;
--soft-missing) SOFT_MISSING=1; shift ;;
--runtime-bin)
RUNTIME_BINS+=("${2:?}"); shift 2 ;;
--search-dir)
SEARCH_DIRS+=("${2:?}"); shift 2 ;;
--require)
REQUIRE+=("${2:?}"); shift 2 ;;
--exe)
flush_pending_meta
EXE_PATHS+=("${2:?}")
DEST_DIRS+=("")
LABELS+=("")
shift 2
;;
--dest)
pending_dest="${2:?}"; shift 2 ;;
--label)
pending_label="${2:?}"; shift 2 ;;
*)
echo "error: unknown arg: $1" >&2
usage
;;
esac
done
flush_pending_meta
if [[ ${#EXE_PATHS[@]} -eq 0 ]]; then
echo "error: at least one --exe is required" >&2
usage
fi
OBJDUMP=""
if command -v x86_64-w64-mingw32-objdump >/dev/null 2>&1; then
OBJDUMP="x86_64-w64-mingw32-objdump"
elif command -v objdump >/dev/null 2>&1; then
OBJDUMP="objdump"
else
if [[ "${SOFT_MISSING}" -eq 1 ]]; then
echo "warning: no objdump; skipping MinGW DLL bundling" >&2
exit 0
fi
echo "error: no objdump; cannot bundle MinGW DLLs" >&2
exit 1
fi
# Windows' own DLLs ship with the OS; copying them is at best noise and at
# worst a version conflict with the loader's.
#
# A name missing from this list is not a warning, it is a FAILED RELEASE: the
# bundler hunts for it in the MinGW sysroot, does not find it (it only exists
# on Windows), and exits 1. That is how DWrite.dll broke a setup-pack job --
# recomp-ui's Win32 emoji/font path imports DirectWrite, Direct2D and WIC, and
# none of the three was listed. When a new import shows up here, ask whether
# Windows ships it before reaching for a copy of it.
SYSTEM_DLL_RE='^(KERNEL32|KERNELBASE|USER32|GDI32|GDIPLUS|ADVAPI32|SHELL32|SHCORE|OLE32|OLEAUT32|WS2_32|WINMM|IMM32|SETUPAPI|VERSION|OPENGL32|GLU32|D2D1|DWRITE|DCOMP|DXGI|D3D[0-9]*|D3DCOMPILER_[0-9]*|WINDOWSCODECS|PROPSYS|COMCTL32|COMDLG32|RPCRT4|SHLWAPI|CRYPT32|BCRYPT|NCRYPT|IPHLPAPI|NSI|DNSAPI|MSVCRT|UCRTBASE|VCRUNTIME[0-9]*|MSVCP[0-9]*|DBGHELP|DWMAPI|UXTHEME|POWRPROF|CFGMGR32|HID|WINTRUST|MSIMG32|AVRT|MF[A-Z]*|AUDIOSES|DINPUT8|XINPUT[0-9_]*|USERENV|API-MS-.*|EXT-MS-.*)\.DLL$'
PROBE_DLLS=(
SDL2.dll
zlib1.dll
libgcc_s_seh-1.dll
libstdc++-6.dll
libc++.dll
libunwind.dll
libwinpthread-1.dll
libssp-0.dll
)
# Companion GCC/MinGW runtimes. Copied when the PE or any already-copied DLL
# imports them (direct or transitive). libssp-0 is pulled in by zlib1.dll even
# when the host exe itself is -static-libgcc.
ALWAYS_RUNTIME_DLLS=(
libgcc_s_seh-1.dll
libstdc++-6.dll
libwinpthread-1.dll
libssp-0.dll
)
dll_name_eq() {
local a b
a="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
b="$(printf '%s' "$2" | tr '[:upper:]' '[:lower:]')"
[[ "${a}" == "${b}" ]]
}
is_system_dll() {
local name="$1"
printf '%s' "${name}" | grep -qiE "${SYSTEM_DLL_RE}"
}
is_always_runtime_dll() {
local dll="$1" cand
for cand in "${ALWAYS_RUNTIME_DLLS[@]}"; do
if dll_name_eq "${cand}" "${dll}"; then
return 0
fi
done
return 1
}
# Avoid `objdump | grep -q` under pipefail: grep -q exits early → objdump SIGPIPE
# → pipeline fails even when the DLL Name line matched.
list_exe_imports() {
local exe="$1"
"${OBJDUMP}" -p "${exe}" 2>/dev/null \
| awk '/DLL Name:/{print $3}' \
|| true
}
exe_imports_dll() {
local exe="$1"
local dll="$2"
local name
while IFS= read -r name; do
[[ -n "${name}" ]] || continue
if dll_name_eq "${name}" "${dll}"; then
return 0
fi
done < <(list_exe_imports "${exe}")
return 1
}
find_dll_src() {
local dll="$1"
local exe="$2"
local dest_dir="$3"
local cand d name
local -a names=("${dll}")
local -a candidates=()
local lower
lower="$(printf '%s' "${dll}" | tr '[:upper:]' '[:lower:]')"
# MSVC/vcpkg: zlib1.dll; some MinGW layouts: z.dll / zlib.dll.
case "${lower}" in
z.dll) names+=("zlib1.dll" "zlib.dll") ;;
zlib1.dll) names+=("z.dll" "zlib.dll") ;;
zlib.dll) names+=("zlib1.dll" "z.dll") ;;
esac
for name in "${names[@]}"; do
candidates+=(
"$(dirname "${exe}")/${name}"
"${dest_dir}/${name}"
"/mingw64/bin/${name}"
"/usr/x86_64-w64-mingw32/bin/${name}"
)
for d in "${SEARCH_DIRS[@]+"${SEARCH_DIRS[@]}"}"; do
candidates+=("${d}/${name}")
done
for d in "${RUNTIME_BINS[@]+"${RUNTIME_BINS[@]}"}"; do
candidates+=("${d}/${name}")
done
done
for cand in "${candidates[@]}"; do
[[ -n "${cand}" ]] || continue
if [[ -f "${cand}" ]]; then
printf '%s' "${cand}"
return 0
fi
done
return 1
}
# True if exe or any PE already in dest imports dll (direct).
tree_imports_dll() {
local exe="$1"
local dest_dir="$2"
local dll="$3"
local pe
if exe_imports_dll "${exe}" "${dll}"; then
return 0
fi
shopt -s nullglob
for pe in "${dest_dir}"/*.dll "${dest_dir}"/*.DLL "${dest_dir}"/*.exe "${dest_dir}"/*.EXE; do
[[ -f "${pe}" ]] || continue
if exe_imports_dll "${pe}" "${dll}"; then
shopt -u nullglob
return 0
fi
done
shopt -u nullglob
return 1
}
bundle_one() {
local exe="$1"
local dest_dir="$2"
local label="$3"
local dll src key name dest_file src_res dest_res
local -a queue=()
local -A queued=()
if [[ ! -f "${exe}" ]]; then
echo "error: cannot bundle DLLs; missing ${exe}" >&2
exit 1
fi
if [[ -z "${dest_dir}" ]]; then
dest_dir="$(dirname "${exe}")"
fi
if [[ -z "${label}" ]]; then
label="$(basename "${exe}")"
fi
mkdir -p "${dest_dir}"
enqueue() {
local n="$1"
local k
[[ -n "${n}" ]] || return 0
if is_system_dll "${n}"; then
return 0
fi
k="$(printf '%s' "${n}" | tr '[:upper:]' '[:lower:]')"
if [[ -n "${queued[$k]:-}" ]]; then
return 0
fi
queued[$k]=1
queue+=("${n}")
}
while IFS= read -r name; do
enqueue "${name}"
done < <(list_exe_imports "${exe}")
# Probe names the exe actually imports (covers case / alias mismatches).
for dll in "${PROBE_DLLS[@]}" "${ALWAYS_RUNTIME_DLLS[@]}"; do
if exe_imports_dll "${exe}" "${dll}"; then
enqueue "${dll}"
fi
done
local i=0
while [[ "${i}" -lt "${#queue[@]}" ]]; do
dll="${queue[$i]}"
i=$((i + 1))
src="$(find_dll_src "${dll}" "${exe}" "${dest_dir}" || true)"
if [[ -z "${src}" ]]; then
echo "error: required DLL missing for ${label}: ${dll}" >&2
echo " exe: ${exe}" >&2
echo " dest: ${dest_dir}" >&2
if [[ ${#SEARCH_DIRS[@]} -gt 0 ]]; then
echo " search-dirs: ${SEARCH_DIRS[*]}" >&2
fi
if [[ ${#RUNTIME_BINS[@]} -gt 0 ]]; then
echo " runtime-bins: ${RUNTIME_BINS[*]}" >&2
fi
exit 1
fi
dest_file="${dest_dir}/${dll}"
if [[ -f "${dest_file}" ]] && [[ "${src}" -ef "${dest_file}" ]]; then
echo "bundled ${dll} → ${dest_dir}/ (${label}; already present)"
else
src_res="$(cd "$(dirname "${src}")" && pwd)/$(basename "${src}")"
dest_res="$(cd "${dest_dir}" && pwd)/${dll}"
if [[ "${src_res}" == "${dest_res}" ]]; then
echo "bundled ${dll} → ${dest_dir}/ (${label}; already present)"
else
# Copy to the PE import name (zlib1.dll source may satisfy z.dll).
cp -f "${src}" "${dest_file}"
echo "bundled ${dll} → ${dest_dir}/ (${label})"
fi
fi
# Recurse: zlib1.dll imports libssp-0.dll even when the host does not.
if [[ -f "${dest_file}" ]]; then
while IFS= read -r name; do
enqueue "${name}"
done < <(list_exe_imports "${dest_file}")
elif [[ -f "${src}" ]]; then
while IFS= read -r name; do
enqueue "${name}"
done < <(list_exe_imports "${src}")
fi
done
}
for i in "${!EXE_PATHS[@]}"; do
bundle_one "${EXE_PATHS[$i]}" "${DEST_DIRS[$i]}" "${LABELS[$i]}"
done
for dll in "${REQUIRE[@]}"; do
[[ -n "${dll}" ]] || continue
for i in "${!EXE_PATHS[@]}"; do
exe="${EXE_PATHS[$i]}"
dest="${DEST_DIRS[$i]:-}"
if [[ -z "${dest}" ]]; then
dest="$(dirname "${exe}")"
fi
if ! tree_imports_dll "${exe}" "${dest}" "${dll}"; then
continue
fi
if [[ ! -f "${dest}/${dll}" ]]; then
echo "error: $(basename "${exe}") needs ${dll} but it is missing under ${dest}" >&2
exit 1
fi
done
done