Skip to content

Updates needed for PyPI wheel support (part 2 of 2): WinGW and Windows updates - #572

Draft
xylar wants to merge 17 commits into
esmf-org:developfrom
xylar:update-mingw-support
Draft

Updates needed for PyPI wheel support (part 2 of 2): WinGW and Windows updates#572
xylar wants to merge 17 commits into
esmf-org:developfrom
xylar:update-mingw-support

Conversation

@xylar

@xylar xylar commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes the ESMF build under the modern 64-bit MinGW-w64 / MSYS2 toolchain so that a self-contained, pip install-able ESMPy wheel can be produced for Windows, mirroring the Linux and macOS wheels enabled by #571. It builds on #571 and should be reviewed and merged after it: the Python loader groundwork for Windows (_ESMF_OS_WINDOWS and the .dll load branch) already landed there, and this PR supplies the C/C++/Fortran and build-system changes needed to actually compile ESMF and its bundled dependencies with MinGW and to load the resulting DLL via ctypes.

The wheel-building pipeline that consumes these changes is prototyped at https://github.com/xylar/esmpy-wheels, and the overall PyPI effort is tracked in #256.

Motivation

ESMF's existing Windows build configuration predates a modern 64-bit MSYS2/MinGW-w64 toolchain and no longer builds against it. Producing libesmf_fullylinked.dll for a wheel surfaced a series of independent issues: a strict-ISO compiler dialect that hides POSIX CRT names, LLP64 assumptions (Windows long is 32-bit), collisions with windows.h macros, MinGW's symbol-export behavior hiding the ESMC C API from the DLL export table, and MOAB/Zoltan build-rule assumptions that only hold on Unix. Each commit is a small, self-contained fix; they are grouped below by area.

Changes

Build system / toolchain (build_config/MinGW.gfortran.default/build_rules.mk)

  • Use the GNU dialect (-std=gnu++NN / -std=gnuNN) instead of strict ISO, so the CRT headers keep exposing the POSIX-named entry points (dup, dup2, open, close, ...) that ESMF's VM code uses.
  • Accept ESMF_MACHINE=x86_64 (what 64-bit MSYS2/MinGW reports for uname -m) alongside the legacy i686 label.
  • Add -Wl,--export-all-symbols to the shared-library link. Parts of the tree explicitly __declspec(dllexport) some symbols, which makes MinGW ld stop auto-exporting everything else; that would leave the undecorated extern "C" ESMC_* C API out of the DLL export table, where ctypes can't find it. Forcing all globals to be exported restores the Unix-.so-like behavior ESMPy expects.
  • Define -DNO_TIMES for C compiles too (the vendored Zoltan timer.c is C and includes sys/times.h).

VM / mpiuni

  • Include <io.h> and define STDIN/STDOUT/STDERR_FILENO on MinGW, and use dup() instead of fcntl(F_DUPFD) in setRedirects.
  • #undef the windows.h interface macro, which collides with MOAB.
  • In the mpiuni stub, cast handle arguments through intptr_t rather than long (Windows is LLP64, so long is 32-bit and would truncate 64-bit pointers/handles).

Info / Fortran-C interface (LLP64)

  • Use C_LONG_LONG for address arguments to match C long long int, and stage the default I8 kind in ESMF_KIND_I8 rather than C_LONG, since Windows long is only 32-bit.

MOAB (src/Infrastructure/Mesh/src/Moab/...)

  • Don't force -DLINUX on MinGW (it selects Unix-only headers that don't exist on Windows).
  • Prefer the ESMF-internal LAPACK names over MOAB's WIN32 branch (including in Matrix3.hpp) so the MinGW link resolves.
  • Carry the NO_TIMES define through the vendored makefiles.

Miscellaneous name collisions

  • Prefix the diff Operation enumerators in dmp_diff.hpp to avoid the Windows DELETE macro.

Relationship to other work

xylar and others added 3 commits July 18, 2026 21:16
Prepare the ESMPy loader for self-contained PyPI wheels that bundle
libesmf_fullylinked and esmf.mk inside the installed package.

- Add a package-relative guess (esmpy/_esmf/lib/esmf.mk) to esmf.mk
  discovery, tried before the conda sys.prefix locations.
- When esmf.mk is found inside the installed esmpy package (a wheel),
  resolve ESMF_LIBSDIR relative to esmf.mk instead of the absolute path
  baked in at build time, which is invalid once pip relocates the package.
- Add a Windows (nt) branch that loads libesmf_fullylinked.dll and puts
  its bundled dependencies on the DLL search path, plus a private
  _ESMF_OS_WINDOWS constant. Forward-prep for the Windows wheel phase.

