Static-link MinGW Release for a self-contained exe (fixes #1)

A default MinGW build dynamically imports three non-system DLLs
(SDL2.dll, libgcc_s_seh-1.dll, libstdc++-6.dll) that get bundled
side-by-side. On a user machine with a mismatched-architecture copy
of one of those earlier on the DLL search path, Windows loads the
wrong one and the process dies at launch with 0xc000007b
(STATUS_INVALID_IMAGE_FORMAT) before any of our code runs.

Add PSX_STATIC_RUNTIME (default ON for MinGW Release/MinSizeRel):
link -static -static-libgcc -static-libstdc++ and pull SDL2 from the
pkg-config --static line (libSDL2.a + its Windows system-lib chain)
instead of the import lib. The resulting exe imports only Windows
system DLLs, so it runs from any folder and 0xc000007b from a
mismatched runtime DLL becomes structurally impossible.

package_release.ps1 no longer bundles the runtime DLLs and instead
asserts self-containment via objdump (fails the build if any
non-system import remains).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-03 23:58:13 -07:00
parent 963a87a327
commit 08fa3765ba
2 changed files with 55 additions and 7 deletions
+36 -1
View File
@@ -43,6 +43,26 @@ if(NOT SDL2_INCLUDE_DIRS OR NOT SDL2_LIBRARIES)
endif()
endif()
# PSX_STATIC_RUNTIME: produce a 100% self-contained MinGW exe.
#
# A default MinGW build dynamically imports three NON-system DLLs —
# SDL2.dll, libgcc_s_seh-1.dll, libstdc++-6.dll — which must be shipped
# next to the exe. On a user's machine that side-by-side scheme breaks
# when a different-architecture copy of one of those DLLs is found earlier
# on the DLL search path (System32, another app on PATH), producing the
# 0xc000007b STATUS_INVALID_IMAGE_FORMAT crash on launch.
#
# Linking those runtimes (and SDL2) statically removes every non-system
# import, so the exe runs from any folder with zero bundled DLLs and the
# 0xc000007b failure mode becomes structurally impossible. Default ON for
# MinGW Release/MinSizeRel (the configs used to cut releases); override
# with -DPSX_STATIC_RUNTIME=OFF to force dynamic linking.
if(MINGW AND (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "MinSizeRel"))
option(PSX_STATIC_RUNTIME "Statically link SDL2 + libgcc/libstdc++ for a self-contained exe" ON)
else()
option(PSX_STATIC_RUNTIME "Statically link SDL2 + libgcc/libstdc++ for a self-contained exe" OFF)
endif()
set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/main.cpp
${PSXRECOMP_ROOT}/runtime/src/memory.c
@@ -151,7 +171,15 @@ function(psxrecomp_add_runtime_target target)
if(SDL2_LIBRARY_DIRS)
target_link_directories(${target} PRIVATE ${SDL2_LIBRARY_DIRS})
endif()
target_link_libraries(${target} PRIVATE ${SDL2_LIBRARIES})
# For a self-contained MinGW build, link SDL2 statically via pkg-config's
# --static link line (libSDL2.a + the full Windows system-lib chain SDL2
# needs: winmm, imm32, ole32, oleaut32, version, setupapi, dinput8, ...).
# Otherwise link the SDL2 import lib (needs SDL2.dll at runtime).
if(PSX_STATIC_RUNTIME AND SDL2_STATIC_LDFLAGS)
target_link_libraries(${target} PRIVATE ${SDL2_STATIC_LDFLAGS})
else()
target_link_libraries(${target} PRIVATE ${SDL2_LIBRARIES})
endif()
target_compile_definitions(${target} PRIVATE
DEFAULT_DEBUG_PORT=${PSXRT_DEBUG_PORT}
@@ -188,6 +216,13 @@ function(psxrecomp_add_runtime_target target)
target_link_options(${target} PRIVATE -Wl,--stack,67108864)
# No console window in Release MinGW builds.
target_link_options(${target} PRIVATE $<$<CONFIG:Release>:-mwindows>)
if(PSX_STATIC_RUNTIME)
# Fold the GCC / C++ / winpthread runtimes into the exe so it
# imports only Windows system DLLs (no libgcc_s_seh-1.dll /
# libstdc++-6.dll dependency). Pairs with the static SDL2 link
# above to make the exe fully self-contained.
target_link_options(${target} PRIVATE -static -static-libgcc -static-libstdc++)
endif()
elseif(MSVC)
target_compile_options(${target} PRIVATE /GS- /guard:cf-)
target_link_options(${target} PRIVATE /STACK:67108864,67108864 /GUARD:NO)
+19 -6
View File
@@ -30,13 +30,26 @@ if (Test-Path (Join-Path $Root "RELEASE_NOTES.md")) {
Copy-Item (Join-Path $Root "RELEASE_NOTES.md") $Stage
}
foreach ($Dll in @("SDL2.dll", "libgcc_s_seh-1.dll", "libstdc++-6.dll")) {
$Source = Join-Path $MingwBin $Dll
if (!(Test-Path $Source)) {
throw "Required runtime DLL not found: $Source"
}
Copy-Item $Source $Stage
# The Release build is statically linked (PSX_STATIC_RUNTIME defaults ON for
# MinGW Release in runtime.cmake), so the exe imports ONLY Windows system DLLs
# — no SDL2.dll / libgcc_s_seh-1.dll / libstdc++-6.dll to bundle. Shipping
# those side-by-side was the cause of the 0xc000007b launch crash on user
# machines that had a mismatched copy earlier on the DLL search path.
#
# Assert self-containment rather than trust it: fail packaging if the exe
# imports any non-system DLL.
$objdump = Join-Path $MingwBin "objdump.exe"
$imports = & $objdump -p (Join-Path $Stage "PSXRecomp.exe") |
Select-String "DLL Name: (.+)" | ForEach-Object { $_.Matches[0].Groups[1].Value.Trim() }
$systemDlls = @("kernel32.dll","user32.dll","gdi32.dll","shell32.dll","msvcrt.dll",
"advapi32.dll","ws2_32.dll","comdlg32.dll","dbghelp.dll","ole32.dll",
"oleaut32.dll","winmm.dll","imm32.dll","version.dll","setupapi.dll",
"dinput8.dll","rpcrt4.dll","hid.dll","cfgmgr32.dll")
$nonSystem = $imports | Where-Object { $systemDlls -notcontains $_.ToLower() }
if ($nonSystem) {
throw "Release exe is NOT self-contained — imports non-system DLL(s): $($nonSystem -join ', ')"
}
Write-Host "Verified self-contained: imports only system DLLs ($($imports.Count) total)"
@"
; PSXRecomp input mapping. PSX buttons are active when any listed source is pressed.