Skip to content

Instantly share code, notes, and snippets.

@Kreijstal
Last active April 28, 2026 03:21
Show Gist options
  • Select an option

  • Save Kreijstal/c4885eccf8f474f916375118ce347594 to your computer and use it in GitHub Desktop.

Select an option

Save Kreijstal/c4885eccf8f474f916375118ce347594 to your computer and use it in GitHub Desktop.
MSYS2-on-ReactOS cross-toolchain build log: patches, flags, rationale per stage

MSYS2-on-ReactOS cross-toolchain build log

Linux-hosted cross toolchain targeting x86_64-pc-cygwin (validation phase) before morphing to x86_64-pc-msys. Goal is bringing the full MSYS2 environment up on ReactOS; ReactOS will gain whatever Win32/NT API surface MSYS2 needs (we do not fork the runtime to paper over ReactOS gaps).

Recipe transcribed from NixOS/nixpkgs PR #444470 (pkgsCross.x86_64-cygwin, head e098bea39d) into plain shell scripts (no nix on host).

Workspace: ~/git/msys-cross/{src,build,prefix,patches,scripts,logs}.

Source pinning

Component Version Source
newlib-cygwin tag cygwin-3.6.4 git://cygwin.com/git/newlib-cygwin.git
binutils 2.46.0 https://ftp.gnu.org/gnu/binutils/binutils-2.46.0.tar.xz
gcc 15.2.0 https://ftp.gnu.org/gnu/gcc/gcc-15.2.0/gcc-15.2.0.tar.xz
mingw-w64 13.0.0 https://downloads.sourceforge.net/project/mingw-w64/mingw-w64/mingw-w64-release/mingw-w64-v13.0.0.tar.bz2
cocom 0.996 https://sourceforge.net/projects/cocom/files/cocom-0.996.tar.gz/download

Patches in use