Discovery and bundled-detection logic are factored into testable helpers
(_find_esmf_mk, _esmf_mk_is_bundled) in loadESMF_helpers.py with unit
tests. Behavior is unchanged for conda/source/HPC installs, where
ESMFMKFILE is set or esmf.mk lives outside the package.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A real-MPI ESMPy wheel (ESMF_COMM != mpiuni) links libesmf_fullylinked against
the C MPI runtime (libmpi). To keep the wheel from double-shipping an MPI, the
runtime is provided by a separate wheel (e.g. `mpich`) that installs libmpi into
<prefix>/lib via the wheel "data" scheme -- a directory not on the default
dynamic-loader search path. Preload it with RTLD_GLOBAL before dlopening
libesmf, discovering it via sysconfig's data path. Best-effort and gated on a
real-MPI build, so serial (mpiuni) wheels and conda/HPC/system-MPI installs
(libmpi already on the loader path) are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
esmpy/__init__.py fetched its own version/metadata via a hard-coded
importlib.metadata.metadata("esmpy"). That breaks when ESMPy is installed under
a variant distribution name that ships the same `esmpy` import package -- e.g.
`esmpy-mpich`, the MPI-enabled wheel -- raising PackageNotFoundError at import
(and the version it sets feeds loadESMF's ESMF-version check).

Fall back to resolving whichever distribution provides the `esmpy` package
(packages_distributions on 3.10+, a distributions() top_level.txt scan on
3.8/3.9) when the direct "esmpy" lookup misses. The serial `esmpy` distribution
still resolves via the direct lookup, unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
xylar and others added 14 commits July 19, 2026 08:45
Extend the loadESMF.py libmpi preload candidate list to include Open MPI's
C libmpi soname (libmpi.so.40 / libmpi.40.dylib) alongside MPICH's
libmpi.so.12, so the esmpy-openmpi wheel finds and preloads the libmpi
shipped by the PyPI `openmpi` runtime wheel. The list stays comm-agnostic:
the first present candidate is loaded; a serial or system-MPI install skips
it. Serial and esmpy-mpich behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MinGW.gfortran build_rules.mk only recognized ESMF_MACHINE=i686, a label
from the 32-bit era. On modern 64-bit MSYS2/MinGW, `uname -m` reports x86_64,
so the auto-detected ESMF_MACHINE was rejected with
"ESMF_MACHINE = x86_64 not recognized. Stop." before any compilation.

Accept x86_64 alongside i686, mapping it through the same ABISTRING logic
(ESMF_ABI selects the 32- vs 64-bit memory model), mirroring how
Linux.gfortran.default already handles x86_64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dmp_diff.hpp declared `enum Operation { EQUAL, INSERT, DELETE }`. On
MinGW/Windows, ESMCI_Util_F.C includes <windows.h>, which #defines DELETE as
a Win32 access-right constant. The preprocessor rewrote the DELETE enumerator
to `(0x00010000L)` before compilation, producing "expected identifier before
'(' token" and "invalid conversion from 'long int' to 'Operation'". A scoped
enum class would not help, since the macro expands the token regardless.

Rename EQUAL/INSERT/DELETE to OP_EQUAL/OP_INSERT/OP_DELETE (the enum is local
to this header, included only by ESMCI_Util_F.C, which does not reference the
enumerators). This removes the collision independent of include order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-std=c++NN / -std=cNN define __STRICT_ANSI__ on MinGW, which makes <io.h> and
<unistd.h> hide their POSIX-named entry points (dup, dup2, close, open, read,
write, ...). ESMF's VM code uses those names, so the strict dialect failed to
compile (e.g. "dup2 was not declared in this scope"). Override the std flags in
the MinGW build_rules.mk to -std=gnu++NN / -std=gnuNN, which keeps them visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MinGW/Windows has no fcntl() or F_DUPFD. The stdout/stderr redirect helper used
fcntl(fd, F_DUPFD, 0), which is exactly equivalent to dup(fd) (both return the
lowest available descriptor). dup() exists on all supported platforms, so switch
to it unconditionally -- no behavior change on Unix, and it builds on MinGW.
(The socket-nonblocking fcntl calls elsewhere are already ESMF_OS_MinGW-guarded.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mpiuni stub macros swallow their arguments via (void*)(long)(arg) to
suppress unused-variable warnings. On Win64 (LLP64) long is 32-bit while
pointers are 64-bit, so casting a pointer through long truncates it -- which
gfortran/g++'s -fpermissive rejects as "cast ... loses precision". Cast through
intptr_t (pointer-sized on every platform) instead, and include <stdint.h>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ESMCI_VMKernel.h included only <windows.h> on MinGW, never a header declaring
the file-descriptor calls its Redirects helper uses (dup2, close). Every TU that
pulled in the header failed with "'dup2' was not declared in this scope". Include
<io.h> (the Windows CRT home of dup/dup2/close/open) and define the POSIX
STD{IN,OUT,ERR}_FILENO constants, which Windows does not provide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bind(C) interfaces in ESMF_InfoCDef.F90 declared their address arguments
(base_address, src/dst_base_address, vmAddress) as integer(C_LONG), but the
corresponding C functions (ESMC_BaseGetInfo, ESMC_InfoCopyForAttributeReference,
ESMC_InfoBaseSync) take long long int&. On Win64 (LLP64) C_LONG is 4 bytes while
long long / pointers are 8, so gfortran rejected passing the 8-byte
ESMF_Pointer (C_SIZE_T) with "passed INTEGER(8) to INTEGER(4)". It only worked
on LP64 by coincidence. Use C_LONG_LONG, which matches C long long everywhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ESMF_InfoGetI8 copied the 8-byte ESMF_KIND_I8 `default` into a C_LONG staging
variable and passed C_LOC(local_default) to ESMC_InfoGetI8(..., long long int*
def). On Win64 C_LONG is 4 bytes, so the default was truncated and C read 8
bytes from a 4-byte buffer -- a silent data bug (it compiled fine on every
platform). Declare local_default as ESMF_KIND_I8 to match the argument and the C
long long*.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
<windows.h> pulls in the COM headers, which #define interface as struct. When an
ESMF TU includes ESMCI_VMKernel.h (-> windows.h) and later a MOAB header, MOAB's
identifiers named `interface` (e.g. moab/Interface.hpp's release_interface(IFace*
interface)) expand to `struct`, failing with "expected ',' or '...' before
'struct'" (seen building ESMCI_Grid.o). ESMF/MOAB use no COM, so #undef interface
right after the windows.h include.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MinGW build added -DNO_TIMES only to ESMF_CXXCOMPILECPPFLAGS, so C++ files
got it but C files did not. Zoltan's timer.c is C and, without NO_TIMES, includes
<sys/times.h> (and <unistd.h> for sysconf), which do not exist on MinGW --
"fatal error: sys/times.h: No such file or directory" building timer.o. Add
-DNO_TIMES to ESMF_CCOMPILECPPFLAGS as well; timer.h's own guard then skips the
POSIX includes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ESMF's MOAB makefiles hardcoded -DLINUX on every platform. It is harmless on
Linux/Darwin (dlfcn.h/dirent.h exist), but on MinGW it activates Core.cpp's
`#ifdef LINUX` block, which includes <dlfcn.h> (and <dirent.h>) -- absent on
Windows: "fatal error: dlfcn.h: No such file or directory" building Core.o.

LINUX is used in exactly one spot in MOAB (the optional dynamic-reader loader);
its WIN32 branches have POSIX #else fallbacks MinGW supports. Guard the define so
it is passed on every platform except MinGW, where MOAB then uses those
fallbacks. Applied to all seven MOAB subdir makefiles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…link)

