From ac142f64bfaee16f27811fb22bd5935ce7712e53 Mon Sep 17 00:00:00 2001 From: Yorick Reum Date: Fri, 11 Sep 2026 13:22:35 +0200 Subject: [PATCH 1/2] Fix portability defects that prevent a native Windows build Apart from a colon in an epsilon-input-file path, none are reachable on Linux or macOS, so they have gone unnoticed; four are latent bugs rather than build breakage. Source: * output_directory.cpp: Windows mkdir() takes one argument, and its remove() fails on directories, so nftw() aborted and delete_directory() removed nothing at all. * meepgeom.cpp: epsilon-input-file was split at its last ':', so "C:\dir\eps.h5" became the filename "C" and the failed H5Fopen aborted. It is now split after a ".h5:" or ".hdf5:" extension, which also keeps colons in Unix paths part of the filename. * mympi.cpp: _GNU_SOURCE came after , too late to declare vasprintf() on mingw-w64. * tests/h5test.cpp: POSIX sync() does not exist there, breaking the C++ test suite's compile. Build: * Windows DLLs may not contain undefined symbols; without -no-undefined libtool silently emits a static archive. * PYTHON_LIBS was referenced but never set. Empty is right on Unix, where modules take their symbols from the interpreter; on Windows they must link the Python library. Module suffixes (.so vs .pyd) are chosen per host too. * _mpb linked only libpympb, relying on transitive resolution Windows does not permit. Both modules use -avoid-version now, as no platform imports a versioned extension module. * sphere-quad.h's rule ignored $(EXEEXT). * TESTS_ENVIRONMENT built PYTHONPATH from $(abs_top_builddir): an MSYS "/c/..." path joined with ":", neither readable by native Python. * test_mpb.py passed a path as an re.sub() replacement, where backslashes are escapes. A native build also needs lt_cv_deplibs_check_method=pass_all; libctl, harminv, mpb and libGDSII need their own fixes. --- configure.ac | 37 +++++++++++++++++++++++++++++++++++++ libpympb/Makefile.am | 2 +- python/Makefile.am | 13 +++++++------ python/tests/test_mpb.py | 29 ++++++++++++++--------------- src/Makefile.am | 6 +++--- src/meepgeom.cpp | 10 ++++++++-- src/mympi.cpp | 11 ++++++++--- src/output_directory.cpp | 22 +++++++++++++++++++--- tests/h5test.cpp | 8 ++++++++ 9 files changed, 105 insertions(+), 33 deletions(-) diff --git a/configure.ac b/configure.ac index dfbbeffb4..f03ed8174 100644 --- a/configure.ac +++ b/configure.ac @@ -3,6 +3,7 @@ AC_INIT([meep],[m4_esyscmd(./version.sh 1.35.0-beta)]) AC_CONFIG_SRCDIR(src/step.cpp) AC_CONFIG_AUX_DIR([build-aux]) +AC_CANONICAL_HOST # Shared-library version number; indicates api compatibility, and is # not the same as the "public" version number. (Don't worry about this @@ -612,6 +613,42 @@ else fi # enabled_shared fi # with_python +# Windows needs three adjustments for the Python interface: +# +# * every symbol in a Windows DLL must resolve at link time, so the extension +# modules have to link against the Python library. On Unix they must not, +# the symbols coming from the interpreter that dlopen()s them. +# * libtool builds a ".dll" there and a ".so" elsewhere, while Python imports +# ".pyd" on Windows and ".so" elsewhere. +# * the interpreter running the test suite is a native Windows one: it cannot +# read MSYS-style "/c/..." paths, and separates path lists with ";". +PYTHON_LIBS="" +PYMODULE_BUILT_EXT=".so" +PYMODULE_EXT=".so" +PYPATH_CONV="echo" +PYPATH_SEP=":" +case $host_os in + cygwin* | mingw* | msys*) + PYMODULE_BUILT_EXT=".dll" + PYMODULE_EXT=".pyd" + PYPATH_CONV="cygpath -m" + PYPATH_SEP=";" + if test x"$have_python" = xyes; then + AC_MSG_CHECKING([for Python library to link extension modules against]) + pyver=`$PYTHON -c "import sys; print('%d.%d' % sys.version_info[[:2]])" 2>/dev/null` + pylibdir=`$PYTHON -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR') or '')" 2>/dev/null` + test -n "$pylibdir" && PYTHON_LIBS="-L$pylibdir" + test -n "$pyver" && PYTHON_LIBS="$PYTHON_LIBS -lpython$pyver" + AC_MSG_RESULT([${PYTHON_LIBS:-none}]) + fi + ;; +esac +AC_SUBST(PYTHON_LIBS) +AC_SUBST(PYMODULE_BUILT_EXT) +AC_SUBST(PYMODULE_EXT) +AC_SUBST(PYPATH_CONV) +AC_SUBST(PYPATH_SEP) + AC_SUBST(PYTHON_INCLUDES) AM_CONDITIONAL(WITH_PYTHON, test x"$have_python" = "xyes") AM_CONDITIONAL(WITH_COVERAGE, test x"$with_coverage" = "xyes") diff --git a/libpympb/Makefile.am b/libpympb/Makefile.am index 6e579a96b..179649e7a 100644 --- a/libpympb/Makefile.am +++ b/libpympb/Makefile.am @@ -10,5 +10,5 @@ pkginclude_HEADERS = pympb.hpp libpympb_la_SOURCES = pympb.cpp pympb.hpp libpympb_la_LIBADD = $(LIBMEEP) -libpympb_la_LDFLAGS = -version-info @SHARED_VERSION_INFO@ +libpympb_la_LDFLAGS = -no-undefined -version-info @SHARED_VERSION_INFO@ libpympb_la_CPPFLAGS = $(AM_CPPFLAGS) diff --git a/python/Makefile.am b/python/Makefile.am index e44fa6216..151e5a892 100644 --- a/python/Makefile.am +++ b/python/Makefile.am @@ -109,7 +109,7 @@ else endif TEST_EXTENSIONS = .py -TESTS_ENVIRONMENT = export PYTHONPATH=$(abs_top_builddir)/python:$$PYTHONPATH; +TESTS_ENVIRONMENT = export PYTHONPATH="`$(PYPATH_CONV) $(abs_top_builddir)/python`$(PYPATH_SEP)$$PYTHONPATH"; ###################################################################### # instructions for building the _meep binary library that implements @@ -130,7 +130,7 @@ SWIG_WARN_FLAGS = -Wno-unused-function -Wno-unused-parameter -Wno-missing-field- _meep_la_SOURCES = meep-python.cxx _meep_la_LIBADD = $(LIBMEEP) $(PYTHON_LIBS) -_meep_la_LDFLAGS = -module -version-info @SHARED_VERSION_INFO@ +_meep_la_LDFLAGS = -module -no-undefined -avoid-version _meep_la_CPPFLAGS = $(PYTHON_INCLUDES) $(AM_CPPFLAGS) $(SWIG_WARN_FLAGS) _meep_la_CXXFLAGS = -O1 @@ -148,8 +148,8 @@ if WITH_MPB pympb_PYTHON = mpb.py solver.py mpb_data.py pympb_LTLIBRARIES = _mpb.la _mpb_la_SOURCES = mpb-python.cxx - _mpb_la_LIBADD = $(PYTHON_LIBS) $(top_builddir)/libpympb/libpympb.la - _mpb_la_LDFLAGS = -module -version-info @SHARED_VERSION_INFO@ + _mpb_la_LIBADD = $(PYTHON_LIBS) $(top_builddir)/libpympb/libpympb.la $(LIBMEEP) + _mpb_la_LDFLAGS = -module -no-undefined -avoid-version _mpb_la_CPPFLAGS = $(PYTHON_INCLUDES) $(AM_CPPFLAGS) $(SWIG_WARN_FLAGS) _mpb_la_CXXFLAGS = -O1 @@ -250,15 +250,16 @@ adjoint_PYTHON = $(srcdir)/adjoint/__init__.py \ $(srcdir)/adjoint/wrapper.py \ $(srcdir)/adjoint/utils.py -PY_PKG_FILES = $(INIT_PY) $(HL_IFACE) .libs/_meep.so +PY_PKG_FILES = $(INIT_PY) $(HL_IFACE) meep: _meep.la $(MPB_LA) __init__.py $(HL_IFACE) mkdir -p meep/adjoint cp $(PY_PKG_FILES) meep + cp .libs/_meep$(PYMODULE_BUILT_EXT) meep/_meep$(PYMODULE_EXT) cp $(adjoint_PYTHON) meep/adjoint if WITH_MPB mkdir -p meep/mpb - cp .libs/_mpb.so meep/mpb + cp .libs/_mpb$(PYMODULE_BUILT_EXT) meep/mpb/_mpb$(PYMODULE_EXT) cp $(MPB_PY_PATH) meep/mpb/__init__.py cp $(srcdir)/solver.py $(srcdir)/mpb_data.py meep/mpb endif diff --git a/python/tests/test_mpb.py b/python/tests/test_mpb.py index 915d8d2c6..9d480ddb9 100644 --- a/python/tests/test_mpb.py +++ b/python/tests/test_mpb.py @@ -1,6 +1,5 @@ import math import os -import re import sys import time import unittest @@ -586,7 +585,7 @@ def test_output_efield_z(self): ref_fname = "tutorial-e.k16.b08.z.tm.h5" ref_path = os.path.join(self.data_dir, ref_fname) - res_path = re.sub("tutorial", ms.filename_prefix, ref_fname) + res_path = ref_fname.replace("tutorial", ms.filename_prefix) self.compare_h5_files(ref_path, res_path) def test_output_dpwr_in_objects(self): @@ -599,8 +598,8 @@ def test_output_dpwr_in_objects(self): ref_path1 = os.path.join(self.data_dir, ref_fname1) ref_path2 = os.path.join(self.data_dir, ref_fname2) - res_path1 = re.sub("tutorial", ms.filename_prefix, ref_fname1) - res_path2 = re.sub("tutorial", ms.filename_prefix, ref_fname2) + res_path1 = ref_fname1.replace("tutorial", ms.filename_prefix) + res_path2 = ref_fname2.replace("tutorial", ms.filename_prefix) self.compare_h5_files(ref_path1, res_path1) self.compare_h5_files(ref_path2, res_path2) @@ -807,7 +806,7 @@ def test_output_charge_density(self): ref_fn = "tutorial-C.k16.b08.te.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("tutorial", ms.filename_prefix, ref_fn) + res_path = ref_fn.replace("tutorial", ms.filename_prefix) self.compare_h5_files(ref_path, res_path) def test_bragg_sine(self): @@ -867,7 +866,7 @@ def test_bragg(self): ref_fn = "bragg-h.k01.b08.y.tm.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("bragg", self.filename_prefix, ref_fn) + res_path = ref_fn.replace("bragg", self.filename_prefix) self.compare_h5_files(ref_path, res_path) @@ -921,7 +920,7 @@ def get_dpwr(ms, band): ref_fn = "diamond-dpwr.k06.b05.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("diamond", self.filename_prefix, ref_fn) + res_path = ref_fn.replace("diamond", self.filename_prefix) self.compare_h5_files(ref_path, res_path) # Test MPBData.convert() @@ -949,7 +948,7 @@ def test_hole_slab(self): ref_fn = "hole-slab-h.k01.b09.z.zeven.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("hole-slab", self.filename_prefix, ref_fn) + res_path = ref_fn.replace("hole-slab", self.filename_prefix) ms.display_eigensolver_stats() self.compare_h5_files(ref_path, res_path) @@ -1084,7 +1083,7 @@ def test_line_defect(self): ref_fn = "line-defect-e.k04.b12.z.tm.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("line-defect", self.filename_prefix, ref_fn) + res_path = ref_fn.replace("line-defect", self.filename_prefix) self.compare_h5_files(ref_path, res_path) def test_sq_rods(self): @@ -1241,7 +1240,7 @@ def test_strip(self): ref_fn = "strip-flux.v.k01.b04.x.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("strip", self.filename_prefix, ref_fn) + res_path = ref_fn.replace("strip", self.filename_prefix) self.compare_h5_files(ref_path, res_path) @@ -1381,7 +1380,7 @@ def test_tri_rods(self): ref_fn = "tri-rods-e.k11.b08.z.tm.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("tri-rods", self.filename_prefix, ref_fn) + res_path = ref_fn.replace("tri-rods", self.filename_prefix) self.compare_h5_files(ref_path, res_path) @@ -1533,7 +1532,7 @@ def test_subpixel_averaging(self): ref_fn = "subpixel_avg-epsilon.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("subpixel_avg", self.filename_prefix, ref_fn) + res_path = ref_fn.replace("subpixel_avg", self.filename_prefix) self.compare_h5_files(ref_path, res_path) self.check_band_range_data(expected_brd, ms.band_range_data) @@ -1587,7 +1586,7 @@ def test_run_te_with_mu_material(self): fname = "tutorial-mu.h5" data_path = os.path.join(self.data_dir, fname) - res_path = re.sub("tutorial", self.filename_prefix, fname) + res_path = fname.replace("tutorial", self.filename_prefix) self.compare_h5_files(data_path, res_path) mu = ms.get_mu() @@ -1602,7 +1601,7 @@ def test_output_tot_pwr(self): ref_fname = "tutorial-tot.rpwr.k16.b08.te.h5" ref_path = os.path.join(self.data_dir, ref_fname) - res_path = re.sub("tutorial", self.filename_prefix, ref_fname) + res_path = ref_fname.replace("tutorial", self.filename_prefix) self.compare_h5_files(ref_path, res_path) @@ -2100,7 +2099,7 @@ def test_poynting(self): ref_fn = "tutorial-flux.v.k11.b08.te.h5" ref_path = os.path.join(self.data_dir, ref_fn) - res_path = re.sub("tutorial", self.filename_prefix, ref_fn) + res_path = ref_fn.replace("tutorial", self.filename_prefix) self.compare_h5_files(ref_path, res_path) diff --git a/src/Makefile.am b/src/Makefile.am index 8b545b333..3127d1ff7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -22,15 +22,15 @@ vec.cpp step_generic.cpp meepgeom.cpp GDSIIgeom.cpp $(HDRS) $(BUILT_SOURCES) SUBDIRS = support libmeep_la_LIBADD = support/libsupport.la -libmeep_la_LDFLAGS = -version-info @SHARED_VERSION_INFO@ +libmeep_la_LDFLAGS = -no-undefined -version-info @SHARED_VERSION_INFO@ PRELUDE = "/* This file was automatically generated --- DO NOT EDIT */" noinst_PROGRAMS = sphere_quad sphere_quad_SOURCES = sphere-quad.cpp -SPHERE_QUAD = $(top_builddir)/src/sphere_quad +SPHERE_QUAD = $(top_builddir)/src/sphere_quad$(EXEEXT) sphere-quad.h: - make sphere_quad + $(MAKE) $(AM_MAKEFLAGS) sphere_quad$(EXEEXT) (echo $(PRELUDE); echo; $(SPHERE_QUAD)) > $@ step_generic_stride1.cpp: step_generic.cpp diff --git a/src/meepgeom.cpp b/src/meepgeom.cpp index 3bd92f875..7f8043718 100644 --- a/src/meepgeom.cpp +++ b/src/meepgeom.cpp @@ -2080,8 +2080,14 @@ material_type make_file_material(const char *eps_input_file) { if (eps_input_file && eps_input_file[0]) { // file specified char *fname = new char[strlen(eps_input_file) + 1]; strcpy(fname, eps_input_file); - // parse epsilon-input-file as "fname.h5:dataname" - char *dataname = strrchr(fname, ':'); + // parse epsilon-input-file as "fname.h5:dataname" (or "fname.hdf5:dataname"). + // Splitting after the extension, not at any colon, keeps colons in the path + // (e.g. a Windows drive letter, "C:\dir\eps.h5") part of the filename. + char *dataname = NULL; + for (const char *ext : {".h5:", ".hdf5:"}) { + char *sep = strstr(fname, ext); + if (sep && (!dataname || sep < dataname)) dataname = sep + strlen(ext) - 1; + } if (dataname) *(dataname++) = 0; meep::h5file eps_file(fname, meep::h5file::READONLY, false); int rank; // ignored since rank < 3 is equivalent to singleton dims diff --git a/src/mympi.cpp b/src/mympi.cpp index 2487d7ebb..605a22c98 100644 --- a/src/mympi.cpp +++ b/src/mympi.cpp @@ -15,6 +15,14 @@ % Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +/* vasprintf() (used by meep::abort below) is a GNU extension. On some + platforms, notably mingw-w64, it is only declared when _GNU_SOURCE is + defined *before* any system header is included, so define it here rather + than further down alongside the include. */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif + #include #include #include @@ -37,9 +45,6 @@ #endif #if defined(DEBUG) && defined(HAVE_FEENABLEEXCEPT) -#ifndef _GNU_SOURCE -#define _GNU_SOURCE 1 -#endif #include #if !HAVE_DECL_FEENABLEEXCEPT extern "C" int feenableexcept(int EXCEPTS); diff --git a/src/output_directory.cpp b/src/output_directory.cpp index 2ad2cd0eb..08695fb54 100644 --- a/src/output_directory.cpp +++ b/src/output_directory.cpp @@ -25,6 +25,18 @@ #include #include +#ifdef _WIN32 +#include +/* Windows has a one-argument mkdir() (permissions come from the ACL) and its + remove() only unlinks files, whereas POSIX remove() also removes empty + directories. */ +#define MEEP_MKDIR(d) _mkdir(d) +#define MEEP_RMDIR(d) _rmdir(d) +#else +#define MEEP_MKDIR(d) mkdir((d), 00777) +#define MEEP_RMDIR(d) rmdir(d) +#endif + #include "meep.hpp" using namespace std; @@ -68,7 +80,7 @@ static bool is_ok_dir(const char *dirname) { if (direxists) closedir(dir); else - mkdir(dirname, 00777); + MEEP_MKDIR(dirname); } direxists = broadcast(0, direxists); return !direxists; @@ -156,13 +168,17 @@ char *make_output_directory() { } void trash_output_directory(const char *dirname) { - if (am_master()) mkdir(dirname, 00777); + if (am_master()) MEEP_MKDIR(dirname); } static int rmpath(const char *path, const struct stat *s, int t, struct FTW *ftw) { (void)s; - (void)t; (void)ftw; // unused + // POSIX remove() unlinks files and removes empty directories, but on Windows + // it only unlinks files and fails with EACCES on a directory, so dispatch on + // the entry type explicitly. (We pass FTW_DEPTH, so directories arrive as + // FTW_DP, after their contents.) + if (t == FTW_D || t == FTW_DP || t == FTW_DNR) return MEEP_RMDIR(path); return remove(path); } diff --git a/tests/h5test.cpp b/tests/h5test.cpp index f38c95a60..8169b3817 100644 --- a/tests/h5test.cpp +++ b/tests/h5test.cpp @@ -9,6 +9,14 @@ #include #include "meep_internals.hpp" #include "config.h" + +#ifdef _WIN32 +/* POSIX sync() has no Windows equivalent; the HDF5 file is closed before it is + reopened below, so flushing the C streams is sufficient here. */ +#include +#define sync() _flushall() +#endif + using namespace meep; using std::complex; using std::max; From ff31889130785c245d0bbe05e52d814d330620e8 Mon Sep 17 00:00:00 2001 From: Yorick Reum Date: Fri, 11 Sep 2026 13:22:35 +0200 Subject: [PATCH 2/2] Add a native Windows CI job (mingw-w64 UCRT64) Builds and tests Meep on windows-latest via MSYS2. No dependency needs patching: harminv is built static, MPB already carries -no-undefined, and the one libctl omits is supplied on the make command line. The reasoning behind the less obvious choices -- UCRT64 over MINGW64, pass_all, libctl from a release tarball, and vendoring the runtime DLLs beside _meep.pyd -- is in comments in the workflow. --- .github/workflows/build-ci-windows.yml | 199 +++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 .github/workflows/build-ci-windows.yml diff --git a/.github/workflows/build-ci-windows.yml b/.github/workflows/build-ci-windows.yml new file mode 100644 index 000000000..920745a30 --- /dev/null +++ b/.github/workflows/build-ci-windows.yml @@ -0,0 +1,199 @@ +name: run tests (windows) + +on: + pull_request: + push: + schedule: + - cron: 0 2 * * * # run at 2 AM UTC + workflow_dispatch: + +# Native Windows build via MSYS2/mingw-w64. Notes on the choices here: +# +# * UCRT64, not MINGW64: CPython 3.5+ links ucrtbase.dll, and mixing CRTs +# across a DLL boundary corrupts FILE*, errno and malloc/free. +# +# * lt_cv_deplibs_check_method=pass_all is required. Without it libtool goes +# looking for a *shared* -lmingw32, fails to find one, and silently +# downgrades every DLL to a static archive -- with make still exiting 0. +# +# * libctl comes from a release tarball rather than git. Its libctlgeom +# sources include geom-ctl-io.c, which a git checkout generates with the +# Guile-based gen-ctl-io; there is no native-Windows Guile, and the tarball +# ships that file pre-generated. (Never run `make distclean` in the +# extracted tarball -- libctl's clean-local deletes geom-ctl-io.c.) +# +# * The Scheme interface is not built (--without-scheme) for the same reason. +# +# * libGDSII is omitted: it uses strcasestr, a GNU extension mingw-w64 does +# not provide, so GDSII import is unavailable here. + +env: + # Pinned so a libctl release cannot change the build underneath us. + LIBCTL_VERSION: 4.7.1 + +jobs: + build: + name: "native windows (mingw-w64 UCRT64)" + runs-on: windows-latest + + defaults: + run: + shell: 'msys2 {0}' + + steps: + - name: Set up MSYS2 (UCRT64) + uses: msys2/setup-msys2@v2 + with: + msystem: UCRT64 + update: true + release: false + install: >- + make + patch + m4 + curl + tar + autoconf-wrapper + autoconf2.72 + automake-wrapper + automake1.18 + libtool + mingw-w64-ucrt-x86_64-gcc + mingw-w64-ucrt-x86_64-gcc-fortran + mingw-w64-ucrt-x86_64-pkgconf + mingw-w64-ucrt-x86_64-hdf5 + mingw-w64-ucrt-x86_64-gsl + mingw-w64-ucrt-x86_64-fftw + mingw-w64-ucrt-x86_64-openblas + mingw-w64-ucrt-x86_64-swig + mingw-w64-ucrt-x86_64-python + mingw-w64-ucrt-x86_64-python-numpy + mingw-w64-ucrt-x86_64-python-scipy + mingw-w64-ucrt-x86_64-python-matplotlib + mingw-w64-ucrt-x86_64-python-h5py + mingw-w64-ucrt-x86_64-python-pip + mingw-w64-ucrt-x86_64-python-parameterized + mingw-w64-ucrt-x86_64-python-pytest + mingw-w64-ucrt-x86_64-python-trimesh + + - name: Checkout Meep repository + uses: actions/checkout@v7 + with: + path: meep-src + + # Everything else in python/requirements.txt is installed above as a native + # mingw package. autograd is not packaged but is pure Python. jax/jaxlib + # are deliberately absent: they publish no Windows wheels, and the only + # tests needing them live in ADJOINT_TESTS, which "make check" does not run. + - name: Install remaining Python test dependencies + run: python -m pip install --break-system-packages autograd + + - name: Define common environment variables + run: | + echo "PREFIX=$(pwd)/local" >> $GITHUB_ENV + echo "CPPFLAGS=-I$(pwd)/local/include" >> $GITHUB_ENV + echo "LDFLAGS=-L$(pwd)/local/lib" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=$(pwd)/local/lib/pkgconfig:${MINGW_PREFIX}/lib/pkgconfig" >> $GITHUB_ENV + + - name: Checkout harminv repository + uses: actions/checkout@v7 + with: + repository: NanoComp/harminv + path: harminv-src + + - name: Checkout MPB repository + uses: actions/checkout@v7 + with: + repository: NanoComp/mpb + path: mpb-src + + - name: Record dependency revisions for the cache key + id: deps + run: | + echo "sha=$(cat harminv-src/.git/HEAD mpb-src/.git/HEAD | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + + - name: Cache dependency builds + uses: actions/cache@v6 + id: deps-cache + with: + path: local + key: deps-windows-ucrt64-libctl${{ env.LIBCTL_VERSION }}-${{ steps.deps.outputs.sha }}-${{ hashFiles('meep-src/.github/workflows/build-ci-windows.yml') }} + + # harminv is built static on purpose: only libmeep links it, so it needs no + # DLL, and its configure.ac defaults shared libraries off anyway. + - name: Build and install harminv + if: steps.deps-cache.outputs.cache-hit != 'true' + run: | + cd harminv-src + sh autogen.sh --prefix=${PREFIX} --disable-shared --enable-static \ + --with-blas=openblas --with-lapack=openblas + make -j $(nproc) && make install + + # libctlgeom must be a DLL (libpympb and libmeep both use it; linking the + # static archive into both produces duplicate-symbol errors). Upstream + # libctl omits -no-undefined from libctlgeom_la_LDFLAGS, so supply it on + # the make command line rather than patching the tree. + - name: Build and install libctl + if: steps.deps-cache.outputs.cache-hit != 'true' + run: | + curl -fsSL -o libctl.tar.gz \ + "https://github.com/NanoComp/libctl/releases/download/v${LIBCTL_VERSION}/libctl-${LIBCTL_VERSION}.tar.gz" + tar xzf libctl.tar.gz + cd libctl-${LIBCTL_VERSION} + ./configure --prefix=${PREFIX} --without-guile --without-python \ + --enable-shared --disable-static lt_cv_deplibs_check_method=pass_all + make -j $(nproc) libctlgeom_la_LDFLAGS="-no-undefined -avoid-version" + make install libctlgeom_la_LDFLAGS="-no-undefined -avoid-version" + + # MPB already carries -no-undefined, so it needs no override. + - name: Build and install MPB + if: steps.deps-cache.outputs.cache-hit != 'true' + run: | + cd mpb-src + sh autogen.sh --prefix=${PREFIX} --without-libctl --without-hdf5 \ + --enable-shared --disable-static lt_cv_deplibs_check_method=pass_all + make -j $(nproc) && make install + + - name: Run configure + run: | + cd meep-src + sh autogen.sh --prefix=${PREFIX} --without-scheme \ + --enable-shared --disable-static lt_cv_deplibs_check_method=pass_all + + - name: Run make + run: cd meep-src && make -j $(nproc) + + # Windows resolves a DLL's dependencies from the directory of the module + # that needs them, and Python 3.8+ ignores PATH for extension modules. + # Placing the runtime DLLs beside _meep.pyd is what a Windows wheel does, + # and it lets the test suite run with no environment fixup at all. + - name: Vendor runtime DLLs beside the extension modules + run: | + cd meep-src + for d in python/meep python/meep/mpb; do + test -d "$d" || continue + cp src/.libs/*.dll libpympb/.libs/*.dll ${PREFIX}/bin/*.dll "$d"/ + done + + - name: Run make check (C++) + run: cd meep-src/tests && make check + + - name: Run make check (Python) + env: + MPLBACKEND: Agg + PYTHONUTF8: 1 # tests print "μm"; cp1252 cannot encode it + run: cd meep-src/python && make check + + - name: Archive C++ test logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: cpp-tests-windows-log + path: ${{ github.workspace }}/meep-src/tests/test-suite.log + + - name: Archive Python test logs + if: failure() + uses: actions/upload-artifact@v7 + with: + name: python-tests-windows-log + path: ${{ github.workspace }}/meep-src/python/test-suite.log