diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a8792f4..e5ef3b1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,10 +4,30 @@ on: [push, pull_request] jobs: test: + name: test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, macos-latest] - runs-on: ${{ matrix.os }} + include: + - os: ubuntu-latest + - os: macos-latest + - os: windows-latest + shell: msys2 {0} + defaults: + run: + shell: ${{ matrix.shell || 'bash -e {0}' }} steps: + - name: Keep LF line endings + if: runner.os == 'Windows' + shell: pwsh + run: git config --global core.autocrlf false - uses: actions/checkout@v7 + - uses: msys2/setup-msys2@v2 + if: runner.os == 'Windows' + with: + msystem: CLANG64 + path-type: inherit # uv and Git for Windows stay visible + install: mingw-w64-clang-x86_64-clang mingw-w64-clang-x86_64-dlfcn # clang for scons, dlopen for the filters + - uses: astral-sh/setup-uv@v5 + if: runner.os == 'Windows' - run: ./test.sh diff --git a/SConstruct b/SConstruct index 7b8fa4e..c7805d9 100644 --- a/SConstruct +++ b/SConstruct @@ -1,10 +1,12 @@ import os import platform import subprocess +import sys import sysconfig import numpy as np import eigen +WINDOWS = platform.system() == "Windows" arch = subprocess.check_output(["uname", "-m"], encoding='utf8').rstrip() common = '' @@ -38,7 +40,9 @@ env = Environment( CXXFLAGS="-std=c++1z", CPPPATH=cpppath, REDNOSE_ROOT=Dir("#").abspath, - tools=["default", "cython", "rednose_filter"], + tools=["mingw" if WINDOWS else "default", "cython", "rednose_filter"], # the default tool picks MSVC on Windows + # the mingw tool assumes gcc and drops the lib prefix ekf_load expects; static libc++ so the DLLs load outside the MSYS2 shell + **({"CC": "clang", "CXX": "clang++", "SHLIBPREFIX": "lib", "LINKFLAGS": ["-static"]} if WINDOWS else {}), ) # Cython build enviroment @@ -48,6 +52,14 @@ envCython["CCFLAGS"] += ["-Wno-#warnings", "-Wno-cpp", "-Wno-shadow", "-Wno-depr envCython["LIBS"] = [] if platform.system() == "Darwin": envCython["LINKFLAGS"] = ["-bundle", "-undefined", "dynamic_lookup"] +elif WINDOWS: + envCython["LINKFLAGS"] = ["-shared", "-static"] + envCython["LIBPATH"] = [os.path.join(sys.base_prefix, "libs")] + envCython["LIBS"] = [f"python{sys.version_info.major}{sys.version_info.minor}"] + # extension modules are .pyd on Windows; the SConscript names them .so + def _pyd_emitter(target, source, env): + return [env.File(str(t)[:-3] + ".pyd") if str(t).endswith(".so") else t for t in target], source + envCython.Append(PROGEMITTER=[_pyd_emitter]) elif arch == "aarch64": envCython["LINKFLAGS"] = ["-shared"] envCython["LIBS"] = [os.path.basename(python_path)] diff --git a/rednose/helpers/__init__.py b/rednose/helpers/__init__.py index 3acc14a..fbc070a 100644 --- a/rednose/helpers/__init__.py +++ b/rednose/helpers/__init__.py @@ -16,7 +16,7 @@ def write_code(folder, name, code, header): def load_code(folder, name): - shared_ext = "dylib" if platform.system() == "Darwin" else "so" + shared_ext = {"Darwin": "dylib", "Windows": "dll"}.get(platform.system(), "so") shared_fn = os.path.join(folder, f"lib{name}.{shared_ext}") header_fn = os.path.join(folder, f"{name}.h") diff --git a/site_scons/site_tools/rednose_filter.py b/site_scons/site_tools/rednose_filter.py index a3f8a9a..7ca393d 100644 --- a/site_scons/site_tools/rednose_filter.py +++ b/site_scons/site_tools/rednose_filter.py @@ -1,4 +1,5 @@ import platform +import sys import eigen from SCons.Script import Dir, File @@ -9,8 +10,10 @@ def compile_single_filter(env, target, filter_gen_script, output_dir, extra_gen_ extra_generated_files = [File(f'{output_dir}/{x}') for x in extra_gen_artifacts] generator_file = File(filter_gen_script) + # SCons runs commands through cmd.exe on Windows, which cannot start a script by its shebang + python = f'"{sys.executable}" ' if platform.system() == "Windows" else "" env.Command(generated_src_files + extra_generated_files, - [generator_file] + script_deps, f"{File(generator_file).relpath} {target} {Dir(output_dir).relpath}") + [generator_file] + script_deps, f"{python}{File(generator_file).relpath} {target} {Dir(output_dir).relpath}") generated_cc_file = File(generated_src_files[:1])