moab/Matrix3.hpp checked `#ifdef WIN32` before ESMF_LAPACK_INTERNAL. MinGW's gcc
predefines WIN32, so on Windows MOAB bound its eigen calls to the plain LAPACK
names (dgeev_, dsyevd_) instead of the esmf_-prefixed ones. ESMF's internal
LAPACK only exposes esmf_dgeev_/esmf_dsyevd_ (DGEEV is #defined to ESMF_DGEEV via
ESMF_LapackBlas.inc), so linking ESMF_PrintInfo failed with "undefined reference
to `dgeev_'/`dsyevd_'" from OrientedBox.o.

Check ESMF_LAPACK_INTERNAL first so the esmf_-prefixed names are used on every
platform, matching the non-Windows behavior. The WIN32 external-LAPACK case is
retained for builds that are not using ESMF's internal LAPACK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The fully-linked shared library (libesmf_fullylinked.dll) loads fine under
ctypes, but symbol lookups fail (AttributeError: function 'ESMC_Initialize' not
found). Cause: several vendored components decorate symbols with
__declspec(dllexport) (yaml-cpp, MOAB/mesquite, MOAB Factory.cpp, verdict,
ESMCI_Trace, nlohmann/json). Under MinGW ld, the presence of any explicit export
disables auto-export of all other globals, so the undecorated extern "C" ESMC_*
/ ESMF C API is absent from the DLL export table. Add -Wl,--export-all-symbols to
ESMF_SL_LIBOPTS to force all globals to be exported (matching the Unix .so
behavior ESMPy's ctypes bindings rely on).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xylar
xylar force-pushed the update-mingw-support branch from d9a8950 to b1a43e7 Compare July 19, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant