Add Windows build support with clang-cl and vcpkg#450
Conversation
Two issues caused 27 test failures on Windows: 1. NATIVE_ULONG / size_t mismatch: On Windows LLP64, unsigned long is 4 bytes while size_t is 8 bytes. Use NATIVE_UINT64 instead, which is unambiguously 64-bit on all platforms. 2. NATIVE_HBOOL / bool mismatch: HDF5's hbool_t is unsigned int (4 bytes), but C++ bool is 1 byte. Reading/writing a bool* with NATIVE_HBOOL is undefined behavior. Use hbool_t intermediaries with static_cast at each serialization site.
On Windows, std::filesystem::remove() fails if the file still has an open handle — unlike Linux, which allows unlinking open files. Two tests opened an std::ifstream to verify FCIDUMP file contents but called remove() while the stream was still in scope, causing: "remove: The process cannot access the file because it is being used by another process" Fix by scoping each ifstream in a block so its destructor closes the file handle before the remove() call. Affected tests: - HamiltonianTest.SparseContainerFCIDUMP - HamiltonianTest.FCIDUMPActiveSpaceConsistency
Shell::from_json() used default initialization (`Shell sh`), leaving
the rpowers array indeterminate for non-ECP shells — the JSON round-
trip never writes rpowers for regular shells. Both BasisHasher and
BasisEqChecker read rpowers unconditionally, so comparing a database-
loaded shell (zero-initialized via `Shell sh{0}`) against a JSON-
deserialized shell was undefined behavior. Fix by value-initializing
(`Shell sh{}`), matching the pattern already used elsewhere.
Also fix BasisSetMap test which had three bugs:
- Reversed basis_json["shells"] but the actual key is "electron_shells"
(operator[] silently created an empty entry, making the reverse a no-op)
- Used Hydrogen (1 shell in STO-3G), so even reversing the correct key
would be a no-op; switch to Lithium which has 3 shells
- Test only passed on Linux by accident: uninitialized rpowers garbage
happened to make the hashes differ
Three issues prevented the Python package from building and importing on Windows when linking to a pre-installed C++ library: 1. pybind11 not found: vcpkg's find_package wrapper intercepts the search and misses pybind11 installed in pip's isolated build environment. Fix by querying `python -m pybind11 --cmakedir` and setting pybind11_DIR explicitly before find_package. 2. DLL load failure at import: Python 3.8+ no longer searches PATH for DLL dependencies of extension modules. Add a Windows-only block in __init__.py that reads the QDK_DLL_DIR environment variable and calls os.add_dll_directory() for each path before _core is imported. 3. Test script updates: use Ninja generator, clang-cl compilers, vcpkg toolchain, CMAKE_PREFIX_PATH for the pre-built C++ library, uv for venv management, and QDK_DLL_DIR for runtime DLL resolution.
In Windows the default encoding is cp1252, not UTF-8. This caused tests to fail.
NamedTemporaryFile with the default delete=True keeps an exclusive file handle on Windows, preventing C++ code from opening the same path. Use delete=False so the handle is released when the with-block exits, then clean up manually with Path.unlink() in a finally block. This pattern is needed wherever a temp file is created in Python and then passed to the C++ library for reading or writing. Affected tests: test_basis_set, test_orbitals, test_stability, test_noise_models (24 instances total). Note: when the minimum Python version is raised to 3.12+, this can be simplified using delete_on_close=False, which releases the file handle on close but auto-deletes on context manager exit — removing the need for manual unlink() calls.
The check_license_headers.py script prints a ✅ emoji on success, but Python defaults to cp1252 encoding for stdout on Windows, which cannot encode it. Reconfigure stdout to UTF-8 at the top of the script.
On Windows, spdlog::stdout_color_mt() creates a wincolor_stdout_sink that caches a Windows HANDLE via GetStdHandle(STD_OUTPUT_HANDLE) at construction time. All subsequent writes go through WriteFile(HANDLE) which bypasses the C runtime's file descriptor layer entirely. When pytest's capfd redirects fd 1 via dup2(), the cached HANDLE still points to the original stdout, so all Logger output bypasses capture — causing ~25 test failures across test_logger.py, test_constants_documentation.py, and test_scf.py. On Linux this is not a problem because stdout_color_sink_mt is aliased to ansicolor_stdout_sink, which writes via fwrite(stdout) through the C runtime's fd layer. Add a custom stdout_fd_sink for Windows that writes via fwrite(stdout), giving Windows the same fd-based write path that Linux gets by default. The sink is used only on Windows (#ifdef _WIN32); Linux/macOS continue to use stdout_color_mt() unchanged.
Windows uses cp1252 as the default locale encoding, which silently corrupts non-ASCII text in open(), read_text(), write_text(), and subprocess.run(text=True) calls. All text I/O in the package and its test suite now passes encoding="utf-8" explicitly, and subprocess invocations additionally propagate PYTHONIOENCODING=utf-8 to child processes so that both parent and child agree on the codec regardless of the system locale. A dedicated round-trip regression test verifies that Unicode content (φ, Å, ΔE, αβγ, 你好) survives file serialization and stdout capture end-to-end on Windows.
The uv-installed CPython 3.14 on Windows ships with an incomplete Tcl/Tk installation (missing tcl_findLibrary). Matplotlib defaults to the TkAgg backend, which crashes when trying to create a Tk window during circuit diagram plotting tests. Force the non-interactive Agg backend in conftest.py before any pyplot import. This is standard practice for CI/headless test environments and avoids the Tk dependency entirely.
This eliminates CRLF warnings on Windows and ensures consistent line endings across platforms.
Run each example script in a fresh TemporaryDirectory so output files (e.g. .h5, .json) don't pollute the source tree. On failure the temp directory is preserved for debugging; on success it is cleaned up.
Using more than 2 OpenMP threads causes some tests to fail (SCF not converging, ...) due to numerical instabilities. See: https://gist.github.com/lorisercole/c8081e5f4966c6bf9a2e64734c781fd3 The culprit appears to be GauXC's XC integrator, that uses element-by-element #pragma omp atomic accumulation on shared matrices (inc_by_submat_atomic in util.hpp). Under LLVM's libomp this causes non-deterministic floating-point summation order, leading to NaN/divergence in SCF with >2 threads. The issue does not manifest with GCC/libgomp due to its more conservative thread scheduling. An issue on GauXC repo will be opened. Temporary fix: disable GAUXC_ENABLE_OPENMP on MSVC while keeping OpenMP enabled for the rest of the project (MACIS, our own code).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 57 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 57 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if _sys.platform == "win32": | ||
| # Allow users / CI to point to extra DLL directories via a semicolon- | ||
| # separated environment variable (e.g. the vcpkg bin directory). | ||
| # Note: DLLs bundled next to _core.pyd are found automatically by the | ||
| # Windows loader, so no add_dll_directory() is needed for those. | ||
| _dll_dirs = _os.environ.get("QDK_DLL_DIR", "") | ||
| _dll_dir_handles = [] | ||
| for _d in _dll_dirs.split(";"): | ||
| _d = _d.strip() | ||
| if _d and _os.path.isdir(_d): | ||
| _dll_dir_handles.append(_os.add_dll_directory(_d)) # type: ignore[attr-defined] | ||
| del _dll_dirs | ||
|
|
There was a problem hiding this comment.
@idavis Is there a better way to handle this for python libraries on Windows platforms?
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 57 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…nd-of-file fixer checks
blaspp's BLASFinder.cmake never caches blaspp_defs_ (which holds -DBLAS_FORTRAN_ADD_). On reconfigure, detection is skipped (run_=false) so blaspp_defs_ is empty, and the main CMakeLists overwrites blaspp_defines with empty (CACHE INTERNAL implies FORCE), causing defines.h to lose all Fortran mangling definitions. Add a git patch that caches blaspp_defs_ at the end of detection and restores it in both cached-settings paths. Also fix lobpcgxx CMakeLists for Windows/clang-cl: - Force-include <complex> for lapackpp (MSVC config.h omits it) - Suppress all warnings from blaspp/lapackpp with -w (clang-cl)
We can save build time by not building MACIS tests if not needed (e.g. in CI pipelines).
…exer Structure has const data members, making assignment impossible; mark copy/move assignment as =delete to match the immutable design. ERIMultiplexer's default ctor is implicitly deleted (base class ERI has no default ctor); mark it =delete explicitly.
Fixes -Winconsistent-missing-override warnings from clang-cl for pure virtual redeclarations in DynamicalCorrelationCalculator, StabilityChecker, Macis, and OneBodyIntegralEngine subclasses.
2a2760c to
e4cdf81
Compare
- Replace string(APPEND) with set() for per-config flags; add RelWithDebInfo config and mirror C flags from CXX flags. - Remove the global add_compile_options(-Wno-...) block and the per-target re-enable block — dependencies handle their own warnings. - Remove MSVC-specific workarounds now handled in dependencies (_USE_MATH_DEFINES, /FI complex, BLAS_FORTRAN_ADD_, -Wno-implicit-function-declaration). - Update gauxc hash containing clang-cl fixes. - Enable MACIS_ENABLE_TESTS by default (overridable via -D).
e4cdf81 to
0b7b049
Compare
| # Install the Python extension module | ||
| install(TARGETS _core | ||
| LIBRARY DESTINATION . | ||
| COMPONENT _core | ||
| ) |
|
|
||
| # Ensure venv is activated | ||
| if (Test-Path .\venv\Scripts\activate.ps1) { | ||
| .\venv\Scripts\activate |
|
Requires:
|
Summary
First PR in a series to introduce native Windows support for QDK/Chemistry. The project now builds and passes all tests on Windows using
clang-cl(MSVC Build Tools) + vcpkg for native dependencies.Strategy
Compiler:
clang-cl— the LLVM Clang frontend with MSVC-compatible ABI. This gives us GCC/Clang C++20 feature support while linking against the MSVC runtime, which is required for Python extension modules on Windows.Dependencies: vcpkg in manifest mode (
vcpkg.json), with a custom OpenBLAS overlay port that builds LAPACK from C sources (no Fortran toolchain needed). By default, dependencies are statically linked using thex64-windows-static-mdtriplet — all library code is linked directly into_core.pyd, eliminating DLL bundling and the need to callos.add_dll_directory()at import time. A-DynamicDepsswitch is available to fall back to shared libraries if needed.Build scripts: Two PowerShell scripts under
.pipelines/pip-scripts/:windows-build-clang-cmake.ps1— separate C++ build + Python pip installwindows-build-clang-pip.ps1— single-step pip install via scikit-build-coreBoth scripts handle the full toolchain setup: VS Build Tools detection/installation, vcvarsall environment, vcpkg bootstrap, and Python pip install.
CMake flags: Per-config compile flags (
Debug,Release,RelWithDebInfo) are set explicitly for both MSVC (clang-cl / cl) and GCC/Clang toolchains. Dependencies handle their own warning suppression — no global-Wno-*overrides are applied. Coverage flags (--coverage,-fprofile-arcs) are guarded withAND NOT MSVCsinceclang-cluses MSVC-style option parsing despite reportingCMAKE_CXX_COMPILER_ID=Clang.Testing: vcpkg also provides
catch2andgtest, so C++ tests build on Windows without system-installed test frameworks. AMACIS_ENABLE_TESTSoption controls building MACIS tests (ON by default, overridable via-D).Windows-specific issues discovered and fixed
Data model differences (LLP64 vs LP64)
size_tserialization: On Windows LLP64,unsigned longis 4 bytes (vs 8 on Linux LP64).NATIVE_ULONGwas silently truncating 64-bit values. Fixed by usingNATIVE_UINT64with astatic_assert(sizeof(size_t) == 8).boolserialization:hbool_tisunsigned int(4 bytes) but C++boolis 1 byte. Readingbool*withNATIVE_HBOOLwas UB. Fixed by usinghbool_tintermediaries withstatic_cast.DLL loading (Python 3.8+)
.pyddependencies are only found in the.pyd's own directory or viaos.add_dll_directory(). PATH is not searched. See this comment._core.pyd, like on our Linux/macOS wheels._core.pydvia CMakeinstall(). An explicit list of the 7 required DLLs (openblas, hdf5, fmt, etc.) is installed — OpenMP runtime (libomp) is excluded (locally available from LLVM toolchain, handled bydelvewheelfor release wheels). TheQDK_BUNDLE_RUNTIME_DLLSoption (ON by default) auto-detects the vcpkg triplet'sVCPKG_LIBRARY_LINKAGEand can be disabled for CI pipelines usingdelvewheel repair.QDK_DLL_DIRenvironment variable is kept as an escape hatch for non-vcpkg setups.Encoding (cp1252 vs UTF-8)
encoding="utf-8". Q# circuit diagrams contain special characters that fail under cp1252.PYTHONIOENCODING=utf-8to child processes.File locking
NamedTemporaryFile(delete=False)+ manualunlink()) and 2 C++ test cases (scopingstd::ifstreamin blocks so the destructor closes the handle beforestd::filesystem::remove()).spdlog stdout capture
spdlog::stdout_color_mt()caches a Windows HANDLE at construction. When pytest'scapfdredirects fd 1 viadup2(), the cached HANDLE bypasses capture. Added a customstdout_fd_sink(in the logger module) that writes viafwrite(stdout)through the C runtime's fd layer, giving Windows the same behavior Linux gets by default.OpenMP
#pragma omp atomicaccumulation on shared matrices. Under LLVM'slibomp, this causes non-deterministic floating-point summation order, leading to NaN/divergence in SCF with >2 threads. Does not manifest with GCC/libgomp. Workaround: disableGAUXC_ENABLE_OPENMPon MSVC while keeping OpenMP enabled for the rest of the project (MACIS, our own code). See this GauXC issue.-Xclang -fopenmp+libomp.lib). Thelibomp.dllmust be present at runtime from the VS LLVM tools directory.blaspp/lapackpp on Windows
BLASFinder.cmakenever cachesblaspp_defs_(which holds-DBLAS_FORTRAN_ADD_). On CMake reconfigure, detection is skipped soblaspp_definesbecomes empty, causingdefines.hto lose all Fortran mangling definitions. Fixed via a git patch that cachesblaspp_defs_at detection time and restores it on reconfigure.config.homits#include <complex>, causing compile failures in lobpcgxx. Fixed by force-including<complex>for lapackpp targets on Windows.C++ code quality fixes (exposed by clang-cl warnings)
-Wreorder-ctor: Fixed initializer list order to match declaration order in 5 source files.-Winconsistent-missing-override: Addedoverridespecifiers to virtual function redeclarations inDynamicalCorrelationCalculator,StabilityChecker,Macis, andOneBodyIntegralEnginesubclasses.-Wdefaulted-function-deleted: Explicitly=deletecopy/move assignment inStructure(const data members) and default ctor inERIMultiplexer(base class has no default ctor).Shell::rpowers:Shell::from_json()used default initialization leavingrpowersindeterminate for non-ECP shells. Fixed by value-initializing (Shell sh{}). Also fixed theBasisSetMaptest which had wrong JSON key ("shells"instead of"electron_shells") and used a single-shell element making the test a no-op.CMake / compiler flags
_USE_MATH_DEFINESrequired forM_PIetc. under MSVC (now handled in dependencies)BLAS_FORTRAN_ADD_override for Fortran name mangling — no trailing underscore on Windows (now handled in dependencies via patched blaspp)-fPICskipped on Windows (position-independent code is the default for DLLs)Misc
.gitattributesenforcing LF line endings repo-wideAgg) for headless test environments (avoids Tcl/Tk dependency crash on Windows)uint128_tpolyfill: added==,<<,|=operators for MSVC (which lacks__uint128_t).patchfiles from trailing-whitespace and end-of-file fixerspython -m pybind11 --cmakedirand setpybind11_DIRexplicitly to avoid vcpkg'sfind_packagewrapper intercepting the searchConsiderations for future development
cl.exesupport: The codebase is ~80% ready. OpenMP 2.0 limitation with MSVC's/openmp(may need/openmp:llvm).delvewheel repair(the Windows equivalent ofauditwheel) instead of CMake DLL bundling — it resolves the full PE dependency tree automatically.encoding="utf-8"explicitly. Relying on the default locale encoding will silently break on Windows.NamedTemporaryFile(delete=False)+ manualunlink()is the pattern. Python 3.12+ offersdelete_on_close=Falseas a cleaner alternative.unsigned longis 32-bit on Windows: Never useunsigned longfor 64-bit values in portable code. Useuint64_t/size_tinstead. This affects HDF5 type mappings in particular.