Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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}
Expand All @@ -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$' && {
Expand Down
39 changes: 33 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()

Expand Down
Binary file added src/mcp-w32s.res
Binary file not shown.
11 changes: 7 additions & 4 deletions tests/test_binfmt.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
23 changes: 13 additions & 10 deletions tests/test_exec_ops.c
Original file line number Diff line number Diff line change
Expand Up @@ -335,29 +335,32 @@ 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 <builtin>" works and a bare "<builtin>"
* (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)
{
ExecResult r;
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");
}

/* ================================================================
Expand Down
10 changes: 2 additions & 8 deletions toolchains/mingw-w64-i386.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading