diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 379b642..1c6c7c3 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -125,6 +125,21 @@ jobs: # one alongside ours) would be a malformed resource directory; zero means # the *A APIs never switch to UTF-8. wrestool (icoutils) lists and extracts # the resource; --raw dumps the manifest bytes verbatim for the content grep. + # The MinGW build embeds a PREBUILT src/mcp-w32s.res (so the MSYS2 native + # runner needs no windres preprocessor, which is broken there on binutils + # 2.46). Guard the committed artifact against drift: regenerate the .res from + # the .rc + .manifest here - where the cross windres preprocessor works - and + # byte-diff. A stale .res fails the build and prints the regen command. + - name: Verify the prebuilt manifest .res matches its source + run: | + i686-w64-mingw32-windres -I src src/mcp-w32s.rc -O res -o /tmp/regen.res + if ! cmp -s /tmp/regen.res src/mcp-w32s.res; then + echo "ERROR: src/mcp-w32s.res is stale - regenerate it:" + echo " i686-w64-mingw32-windres -I src src/mcp-w32s.rc -O res -o src/mcp-w32s.res" + exit 1 + fi + echo "OK: prebuilt manifest .res matches mcp-w32s.rc + mcp-w32s.manifest." + - name: Verify exactly one UTF-8 activeCodePage manifest at RT_MANIFEST ID 1 run: | N=$(wrestool -l build/mingw/mcp-w32s.exe | grep -c -- '--type=24 --name=1' || true) @@ -444,10 +459,13 @@ jobs: windows-native: name: real-Windows (exec / Winsock / ConPTY, no Wine) runs-on: windows-latest - # Newly introduced job, still being stabilised on the MSYS2 toolchain - keep - # it informational so it cannot gate merges until it is reliably green; flip - # to a required check once the real-Windows run is consistently passing. - continue-on-error: true + # Gating (no longer continue-on-error): the manifest is embedded from a + # prebuilt .res (no windres preprocessor), the import check uses the + # MSYS2-unprefixed objdump, and the shell-builtin tests use a host-robust + # subject - so this job builds and runs the full exec/Winsock/ConPTY suite + # green on real Windows. It was informational while the MSYS2 toolchain was + # being stabilised. (Add it to the branch-protection required checks to make + # it a hard merge gate.) defaults: run: shell: msys2 {0} @@ -469,7 +487,9 @@ jobs: cmake --build --preset mingw - name: Verify the device still imports ONLY kernel32 + user32 run: | - IMPORTS=$(i686-w64-mingw32-objdump -p build/mingw/mcp-w32s.exe \ + # MSYS2 MINGW32 ships UNPREFIXED binutils (objdump, not + # i686-w64-mingw32-objdump - the cross-prefixed name does not exist here). + IMPORTS=$(objdump -p build/mingw/mcp-w32s.exe \ | sed -n 's/.*DLL Name: //p' | tr 'A-Z' 'a-z' | sort -u) echo "imports: $IMPORTS" echo "$IMPORTS" | grep -vqE '^(kernel32|user32)\.dll$' && { diff --git a/CMakeLists.txt b/CMakeLists.txt index fb3017c..0b35b14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,10 +15,37 @@ cmake_minimum_required(VERSION 3.21) project(mcp-w32s C) -# RC compiles the application manifest (src/mcp-w32s.rc) into mcp-w32s.exe only. -# windres on MinGW (CMAKE_RC_COMPILER, set by the toolchain) and rc.exe on MSVC -# both drive a .rc through this language - no per-toolchain special-casing. -enable_language(RC) +# RT_MANIFEST (the UTF-8 activeCodePage manifest), embedded in mcp-w32s.exe only. +# On MSVC, rc.exe compiles src/mcp-w32s.rc directly. On MinGW the manifest is +# shipped as a PREBUILT, byte-deterministic resource (src/mcp-w32s.res, generated +# from mcp-w32s.rc + mcp-w32s.manifest): compiling the committed .res to a coff +# object needs NO preprocessor, so it builds on every runner - including the +# MSYS2 native-Windows CI runner whose windres (binutils 2.46) cannot spawn a +# preprocessor to compile the .rc directly ("CreateProcess failed: The system +# cannot find the file specified", even with a full-path .exe). The SAME manifest +# lands in every binary; the Ubuntu CI job regenerates the .res from source and +# byte-diffs it, so the committed artifact can never drift from the .rc/.manifest. +enable_language(RC) # MSVC: rc.exe compiles the .rc. (MinGW uses the .res below.) +if(MSVC) + set(MCP_MANIFEST src/mcp-w32s.rc) +else() + # Resolve windres to a FULL PATH. The custom command below runs via cmd.exe on + # the MSYS2 native runner, where a bare `i686-w64-mingw32-windres` is "not + # recognized" - CMake's own RC rule resolves it internally, but the + # ${CMAKE_RC_COMPILER} variable stays the bare toolchain name. find_program runs + # at configure time in the MSYS2 PATH and yields the full path cmd.exe can spawn. + find_program(MCP_WINDRES NAMES "${CMAKE_RC_COMPILER}" windres REQUIRED) + set(MCP_MANIFEST ${CMAKE_BINARY_DIR}/mcp-w32s_manifest.obj) + add_custom_command( + OUTPUT ${MCP_MANIFEST} + COMMAND ${MCP_WINDRES} --input-format=res -O coff + ${CMAKE_CURRENT_SOURCE_DIR}/src/mcp-w32s.res ${MCP_MANIFEST} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/mcp-w32s.res + COMMENT "Embedding the prebuilt RT_MANIFEST (res -> coff, no preprocessor)" + VERBATIM) + set_source_files_properties(${MCP_MANIFEST} PROPERTIES + EXTERNAL_OBJECT TRUE GENERATED TRUE) +endif() # C89 strict, no GNU extensions (matches the historic -std=c89 -pedantic). set(CMAKE_C_STANDARD 90) @@ -95,11 +122,11 @@ set(CORE_SOURCES # never on Win32s. The VC6 toolchain path is unchanged. # (Phase 6 Finding #1 remediation; see plan/PHASE6.md in the host repo.) if(CMAKE_C_COMPILER_ID STREQUAL "GNU") - add_executable(mcp-w32s src/mcp-w32s.c ${CORE_SOURCES} src/mini_crt.c src/mcp-w32s.rc) + add_executable(mcp-w32s src/mcp-w32s.c ${CORE_SOURCES} src/mini_crt.c ${MCP_MANIFEST}) target_link_options(mcp-w32s PRIVATE -nostdlib -mconsole) target_link_libraries(mcp-w32s PRIVATE kernel32 user32 gcc) else() - add_executable(mcp-w32s src/mcp-w32s.c ${CORE_SOURCES} src/mcp-w32s.rc) + add_executable(mcp-w32s src/mcp-w32s.c ${CORE_SOURCES} ${MCP_MANIFEST}) target_link_libraries(mcp-w32s PRIVATE kernel32 user32) endif() diff --git a/src/mcp-w32s.res b/src/mcp-w32s.res new file mode 100644 index 0000000..1bee3b2 Binary files /dev/null and b/src/mcp-w32s.res differ diff --git a/tests/test_binfmt.c b/tests/test_binfmt.c index 7b149b3..f1233e2 100644 --- a/tests/test_binfmt.c +++ b/tests/test_binfmt.c @@ -209,16 +209,19 @@ TEST_CASE(missing_file_errors) { TEST_ASSERT(err[0] != '\0', "error message populated"); } -/* 6. Shell built-in name ("dir") -> BIN_SHELL without file read */ +/* 6. Shell built-in name -> BIN_SHELL without file read. Use "ver" (a cmd + * built-in with no .exe equivalent on ANY host), not "dir": some runners ship a + * coreutils dir.exe on PATH (e.g. MSYS2 /usr/bin on the native-Windows CI), so + * "dir" correctly resolves to a real PE there - a host-tolerance trap. */ TEST_CASE(shell_builtin_is_shell) { char err[128]; BinaryType type; int ok; type = BIN_UNKNOWN; - ok = BinFmtClassify("dir", &type, err, sizeof(err)); - TEST_ASSERT_INT_EQUAL(1, ok, "classify 'dir' succeeds"); - TEST_ASSERT_INT_EQUAL(BIN_SHELL, type, "'dir' is shell built-in"); + ok = BinFmtClassify("ver", &type, err, sizeof(err)); + TEST_ASSERT_INT_EQUAL(1, ok, "classify 'ver' succeeds"); + TEST_ASSERT_INT_EQUAL(BIN_SHELL, type, "'ver' is shell built-in"); } /* 7. Uplift: after FeatInit, if pGetBinaryTypeA present, self still PE32. */ diff --git a/tests/test_exec_ops.c b/tests/test_exec_ops.c index 3077566..91a788c 100644 --- a/tests/test_exec_ops.c +++ b/tests/test_exec_ops.c @@ -335,11 +335,14 @@ TEST_CASE(invalid_base64_is_dispatcher) } /* ================================================================ - * #15 - shell=true vs shell=false: dir works only via shell. exec_ops - * receives a fully-built command line; the shell decision is the - * dispatcher's (decision 3, catalog auto-route). At the exec_ops - * layer we verify that "cmd /c dir" works and a bare "dir" (no - * shell wrap) spawn-fails - dir is not an .exe. + * #15 - shell=true vs shell=false: a cmd built-in works only via shell. + * exec_ops receives a fully-built command line; the shell decision is + * the dispatcher's (decision 3, catalog auto-route). At the exec_ops + * layer we verify that "cmd /c " works and a bare "" + * (no shell wrap) spawn-fails - the built-in is not an .exe. Use "ver", + * not "dir": some runners ship a coreutils dir.exe on PATH (e.g. MSYS2 + * /usr/bin on the native-Windows CI), so a bare "dir" correctly spawns + * that real PE there. "ver" has no .exe equivalent on any host. * ================================================================ */ TEST_CASE(shell_dir_routing) { @@ -347,17 +350,17 @@ TEST_CASE(shell_dir_routing) char msg[128]; int ok_shell, ok_bare; clear_bufs(); - ok_shell = ExecOpRun("cmd /c dir", "C:\\", T_TIMEOUT, 1, + ok_shell = ExecOpRun("cmd /c ver", "C:\\", T_TIMEOUT, 1, NULL, 0, g_out, sizeof(g_out), g_err, sizeof(g_err), 0, 0, BIN_PE32, &r, msg, sizeof(msg)); - TEST_ASSERT(ok_shell, "dir via shell works"); - TEST_ASSERT_INT_EQUAL(0, r.exit_code, "shelled dir exit 0"); + TEST_ASSERT(ok_shell, "ver via shell works"); + TEST_ASSERT_INT_EQUAL(0, r.exit_code, "shelled ver exit 0"); clear_bufs(); - ok_bare = ExecOpRun("dir", "C:\\", T_TIMEOUT, 1, + ok_bare = ExecOpRun("ver", "C:\\", T_TIMEOUT, 1, NULL, 0, g_out, sizeof(g_out), g_err, sizeof(g_err), 0, 0, BIN_PE32, &r, msg, sizeof(msg)); - TEST_ASSERT(!ok_bare, "bare dir (no .exe) spawn-fails"); + TEST_ASSERT(!ok_bare, "bare ver (no .exe) spawn-fails"); } /* ================================================================ diff --git a/toolchains/mingw-w64-i386.cmake b/toolchains/mingw-w64-i386.cmake index 1ddec3b..6600d8d 100644 --- a/toolchains/mingw-w64-i386.cmake +++ b/toolchains/mingw-w64-i386.cmake @@ -17,19 +17,13 @@ set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres) # the PEs run against real cmd.exe/Winsock/ConPTY instead of Wine), the # compiler's own sysroot is already correct and the Linux path below does not # exist, so leave CMake's default search alone there. +# (The windres preprocessor is configured universally in CMakeLists.txt - with +# the resolved full-path compiler - so it bakes the manifest in on both hosts.) if(NOT CMAKE_HOST_WIN32) set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -else() - # MSYS2's i686 windres fails to spawn its default preprocessor ("CreateProcess - # failed: The system cannot find the file specified") when CMake drives it. - # Point it at the mingw32 gcc explicitly and use a temp file rather than the - # pipe spawn that misbehaves on Windows. (binutils 2.36+ --preprocessor-arg - # syntax.) Linux cross-windres needs none of this and is left untouched. - set(CMAKE_RC_FLAGS - "--use-temp-file --preprocessor=i686-w64-mingw32-gcc --preprocessor-arg=-E --preprocessor-arg=-xc-header --preprocessor-arg=-DRC_INVOKED") endif() # i386-only codegen + the strict warning gate the project has always used.