newlib-cygwin (3 patches, from nixpkgs PR #444470 pkgs/os-specific/cygwin/newlib-cygwin/):

  • fix-winsize.patch — backport of cygwin commit 73600d6, moves struct winsize decl above first use in winsup/cygwin/include/sys/termios.h
  • store-tls-pointer-in-win32-tls.patch — restores TLS pointer storage in Win32 TLS slot (needed for boost coroutines)
  • fix-cross.patch — three hunks: drop _GCC_AUTOCONF_VERSION_CHECK, change -L to -B for crt0.o lookup, force EXEEXT=.exe in winsup/configure.ac

gcc (2 patches, from sourceware mailing list, author corngood@gmail.com):


Stage 04: binutils-2.46.0

No source patches required. Stock upstream builds clean for x86_64-pc-cygwin.

Configure flags

"$SRC_DIR/configure" \
  --target=x86_64-pc-cygwin \
  --prefix="$PREFIX" \
  --with-sysroot="$SYSROOT" \
  --disable-werror \
  --disable-multilib \
  --disable-nls \
  --disable-gdb \
  --disable-gprofng \
  --enable-64-bit-bfd \
  --enable-deterministic-archives

Verification

$ x86_64-pc-cygwin-as --version | head -1
GNU assembler (GNU Binutils) 2.46.0.20260210
$ x86_64-pc-cygwin-ld -V 2>&1 | grep i386
   i386pep

20 cross tools installed at $PREFIX/bin/x86_64-pc-cygwin-* (as, ld, ld.bfd, ar, nm, objcopy, objdump, ranlib, readelf, strip, strings, size, addr2line, c++filt, dlltool, dllwrap, elfedit, gprof, windmc, windres).


Stage 01: cocom-0.996

cocom is a 2003-era codebase; gcc 15 rejects K&R-style int defaults and implicit-function-declarations as errors (these used to be warnings). The autoconf-2.13 sub-configure in REGEX/ also chokes on autoconf-2.59 positional CC=/CFLAGS= forwarding.

Only msta (parser generator) is needed — that's what newlib-cygwin's build wants. SPRUT, AMMUNITION (libcocom) are msta's transitive deps. DINO/REGEX/OKA/NONA/SHILKA are skipped entirely.

Source patches

src/cocom-0.996/SPRUT/yacc.h:45 — K&R prototype that gcc 15 won't accept inside SPRUT/yacc.y:

-extern yyparse ();
+extern int yyparse (void);

src/cocom-0.996/REGEX/configure:666 (kept dormant — REGEX is hidden during configure, see below — but applied for completeness):

-main(){return(0);}
+int main(void){return(0);}

Build environment

Demote gcc 15 K&R-era diagnostics back to warnings:

LEGACY_CFLAGS="-O2 -g -fcommon \
  -Wno-error=implicit-int \
  -Wno-error=implicit-function-declaration \
  -Wno-error=incompatible-pointer-types \
  -Wno-error=int-conversion \
  -Wno-implicit-int \
  -Wno-implicit-function-declaration \
  -Wno-incompatible-pointer-types \
  -Wno-int-conversion"
LEGACY_CXXFLAGS="-O2 -g -fpermissive -Wno-write-strings"

export CC="gcc"
export CXX="g++"
export CFLAGS="$LEGACY_CFLAGS"
export CXXFLAGS="$LEGACY_CXXFLAGS"

Configure trick

Hide REGEX/ during ./configure so its broken sub-configure isn't recursed into. Restore via EXIT trap:

if [ -d "$SRC_DIR/REGEX" ]; then
  mv "$SRC_DIR/REGEX" "$SRC_DIR/REGEX.disabled"
  trap 'mv "$SRC_DIR/REGEX.disabled" "$SRC_DIR/REGEX" 2>/dev/null || true' EXIT
fi
"$SRC_DIR/configure" --prefix="$PREFIX"
if [ -d "$SRC_DIR/REGEX.disabled" ]; then
  mv "$SRC_DIR/REGEX.disabled" "$SRC_DIR/REGEX"
  trap - EXIT
fi

Build sequence

Only AMMUNITION (libcocom), SPRUT (generates ird.[ch] for MSTA), and MSTA. Each -j1 because AMMUNITION's Makefile races between C and C++ on the bison-generated sgramm.c:

( cd AMMUNITION && make -j1 && make install )
( cd SPRUT      && make -j1 && make install )
( cd MSTA       && make -j1 && make install )

Critical gotcha

Do NOT pass CFLAGS= on make's command line. That overrides the Makefile's own assignment and drops $(OPTIONS) = -DHAVE_CONFIG_H -I.. -I. -I$srcdir, which breaks #include "sgramm.c" inside earley.c. Configure already baked our flags into the Makefile, leave make alone.

Verification

$ /home/kreijstal/git/msys-cross/prefix/bin/msta
/home/kreijstal/git/msys-cross/prefix/bin/msta options:
command line: msta [ options ...] specification-file
[...]

Binary is 1010624 bytes. msta has no --version; printing usage and exit 1 with no args is the success signal.



Stage 02: w32api-headers (mingw-w64 v13.0.0)

mingw-w64-headers/configure ships an --enable-w32api shorthand specifically for the cygwin layout. Per its --help:

--enable-w32api shorthand for --includedir=PREFIX/include/w32api --disable-crt

So a plain --prefix=$SYSROOT --enable-w32api lands headers at $SYSROOT/include/w32api/ natively. No post-install mv gymnastics.

Configure flags

"$SRC_DIR/configure" \
  --build="$($SRC_DIR/build-aux/config.guess)" \
  --host=x86_64-pc-cygwin \
  --prefix="$SYSROOT" \
  --enable-w32api \
  --enable-sdk=all \
  --enable-idl \
  --with-default-msvcrt=msvcrt

--with-default-msvcrt=msvcrt (not the v13.0.0 default ucrt) — must stay consistent in stage 06 (w32api libs) and stages 05/08 (gcc) so __MSVCRT_VERSION__ and import-lib selection match.

Verification

$ ls $SYSROOT/include/w32api/ | wc -l
1791
$ find $SYSROOT/include/w32api -name '*.h' | wc -l
1572

Preprocessor smoke (host gcc with -D__CYGWIN__ since stage-05 target gcc isn't built yet):

printf '#include <windows.h>\nint main(void){return 0;}\n' \
  | gcc -E -D__CYGWIN__ -isystem $SYSROOT/include/w32api -x c - \
  > /dev/null  # exit 0; ~109 055 lines of expansion, no stderr

Note for stage 05

Configure gcc-stage1 with --with-sysroot=$SYSROOT --with-native-system-header-dir=/include/w32api so #include <windows.h> resolves without callers needing explicit -isystem.



Stage 03: runtime-headers (cp-only stage)

Per nixpkgs PR #444470's newlib-cygwin-headers derivation: just two cp -rs, no configure or build.

cp -r "$SRC/newlib-cygwin/newlib/libc/include/."   "$SYSROOT/include/"
cp -r "$SRC/newlib-cygwin/winsup/cygwin/include/." "$SYSROOT/include/"

The newlib + winsup trees have no top-level filename collisions and neither uses w32api/ as a path, so this safely coexists with stage 02's mingw-w64 headers in the same $SYSROOT/include/ directory.

Verification

  • 265 .h files staged (excluding w32api/)
  • CYGWIN_VERSION_API_MAJOR=0, CYGWIN_VERSION_API_MINOR=357, CYGWIN_VERSION_DLL_MAJOR=3006, CYGWIN_VERSION_DLL_MINOR=4 (matches cygwin-3.6.4 tag)
  • $SYSROOT/include/w32api/windows.h intact (mtime unchanged from stage 02)

Stage 05: gcc-stage1 (gcc-15.2.0)

C and C++ frontends only — --without-headers --with-newlib --disable-libstdcxx. No target runtime libs (libgcc, libstdc++) built yet; this compiler exists purely to compile cygwin1.dll in stage 07. We rebuild gcc end-to-end in stage 08 once the runtime exists, against real headers + libs.

Patches applied

Two patches from sourceware mailing list, both author corngood@gmail.com, both required for gcc 15.2.0 against the cygwin/freestanding configuration:

Marker file $SRC_DIR/.cygwin-patches-applied keeps the script idempotent.

Prerequisite libraries

./contrib/download_prerequisites pulls these into the gcc source tree as in-tree subdirs (built statically, not installed separately):

Lib Version
gmp 6.2.1
mpfr 4.1.0
mpc 1.2.1
isl 0.24

Configure flags

"$SRC_DIR/configure" \
  --target=x86_64-pc-cygwin \
  --prefix="$PREFIX" \
  --with-sysroot="$SYSROOT" \
  --with-native-system-header-dir=/include \
  --enable-languages=c,c++ \
  --disable-multilib \
  --disable-nls \
  --disable-shared \
  --disable-libssp \
  --disable-libstdcxx \
  --disable-libquadmath \
  --disable-threads \
  --disable-tls \
  --disable-bootstrap \
  --without-headers \
  --with-newlib \
  --enable-checking=release

--with-native-system-header-dir=/include is sysroot-relative — gcc will look in $SYSROOT/include/ for the runtime headers staged in stage 03 (newlib + cygwin) and in $SYSROOT/include/w32api/ (stage 02). --with-newlib here is a misnomer in our setup; it just suppresses the libgcc EH model that would assume glibc, and pairs with --without-headers for the bootstrap compiler.

Build sequence

make -j$JOBS all-gcc
make install-gcc

all-gcc / install-gcc only — skips libgcc/libstdc++/target-libs entirely. Those need a working runtime, which we don't have yet.

Verification

$ x86_64-pc-cygwin-gcc --version | head -1
x86_64-pc-cygwin-gcc (GCC) 15.2.0
$ x86_64-pc-cygwin-gcc -dumpmachine
x86_64-pc-cygwin
$ x86_64-pc-cygwin-gcc -v 2>&1 | tail -3
Thread model: single
Supported LTO compression algorithms: zlib zstd
gcc version 15.2.0 (GCC)

Thread model: single is expected — --disable-threads suppresses pthread/win32 thread shims since we have no runtime to host them; stage 08 turns this into posix.

Cross binaries installed at $PREFIX/bin/x86_64-pc-cygwin-{gcc,g++,c++,cpp,gcc-{ar,nm,ranlib},gcov,gcov-{dump,tool},lto-{dump}}. cc1/cc1plus/lto1/collect2 land in $PREFIX/libexec/gcc/x86_64-pc-cygwin/15.2.0/.


Stage 06 — w32api libs (mingw-w64 v13.0.0 mingw-w64-crt)

Headers landed in stage 02 at $SYSROOT/include/w32api/. Stage 06 builds the companion import libs / w32api CRT subset — the lib*.a files Cygwin compilers link against (libkernel32.a, libuser32.a, libgdi32.a, libws2_32.a, plus ~880 more device/SDK import libs).

Source: $SRC/mingw-w64-v13.0.0/mingw-w64-crt/ (already extracted in stage 02 from the same tarball). No autoreconf was needed — the shipped configure worked as-is.

Build dir: $BUILD/w32api-libs.

Configure (matches stage 02's headers contract; in particular --with-default-msvcrt=msvcrt so __MSVCRT_VERSION__ agrees end-to-end):

"$SRC/mingw-w64-v13.0.0/mingw-w64-crt/configure" \
  --build="$($SRC/mingw-w64-v13.0.0/mingw-w64-crt/build-aux/config.guess)" \
  --host=x86_64-pc-cygwin \
  --prefix=$SYSROOT \
  --enable-w32api \
  --with-default-msvcrt=msvcrt \
  --disable-dependency-tracking

--enable-w32api on the crt dir is the Cygwin-specific shorthand: it routes installs to $SYSROOT/lib/w32api/ (lib64) and $SYSROOT/lib32/w32api/ (lib32), mirroring the /include/w32api/ header layout from stage 02. Both 32-bit and 64-bit import libs are built — the cross gcc is multilib-aware via -m64/-m32 on the same x86_64-pc-cygwin-gcc.

make -j$JOBS && make install ran clean against the stage 05 cross compiler. No patches, no overrides, no extra env. Total build/install (excluding the prior stage-02 header source extract): a few minutes on a 16-core box.

Verification

$ ls $SYSROOT/lib/w32api/ | wc -l
891
$ ls $SYSROOT/lib/w32api/lib*.a | head -10
.../libacledit.a
.../libaclui.a
.../libactiveds.a
.../libadmparse.a
.../libadmwprox.a
.../libadptif.a
.../libadsiid.a
.../libadsiisex.a
.../libadsldpc.a
.../libadvapi32.a
$ ls $SYSROOT/lib/w32api/{libkernel32,libuser32,libgdi32,libws2_32}.a
.../libkernel32.a  .../libuser32.a  .../libgdi32.a  .../libws2_32.a
$ x86_64-pc-cygwin-ar t $SYSROOT/lib/w32api/libkernel32.a | head -5
libkernel32t.o
libkernel32h.o
libkernel32s01661.o
libkernel32s01660.o
libkernel32s01659.o
$ x86_64-pc-cygwin-nm $SYSROOT/lib/w32api/libkernel32.a 2>&1 | grep -cE ' [TU] '
3435

The ar t output (*t.o, *h.o, *sNNNNN.o thunks) is the standard dlltool/-k short-import-library layout: head/tail markers plus one stub per exported symbol. 3435 T/U entries is consistent with the full kernel32 export surface (Windows 10 SDK level).

Footnote — pipefail in verification

First run aborted just before the === OK === banner because ls ... | head -10 under set -o pipefail fails when head closes the pipe early (SIGPIPE → ls exit 141 → script exits). All install steps had completed; only the cosmetic verification footer was truncated. Fixed in scripts/06-w32api-libs.sh by wrapping the verification pipelines in set +o pipefail/set -o pipefail. Worth noting for stage 07+ which use the same ls | head pattern.


Stage 07 — newlib-cygwin runtime (cygwin-3.6.4)

Builds cygwin1.dll (the cygwin POSIX runtime), libcygwin.a (its import lib), all standard cygwin libraries (libc.a, libm.a, libpthread.a, libdl.a, libacl.a, libaio.a, libresolv.a, librt.a, libssp.a, libutil.a, libg.a, libgmon.a), CRT startup objects (crt0.o, gcrt0.o, automode.o, binmode.o, textmode.o, textreadmode.o), and the standard cygwin user-tools (cygpath.exe, mount.exe, ps.exe, kill.exe, etc. — all PE/cygwin executables emitted by the cross compiler, useful as link-time validation that the toolchain works).

This is the validation gate of Phase 1: once cygwin1.dll exists, the toolchain can produce real cygwin executables.

Patches

3 patches from nixpkgs PR #444470 pkgs/os-specific/cygwin/newlib-cygwin/, applied in $SRC/newlib-cygwin/ via patch -p1 and tracked by marker .cygwin-patches-applied:

  • fix-winsize.patch — backport of cygwin commit 73600d6, moves struct winsize decl above first use in winsup/cygwin/include/sys/termios.h.
  • store-tls-pointer-in-win32-tls.patch — restores TLS pointer storage in Win32 TLS slot (boost coroutines depend on this).
  • fix-cross.patch — three hunks: drop _GCC_AUTOCONF_VERSION_CHECK (newer autoconf than the source assumes), change -L to -B for crt0.o lookup, force EXEEXT=.exe in winsup/configure.ac.

fix-cross.patch touches both winsup/configure.ac and the top-level configure.ac, so both need autoreconf -fi afterward (matching nixpkgs autoreconfFlags = ["winsup" "."]). Tracked by marker .cygwin-autoreconf-done.

Two prerequisites earlier stages don't supply

Both bootstrapped in-place at the start of stage 07:

(a) shilka (cocom)winsup/cygwin/scripts/gendevices shells out to shilka to generate devices.cc. Stage 01 deliberately built only msta + sprut + AMMUNITION. Build SHILKA from the same $BUILD/cocom/SHILKA/ build dir, with -std=gnu17 injected into the configure-baked CFLAGS (gcc 15 K&R prototype error in SHILKA/yacc.h:45 form, same class as the SPRUT/yacc.h fix in stage 01) — sed -i 's|^CFLAGS = -O2 -g -fcommon|CFLAGS = -O2 -g -fcommon -std=gnu17|' Makefile, then make -j$JOBS && make install. Lands at $PREFIX/bin/shilka (≈ 330 KB).

(b) Freestanding libstdc++ headerswinsup/cygwin/cxx.cc includes <new> (for std::nothrow_t, std::size_t). Stage 05 passed --disable-libstdcxx, which dropped not just the library but also the C++ headers entirely. Reconfigure stage 5's existing $BUILD/gcc-stage1/ with --disable-hosted-libstdcxx --disable-libstdcxx-pch instead of --disable-libstdcxx, then make all-target-libstdc++-v3 && make install-target-libstdc++-v3. Lands <new> at $SYSROOT/include/c++/15.2.0/ and libstdc++.a at $SYSROOT/lib/. The --disable-hosted-libstdcxx flag is what corngood's gcc patches in stage 05 are designed for: it builds libstdc++ in freestanding mode (libsupc++-only — exception/RTTI/new-delete machinery, no iostream/locale/filesystem) without needing a target libc. Tracked by checking [ -f "$LIBSTDCXX_HDR" ] where LIBSTDCXX_HDR="$SYSROOT/include/c++/$GCC_VER/new".

Build env (from nixpkgs newlib-cygwin/default.nix)

export CFLAGS_FOR_TARGET="-Wl,-L$SYSROOT/lib/w32api"
export CXXFLAGS_FOR_TARGET="-Wno-error=register -Wl,-L$SYSROOT/lib/w32api"
export CFLAGS="${CFLAGS:-} -U_FORTIFY_SOURCE -fno-stack-protector"
export CXXFLAGS="${CXXFLAGS:-} -U_FORTIFY_SOURCE -fno-stack-protector"

-Wno-error=register is needed because winsup/cygwin/cygtls.cc:29 uses the register storage class which gcc 15 / C++17 demotes to a warning that becomes an error under the tree's -Werror. -U_FORTIFY_SOURCE -fno-stack-protector mirrors nixpkgs hardeningDisable = ["fortify" "stackprotector"] (cygwin1.dll bootstraps before any of that hardening machinery is functional).

Configure flags (top-level combined tree, verbatim from nixpkgs)

"$SRC/newlib-cygwin/configure" \
  --build="$(uname -m)-pc-linux-gnu" \
  --target=x86_64-pc-cygwin \
  --prefix="$PREFIX" \
  --disable-shared \
  --disable-doc \
  --enable-static \
  --disable-dumper \
  --with-cross-bootstrap \
  ac_cv_prog_CC=gcc

--with-cross-bootstrap is the cygwin-specific flag that tells the build it is producing the runtime for the first time (no pre-existing libcygwin.a to link against). ac_cv_prog_CC=gcc short-circuits the build-side compiler probe to host gcc (avoids it discovering and using the cross compiler for build-side tools).

configurePlatforms = [ "build" "target" ] in nixpkgs ⇒ --build= and --target= only (deliberately no --host=); the omitted --host defaults equal to --build so build-side tools compile native and target-side tools cross.

Build

make -j$JOBS tooldir="$PREFIX"
make -j1 install tooldir="$PREFIX"     # nixpkgs enableParallelInstalling=false

tooldir=$PREFIX matches nixpkgs makeFlags = [ "tooldir=${placeholder \"out\"}" ] (their out = our $PREFIX).

Sysroot staging (post-install copy)

The newlib-cygwin top-level Makefile installs to $PREFIX/{bin,lib}/ regardless of --with-sysroot (its install paths predate the GNU sysroot convention). For stage 08 (gcc-final, configured with --with-sysroot=$SYSROOT) and for -isystem/-B lookup against the sysroot, the runtime artifacts have to live under $SYSROOT/{bin,lib}/. Copy them after install:

cp -p "$PREFIX/bin/cygwin1.dll" "$SYSROOT/bin/"
for f in libcygwin.a libc.a libm.a libpthread.a libdl.a libacl.a libaio.a \
         libresolv.a librt.a libssp.a libutil.a libg.a libgmon.a \
         crt0.o gcrt0.o automode.o binmode.o textmode.o textreadmode.o; do
  cp -p "$PREFIX/lib/$f" "$SYSROOT/lib/$f"
done

Verification

$ ls -la $SYSROOT/bin/cygwin1.dll
-rwxr-xr-x ... 5002978 ... cygwin1.dll
$ x86_64-pc-cygwin-objdump -p $SYSROOT/bin/cygwin1.dll | grep 'DLL Name'
        DLL Name: KERNEL32.dll
        DLL Name: ntdll.dll
$ x86_64-pc-cygwin-nm $SYSROOT/lib/libcygwin.a | grep -cE ' [TU] '
1688
$ ls $SYSROOT/lib/{libc,libm,libpthread,libdl,crt0,gcrt0}.{a,o} 2>/dev/null | wc -l
6

5 MB cygwin1.dll, imports KERNEL32 + ntdll only (the correct dependency surface for a cygwin runtime — no msvcrt/ucrt direct linkage), 1688 exported symbols in libcygwin.a.

End-to-end smoke

The Phase 1 validation gate is "can the toolchain produce a real cygwin executable" — answer: yes.

$ cat > /tmp/hello.c <<'EOF'
#include <stdio.h>
int main(int argc, char **argv) { printf("hello cygwin from %s\n", argv[0]); return 0; }
EOF
$ x86_64-pc-cygwin-gcc /tmp/hello.c -o /tmp/hello.exe
$ ls -la /tmp/hello.exe
-rwxr-xr-x ... 26396 ... hello.exe
$ x86_64-pc-cygwin-objdump -p /tmp/hello.exe | grep 'DLL Name'
        DLL Name: cygwin1.dll
        DLL Name: KERNEL32.dll

Cross-compiled cygwin executable, imports cygwin1.dll + KERNEL32.dll exactly as a real cygwin executable should.

End-to-end execution under wine (cygwin1.dll alongside hello.exe in /tmp/):

$ cp $SYSROOT/bin/cygwin1.dll /tmp/cygwin1.dll
$ wine /tmp/hello.exe
... (wine NT-driver fixme noise) ...
Cygwin WARNING:
  Couldn't compute FAST_CWD pointer.  This typically occurs if you're using
  an older Cygwin version on a newer Windows.  ...
hello cygwin from /tmp/hello

The program runs, completes its printf, and exits cleanly. The "Couldn't compute FAST_CWD pointer" warning is cygwin's Windows-internals compatibility probe failing under wine's NT-emulation surface (different from a real Windows or ReactOS kernel) — informational, not fatal. ReactOS validation comes in stage 09 once we get a build to a ReactOS VM.


Stage 08 — gcc-final (gcc-15.2.0, full target libs) — VERIFIED

Rebuilds the gcc cross compiler end-to-end with all target libraries built against the just-installed cygwin runtime: libgcc, full hosted libstdc++ (iostream/locale/filesystem/regex/threads), libgomp, libatomic, libbacktrace. Replaces stage 5's Thread model: single stage1 compiler with Thread model: posix.

Patches

Same two corngood patches from stage 05 (libstdc-fix-compilation-in-freestanding-win32.patch, cygwin-fix-compilation-with-inhibit_libc.patch). Already applied; no new patches in stage 08.

Build dir

$BUILD/gcc-final/ (fresh — not the same dir as stage 5's $BUILD/gcc-stage1/, which was contaminated by the --disable-hosted-libstdcxx reconfigure during stage 7's freestanding-headers install).

Configure flags (transcribed from corngood/nixpkgs e098bea39d, pkgs/development/compilers/gcc/common/configure-flags.nix, the !withoutTargetLibc branch + cygwin specifics)

"$SRC/gcc-15.2.0/configure" \
  --target=x86_64-pc-cygwin \
  --prefix="$PREFIX" \
  --with-sysroot="$SYSROOT" \
  --with-native-system-header-dir=/include \
  --with-gxx-include-dir="$PREFIX/include/c++/15.2.0" \
  --enable-languages=c,c++ \
  --enable-__cxa_atexit \
  --enable-long-long \
  --enable-threads=posix \
  --enable-tls \
  --enable-shared \
  --enable-static \
  --enable-checking=release \
  --disable-multilib \
  --disable-nls \
  --disable-libssp \
  --disable-libquadmath \
  --disable-libstdcxx-pch \
  --disable-bootstrap \
  --without-included-gettext \
  --with-system-zlib

Deviation from nixpkgs: dropped --with-build-sysroot=/. Nixpkgs uses it for nix-store relocation, but combined with --with-native-system-header-dir=/include it makes gcc concatenate them into a literal //include and fail at fixinc: The directory (BUILD_SYSTEM_HEADER_DIR) that should contain system headers does not exist: //include. Without the flag, build-sysroot defaults to --with-sysroot, so gcc looks at $SYSROOT/include — the right place.

Diff vs stage 5 (05-gcc-stage1.sh):

Flag Stage 05 Stage 08 Why
--without-headers yes dropped sysroot now has cygwin/newlib + w32api headers
--with-newlib yes dropped real cygwin runtime exists now
--disable-libstdcxx yes dropped (full libstdc++) runtime can host iostream/etc.
--disable-shared yes --enable-shared want libgcc_s.dll, cygstdc++-6.dll
--disable-threads yes --enable-threads=posix cygwin pthreads via libcygwin/cygwin1.dll
--disable-tls yes --enable-tls runtime supports TLS now
--enable-__cxa_atexit yes required by libstdc++ for static dtors
--with-build-sysroot=/ dropped (see deviation note above) nixpkgs flag breaks our layout
--with-gxx-include-dir=... yes force C++ headers under $PREFIX/include/c++/15.2.0/
--with-system-zlib yes use host zlib, don't build in-tree
--without-included-gettext yes don't pull in-tree gettext (libintl)

Build-sysroot in our layout: with no --with-build-sysroot, gcc uses --with-sysroot for both build and runtime header lookups. Cygwin headers already live at $SYSROOT/include/ (newlib + cygwin) and $SYSROOT/include/w32api/ (mingw-w64), so the default behaviour finds them. The double-prefix concern that nixpkgs's --with-build-sysroot=/ works around does not apply here because we install with make install directly, not via a relocatable nix store path.

Build

make -j$JOBS         # all-targets, not just all-gcc
make install

This is the long step — host gcc + libgcc + full libstdc++-v3 + libgomp + libatomic + libbacktrace + libssp's stubs. ~30–90 min on a multicore.

Verification (actual output, 2026-04-27)

$ x86_64-pc-cygwin-gcc --version | head -1
x86_64-pc-cygwin-gcc (GCC) 15.2.0
$ x86_64-pc-cygwin-gcc -dumpmachine
x86_64-pc-cygwin
$ x86_64-pc-cygwin-gcc -v 2>&1 | grep -i 'thread model'
Thread model: posix                                  # ← was 'single' in stage 05

$ ls $SYSROOT/lib/cyg*.dll
.../lib/cygatomic-1.dll
.../lib/cyggcc_s-seh-1.dll
.../lib/cygstdc++-6.dll
$ ls $SYSROOT/bin/cyg*.dll
.../bin/cygwin1.dll                                  # from stage 07

$ x86_64-pc-cygwin-gcc /tmp/hello.c -o /tmp/hello-final.exe
$ ( cd /tmp && cp $SYSROOT/bin/cygwin1.dll . && wine /tmp/hello-final.exe )
ok thread model: posix

$ x86_64-pc-cygwin-g++ /tmp/hello.cpp -o /tmp/hellopp.exe
$ ( cd /tmp && cp $SYSROOT/lib/cygstdc++-6.dll $SYSROOT/lib/cyggcc_s-seh-1.dll . && wine /tmp/hellopp.exe )
posix iostream ok

$ x86_64-pc-cygwin-objdump -p /tmp/hello-final.exe | grep 'DLL Name'
	DLL Name: cygwin1.dll
	DLL Name: KERNEL32.dll
$ x86_64-pc-cygwin-objdump -p /tmp/hellopp.exe | grep 'DLL Name'
	DLL Name: cygwin1.dll
	DLL Name: cygstdc++-6.dll
	DLL Name: KERNEL32.dll

Notes:

  • The script's C smoke pre-stages cygwin1.dll next to the exe; the C++ smoke does not stage cygstdc++-6.dll / cyggcc_s-seh-1.dll, so the in-script wine run for C++ fails to load. The compile/link itself is fine — verified by re-running with the DLLs co-located (output above). Future iteration: have 08-gcc-final.sh copy all three DLLs.
  • Wine prints a Couldn't compute FAST_CWD pointer warning on every cygwin-runtime exec. Wine's ntdll layout doesn't match the byte sequences cygwin's find_fast_cwd_pointer() looks for in RtlGetCurrentDirectory_U. Same will be true on ReactOS until ntdll exposes a real fcwd_access_t **RtlpCurDirRef static and emits the right lea rel,%rcx / mov rel,%rbx sequence. Tracked separately for the ReactOS-side fix; doesn't block the toolchain.
  • Total build time on this host: ~10 minutes (8 cores).

Stage 09 — msys2-runtime (msys2-3.6.9 branch, side install via specs)

Goal: produce a working x86_64-pc-cygwin-gcc -specs=msys.specs hellomsys.c that links against msys-2.0.dll instead of cygwin1.dll, while keeping the plain (no -specs) cygwin baseline working. Scripts: 09-msys2-runtime.sh (runtime build) and 10-msys-specs.sh (specs file generator).

Why option B (specs-driven side install), not C (retriple to x86_64-pc-msys)

Reuse the existing x86_64-pc-cygwin gcc-15.2.0 and binutils-2.46 verbatim. Don't rebuild gcc with a new triple, don't patch gcc's spec for -lmsys-2.0, don't rebuild binutils. We build only msys2-runtime here, install it as a nested side prefix under $PREFIX/x86_64-pc-cygwin/msys/, and switch the dialect from a gcc -specs=msys.specs file. The compiler's dumpmachine still says x86_64-pc-cygwin; the choice of runtime is per-invocation. Two-runtime, one-toolchain — easier to maintain than duplicating the whole 01..08 pipeline for *-pc-msys.

Source provenance

$ cd $SRC/msys2-runtime
$ git log --oneline -1
aa532e7bb Cygwin: Fix segfault when XSAVE area sizes are unaligned
$ git status
On branch msys2-3.6.9
nothing to commit, working tree clean

The msys2-3.6.9 branch already carries the 39 MSYS2-specific patches on top of cygwin-3.6.x; we don't apply MSYS-specific patches on top.

Of stage 07's three newlib-cygwin patches:

  • fix-winsize.patch — already applied upstream in msys2-3.6.9 (skip).
  • store-tls-pointer-in-win32-tls.patch — intentionally skipped. It rewrites x86_64 _my_tls to use an explicit Win32 TLS slot for fiber/coroutine interop. MSYS2 itself doesn't ship that patch; reapplying it on top of msys2's own dcrt0/init/cygtls divergence is not safe to do unverified. Keeping plain msys2 behaviour.
  • fix-cross.patch — applied. The autoconf-version unblock + EXEEXT=.exe cross-bootstrap fixup + -B/-isystem flag adjustments are independent of the MSYS2 diff and are needed for the same reasons here (autoreconf with current autoconf, cross link path).

Configure flags (literal)

$SRC/msys2-runtime/configure \
  --build=x86_64-pc-linux-gnu \
  --target=x86_64-pc-cygwin \
  --prefix=$PREFIX/x86_64-pc-cygwin/msys \
  --disable-shared \
  --disable-doc \
  --enable-static \
  --disable-dumper \
  --with-cross-bootstrap \
  --with-msys2-runtime-commit=aa532e7bbf59a3ec1e29e13c9e8515d545cb2e16 \
  ac_cv_prog_CC=gcc

Env block (target compile flags):

CFLAGS_FOR_TARGET="-D__MSYS__ -Wl,-L$PREFIX/x86_64-pc-cygwin/lib/w32api"
CXXFLAGS_FOR_TARGET="-D__MSYS__ -Wno-error=register -Wl,-L$PREFIX/x86_64-pc-cygwin/lib/w32api"
CFLAGS="-U_FORTIFY_SOURCE -fno-stack-protector"
CXXFLAGS="-U_FORTIFY_SOURCE -fno-stack-protector"

-D__MSYS__ matters. MSYS2's standard build uses target triplet x86_64-pc-msys, where gcc's built-in spec defines __MSYS__ for free. We're cross-compiling with the unmodified x86_64-pc-cygwin gcc, so we push the macro in by hand. Without it, dll_init.cc/dcrt0.cc/cygwin.din fall back to the cygwin spelling (cygwin_dll_init / cygwin_detach_dll) and the link of new-msys-2.0.dll fails:

ld: cannot export msys_dll_init: symbol not defined
ld: libdll.a(sigfe.o):fake:(.text+0x1ec2): undefined reference to `msys_detach_dll'

--with-msys2-runtime-commit= is required: configure errors without it on a non-git tree, and the runtime's uname -r reads it. SHA verified above.

Build targets — only newlib + winsup/cygwin

make -j$JOBS all-target-newlib
( cd $BLD_DIR/x86_64-pc-cygwin/winsup/cygwin && make -j$JOBS all )
make -j1 install-target-newlib
( cd $BLD_DIR/x86_64-pc-cygwin/winsup/cygwin && make -j1 install )

We deliberately don't build winsup/cygserver, winsup/utils, or winsup/testsuite. cygserver.exe etc. link with -lcygwin (hardcoded in the unpatched cygwin gcc spec). At the msys2 build that resolves to the cygwin sysroot's libcygwin.a — which has cygwin_crt0 but not msys_crt0, and the link fails:

ld: build/.../winsup/cygwin/crt0.o:crt0.c:(.text+0x13): undefined reference to `msys_crt0'

(crt0.c was compiled with -D__MSYS__, so it calls msys_crt0; libmsys-2.0.a defines it, but the spec asks for -lcygwin not -lmsys-2.0.) MSYS2's normal build patches gcc's spec to emit -lmsys-2.0; we deliberately don't patch gcc — driving the dialect via specs is the whole point of option B. So we skip cygserver/utils. The runtime artefacts we actually need (msys-2.0.dll, libmsys-2.0.a, crt0.o, gcrt0.o, headers) all come out of newlib + winsup/cygwin.

Sub-prefix layout

$PREFIX/x86_64-pc-cygwin/msys/
  bin/msys-2.0.dll                          # the runtime
  lib/msys.specs                            # the specs file (stage 10)
  lib/libmsys-2.0.a, libmsys-2.0.dll.a      # static lib + import lib
  lib/crt0.o, gcrt0.o                       # startup
  lib/libc.a, libm.a, libpthread.a, libdl.a, libacl.a, libresolv.a, ...
  include/                                  # msys-flavored cygwin/sys/<...> headers
    cygwin/version.h, sys/cygwin.h, ...

The cygwin sysroot at $PREFIX/x86_64-pc-cygwin/{bin,lib,include} is left untouched. Plain (no -specs) x86_64-pc-cygwin-gcc hello.c continues to link against cygwin1.dll.

msys.specs (literal contents written by 10-msys-specs.sh)

# msys.specs - drive x86_64-pc-cygwin gcc-15.2.0 to link against
# msys-2.0.dll instead of cygwin1.dll. Generated by scripts/10-msys-specs.sh.
# Usage:
#   x86_64-pc-cygwin-gcc -specs=$PREFIX/x86_64-pc-cygwin/msys/lib/msys.specs hellomsys.c -o hellomsys.exe
# The compiled exe will import msys-2.0.dll, not cygwin1.dll.

*cpp:
+ -D__MSYS__

*lib:
%{pg:-lgmon} %{pthread: } -lmsys-2.0 %{mwindows:-lgdi32 -lcomdlg32} %{fvtable-verify=preinit:-lvtv -lpsapi; fvtable-verify=std:-lvtv -lpsapi} -ladvapi32 -lshell32 -luser32 -lkernel32

*self_spec:
+ -isystem $PREFIX/x86_64-pc-cygwin/msys/include -B$PREFIX/x86_64-pc-cygwin/msys/lib/

(literal $PREFIX is expanded to /home/kreijstal/git/msys-cross/prefix at write time — specs files don't expand env vars at parse time)

How it works:

  • *cpp +-prepends -D__MSYS__ to gcc's stock cpp builtins. __CYGWIN__ is still defined by the underlying cygwin spec, and __MSYS__ is now defined too — msys is a superset of cygwin so both should be visible to feature-test code.
  • *lib is a full replacement of the stock cygwin *lib with -lcygwin swapped for -lmsys-2.0. Everything else (kernel32, user32, ...) mirrors gcc-15.2's stock cygwin spec.
  • *self_spec +-appends -isystem <msys>/include -B<msys>/lib/ to every invocation. -isystem makes the msys-flavored cygwin/version.h / sys/cygwin.h win over the cygwin sysroot's same-named headers; -B makes gcc treat <msys>/lib/ as both a startfile prefix (so crt0.o / gcrt0.o come from the msys sub-prefix) and a library prefix (so -lmsys-2.0 resolves there before falling through to the cygwin sysroot).

We do not override *startfile or *cpp_unique_options directly — attempting to redefine *cpp_unique_options with a self-reference like -isystem X %(cpp_unique_options) segfaults gcc-15.2 (recursive macro expansion). -B via *self_spec achieves the same startfile redirection without that risk.

Smoke output (verbatim)

$ x86_64-pc-cygwin-gcc -specs=$PREFIX/x86_64-pc-cygwin/msys/lib/msys.specs /tmp/hellomsys.c -o /tmp/hellomsys.exe

$ x86_64-pc-cygwin-objdump -p /tmp/hellomsys.exe | grep "DLL Name"
	DLL Name: KERNEL32.dll
	DLL Name: msys-2.0.dll

$ ( cd /tmp && cp $PREFIX/x86_64-pc-cygwin/msys/bin/msys-2.0.dll . && wine /tmp/hellomsys.exe )
__MSYS__ defined
__CYGWIN__ defined
argv0=/tmp/hellomsys

argv0 comes back POSIX-translated (/tmp/hellomsys, not Z:\tmp\...) — that's the msys runtime's path-conversion layer kicking in. msys-only behaviour we wouldn't see under cygwin.

Cygwin baseline regression check (no -specs):

$ x86_64-pc-cygwin-gcc /tmp/hello.c -o /tmp/hello.exe

$ x86_64-pc-cygwin-objdump -p /tmp/hello.exe | grep "DLL Name"
	DLL Name: cygwin1.dll
	DLL Name: KERNEL32.dll

$ ( cd /tmp && cp $PREFIX/x86_64-pc-cygwin/bin/cygwin1.dll . && wine /tmp/hello.exe )
hello cygwin from /tmp/hello

Still cygwin1.dll, still working. The two dialects coexist.

The Cygwin WARNING: Couldn't compute FAST_CWD pointer warning shows on both runs and is expected under wine (already documented in stage 08).

hellomsys.c source

#include <stdio.h>
int main(int argc, char **argv) {
#ifdef __MSYS__
    printf("__MSYS__ defined\n");
#else
    printf("__MSYS__ NOT defined\n");
#endif
#ifdef __CYGWIN__
    printf("__CYGWIN__ defined\n");
#else
    printf("__CYGWIN__ NOT defined\n");
#endif
    printf("argv0=%s\n", argv[0]);
    return 0;
}

Notes / deviations

  • The runtime build had to be split into newlib + winsup/cygwin only; make all would also pull in cygserver/utils/testsuite, which fail to link without a -lmsys-2.0-aware gcc spec (see "Build targets" above).
  • 09-msys2-runtime.sh reuses an existing $BLD_DIR/config.status if present, so re-runs of the script don't re-configure from scratch on a failed-then-fixed build. Wipe $BLD_DIR manually to force a clean configure.
  • 10-msys-specs.sh is idempotent — re-running just rewrites the specs file with current $PREFIX.
  • Total wall time for stage 09 on this host: ~12 min for the runtime build (fresh build dir, 16 cores) + a few seconds for stage 10 + smoke.

Pending stages

  • 09 smoke — execute hello.exe / hellomsys.exe on a cygwin/ReactOS host (currently only validated under wine).
  • 11 (option C, optional) — proper x86_64-pc-msys retriple of the full pipeline. Not needed if option B is sufficient for the porting work at hand.

This document will be updated as each stage lands.


ReactOS NT6.2 smoke — MSYS hello world and FAST_CWD probe (2026-04-28)

Target: /home/kreijstal/git/reactos/build_nt62/livecd.iso, booted under QEMU with the MSYS payload ISO and user-mode NIC forwarding enabled:

-nic user,hostfwd=tcp:127.0.0.1:7000-:7000

Result

The canonical MSYS layout works on ReactOS NT6.2:

E:\usr\bin\hellomsys.exe
hello from msys runtime on reactos
argc=1 pid=1535

Final run summary:

FAST_CWD_WARNING=False
SUCCESS=True
FATAL=False
BAD_COMMAND=False

This confirms both the ReactOS LSASS account-domain fix and the x64 FAST_CWD compatibility path are sufficient for the simple MSYS runtime hello-world smoke.

Required payload layout

The successful ISO layout was:

E:\usr\bin\hellomsys.exe
E:\usr\bin\msys-2.0.dll

A root-level layout is expected to fail:

E:\hellomsys.exe
E:\msys-2.0.dll

Failure mode:

*** fatal error - add_item ("\??", "/", ...) failed, errno 22

Reason: MSYS computes cygheap->installation_root from the loaded runtime DLL handle (cygwin_hmodule, actually msys-2.0.dll). In init_cygheap::init_installation_root, MSYS backs up from:

...\usr\bin\msys-2.0.dll

by stripping:

msys-2.0.dll
bin
usr

If the DLL is at E:\msys-2.0.dll, backing up two MSYS layout directories collapses to bare \??, which is not a valid native root mount.

ReactOS-side fixes involved

  • dll/win32/lsasrv/policy.c

    • Ensure PolicyDnsDomainInformation and PolicyAccountDomainInformation return allocated empty strings instead of bogus NULL/garbage fields. This fixed the earlier hang in MSYS user/domain initialization.
  • dll/ntdll/def/ntdll.spec

    • On x64, export RtlGetCurrentDirectory_U through the MSYS decoy so GetProcAddress(ntdll, "RtlGetCurrentDirectory_U") exposes the byte pattern MSYS scans.
  • dll/ntdll/compat/amd64/msys2.S

    • Decoy sequence points MSYS at RtlpCurDirRef via the expected FastPebLock / RtlEnterCriticalSection / mov rel(%rip), %rbx pattern.
  • sdk/include/ndk/rtltypes.h

    • Extended RTLP_CURDIR_REF to be prefix/layout-compatible with MSYS fcwd_access_t:
LONG RefCount;
HANDLE Handle;
ULONG OldDismountCount;
UNICODE_STRING Path;
LONG FSCharacteristics;
WCHAR Buffer[2] aligned(8);
  • sdk/lib/rtl/path.c

    • RtlSetCurrentDirectory_U now allocates enough storage for the embedded FAST_CWD path buffer and populates Path, FSCharacteristics, and the buffer.
  • dll/win32/kernel32/client/loader.c

    • Defensive GetModuleFileNameW normalization strips native drive prefix \??\X:\... to X:\... for user-visible module names.
  • dll/win32/kernel32/kernel32_vista/GetFinalPathNameByHandle.c

    • DOS final-path output avoids producing malformed \\?\\??\X:\... when mountmgr/object names already contain a native DOS-device prefix.

Important debugging notes

MSYS installation-root derivation is from msys-2.0.dll, not from hellomsys.exe:

msys2-runtime/winsup/cygwin/init.cc: dll_entry
msys2-runtime/winsup/cygwin/mm/cygheap.cc: init_cygheap::init_installation_root

Accepted path forms for MSYS runtime discovery include:

C:\msys64\usr\bin\msys-2.0.dll
\\?\C:\msys64\usr\bin\msys-2.0.dll
\\server\share\msys64\usr\bin\msys-2.0.dll
\\?\UNC\server\share\msys64\usr\bin\msys-2.0.dll

Malformed/problematic forms include raw NT device paths and root-level MSYS DLL placement:

\Device\HarddiskVolume1\...\msys-2.0.dll
E:\msys-2.0.dll

Next hardening target

The current GetModuleFileNameW path normalization is a public-API boundary fix. For production-quality behavior, normalize user-visible module full names earlier in ntdll loader state:

dll/ntdll/ldr/ldrinit.c: LdrpInitializeProcess
dll/ntdll/ldr/ldrutils.c: LdrpResolveDllName / LdrpMapDll / LdrpCheckForLoadedDll

That avoids inconsistent behavior for code reading LDR_DATA_TABLE_ENTRY.FullDllName directly, loader notifications, PSAPI/debug consumers, and loader-cache comparisons.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment