Skip to content

Build Windows wheels and fix the DLL the loader picks - #131

Merged
shimwell merged 3 commits into
making-wheel-3from
windows-wheel
Aug 5, 2026
Merged

Build Windows wheels and fix the DLL the loader picks#131
shimwell merged 3 commits into
making-wheel-3from
windows-wheel

Conversation

@shimwell

@shimwell shimwell commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Now that #129 has the vanilla Windows build producing a DLL and a linked openmc.exe, this adds the packaging to turn that into an actual wheel artifact, and fixes the bug that would have made any such wheel fail on import.

The bug the pip install probe found

get_core_libraries() globs libopenmc* over lib, lib64 and bin in that order. On Windows the loadable DLL is a RUNTIME artifact in bin, but the import library is an ARCHIVE artifact named libopenmc.lib sitting in lib. It matches the same pattern, and because lib is searched first it is returned first. openmc/lib/__init__.py:21 takes element zero of that list and passes it to CDLL:

OSError: [WinError 193] %1 is not a valid Win32 application

Observed from the actual install layout:

site-packages\openmc\core\bin\libopenmc.dll   5698048   <- what should be loaded
site-packages\openmc\core\lib\libopenmc.lib  17198372   <- what was loaded

This is what the pip install probe job was for. It also shows that adding bin to the search path in 033fa82 was necessary but not sufficient, and that this had nothing to do with HDF5 bundling: the import failed identically with the HDF5 DLLs on the path, which is why that step was split in two.

The pattern is now extension-specific on Windows so the import library can never be chosen. Verified against both real layouts, with POSIX output identical before and after.

Without this, cibuildwheel's own test-command would fail on every Windows wheel, so there would be no usable artifact to publish.

A missing import, found alongside

openmc/__init__.py uses sys.platform in get_extra_libraries() but never imports sys. It only resolves because from openmc.material import * leaks the name, that module doing import sys and having no __all__. That is fragile, it would break the moment material.py gained an __all__, and the new code needs sys too, so the import is now explicit.

Packaging

  • cp312, cp313, cp314 win_amd64 added to [tool.cibuildwheel] build. Worth calling out: that table is an allowlist, so without this nothing is built for Windows regardless of what else is configured. It is the easiest piece to overlook.
  • [tool.cibuildwheel.windows], vanilla and serial. delvewheel is the auditwheel equivalent: it bundles the HDF5 DLLs into openmc.libs and injects the os.add_dll_directory calls that a plain build does not. get_extra_libraries() needs no change, since its non-darwin branch already resolves to openmc.libs, which is delvewheel's default output directory.
  • build-wheels-windows.yml, mirroring the Linux workflow, uploading a windows-wheels artifact with if-no-files-found: error so a silent no-build cannot look like success.

Serial for now: /openmp:llvm still rejects four constructs in src/, so threaded Windows is separate work. This means single-threaded transport, which is a real limitation for a Monte Carlo code and worth being explicit about, but it is what makes a first wheel shippable.

The Windows test-command deliberately omits openmc --version, because the console script written by cmake/GenerateScript.cmake is POSIX only. That remains open.

Testing

cibuildwheel already runs test-command inside the build job, but that runs on the machine that has vcpkg and the source tree. The separate test job installs the repaired wheel into a clean interpreter across 3.12, 3.13 and 3.14 with no checkout and no vcpkg, which is what actually proves delvewheel bundled HDF5 rather than the runner happening to have it. It then reads back a const flag and a namespaced global through ctypes:

assert openmc.lib.DAGMC_ENABLED is False
print(openmc.lib.settings.run_mode)
print(openmc.lib.settings.rel_max_lost_particles)

Those are precisely the two export cases that were broken before #129 and #130, so they are worth asserting rather than just importing.

Linux and macOS configuration is untouched.

Adds the packaging pieces for a vanilla Windows wheel and fixes the bug
that would have made any such wheel fail on import.

The bug: get_core_libraries globs "libopenmc*" over lib, lib64 and bin in
that order. On Windows the DLL is a RUNTIME artifact in bin but the
import library is an ARCHIVE artifact named libopenmc.lib in lib, so it
matches the same pattern and, because lib is searched first, it is
returned first. openmc/lib takes element zero of that list and hands it
to CDLL, which cannot load an import library:

    OSError: [WinError 193] %1 is not a valid Win32 application

Found by the pip install probe job, which is also what showed that
adding bin to the search path was necessary but not sufficient. The
pattern is now extension-specific on Windows so the import library can
never be selected. POSIX behaviour is unchanged, verified against both
layouts.

Also adds the import of sys that openmc/__init__.py has been missing.
sys.platform was already used in get_extra_libraries and only resolved
because "from openmc.material import *" happens to leak the name, that
module having no __all__. Relying on that is fragile and the new code
needs sys too.

Packaging:

* cp312, cp313 and cp314 win_amd64 added to [tool.cibuildwheel] build.
  That table is an allowlist, so without this nothing is built for
  Windows no matter what else is configured.
* a [tool.cibuildwheel.windows] section building vanilla and serial, with
  delvewheel as the auditwheel equivalent to bundle the HDF5 DLLs and
  inject the os.add_dll_directory calls a plain build does not.
* build-wheels-windows.yml, mirroring the Linux workflow, uploading a
  windows-wheels artifact.

The Windows test-command deliberately omits "openmc --version", since
the console script generated by cmake/GenerateScript.cmake is POSIX only.
The separate test job installs the repaired wheel into a clean
interpreter with no vcpkg and no source tree present, which is what
actually proves delvewheel bundled HDF5 rather than the machine having
it, and asserts the two export cases that were broken before #129 and
#130 read back through ctypes.
The first Windows wheel run compiled libopenmc.dll cleanly and then
failed linking the Catch2 unit tests, with 21 LNK2019 errors across
test_mesh, test_ray, test_region and test_tally. They reference
openmc::model globals directly (meshes, mesh_map, surfaces, surface_map,
filter_map, root_universe, universe_level_counts, n_coord_levels), and
importing data across a DLL boundary requires dllimport on the
declaration, which WINDOWS_EXPORT_ALL_SYMBOLS cannot supply. Being
present in the export table is not sufficient for data: n_coord_levels
is exported and still failed to link, because a plain data reference has
no thunk to go through.

OPENMC_BUILD_TESTS defaults to ON and the Windows environment did not
turn it off, unlike the probe job which has always passed
-DOPENMC_BUILD_TESTS=OFF. A wheel has no use for the C++ test binaries
anyway, so this is a fix rather than a workaround, and it drops several
minutes of Catch2 compilation from every wheel build.

Building the C++ tests on Windows would need those model globals
annotated with OPENMC_API in the same way as #129 did for the four
export cases main.cpp needed. That is separate work and is not required
for a wheel.
The check step reported success while one of its commands was raising:

    AttributeError: module 'openmc.lib' has no attribute 'DAGMC_ENABLED'

Two separate mistakes. The flags are not module attributes, they are read
through accessor functions (openmc.lib._dagmc_enabled and friends),
which is how openmc's own test suite does it, see tests/conftest.py. And
the step ran three "python -c" calls under pwsh, where only the last
native command's exit code is inspected, so the first two failing was
invisible and the step still passed.

Now a single python process under bash, which GitHub invokes with -e, so
anything raising fails the step. Explicit $LASTEXITCODE guards added to
the pwsh install step for the same reason.

While rewriting, the assertions were made worth having:

* the loaded path must end in .dll, which is the regression that the
  extension-specific search pattern fixes. Asserting the import
  succeeded is not enough, since loading the import library is exactly
  what used to happen and it fails at CDLL rather than silently.
* all three const bool flags are checked, not just DAGMC_ENABLED. These
  are the read-only data case that WINDOWS_EXPORT_ALL_SYMBOLS drops.
* rel_max_lost_particles is checked against its 1.0e-6 default from
  src/settings.cpp rather than merely for the absence of an exception, so
  a symbol resolving to the wrong storage would be caught.

run_mode is printed but not asserted on. It reads back as None rather
than a string because the library is uninitialised, so RunMode::UNSET is
absent from openmc.lib's mapping. Resolving without raising is the point;
before the extern "C" fix it was a ValueError about an undefined symbol.
@shimwell
shimwell merged commit 2e385a6 into making-wheel-3 Aug 5, 2026
5 checks passed
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