diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a8792f4..8134be2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,8 +6,16 @@ jobs: test: strategy: matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + defaults: + run: + shell: ${{ contains(matrix.os, 'windows') && 'msys2 {0}' || 'bash -e {0}' }} steps: - uses: actions/checkout@v7 + - uses: msys2/setup-msys2@v2 + if: runner.os == 'Windows' + with: + msystem: CLANG64 + install: mingw-w64-clang-x86_64-clang mingw-w64-clang-x86_64-dlfcn mingw-w64-clang-x86_64-uv # clang for scons, dlopen for the filters - run: ./test.sh diff --git a/SConstruct b/SConstruct index 7b8fa4e..14a8305 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,8 +40,12 @@ 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 ) +if WINDOWS: + env["CC"], env["CXX"] = "clang", "clang++" # the mingw tool assumes gcc + env["SHLIBPREFIX"] = "lib" # the mingw tool drops the prefix ekf_load expects + env.Append(LINKFLAGS=["-static"]) # libc++ into the DLLs so they load outside the MSYS2 shell # Cython build enviroment envCython = env.Clone() @@ -48,6 +54,10 @@ 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}"] elif arch == "aarch64": envCython["LINKFLAGS"] = ["-shared"] envCython["LIBS"] = [os.path.basename(python_path)] diff --git a/pyproject.toml b/pyproject.toml index eadb653..9d70c44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,3 +46,13 @@ flake8-implicit-str-concat.allow-multiline = false [tool.ty.rules] unresolved-import = "ignore" # Cython-compiled modules (.pyx) and dynamic SCons exports unresolved-attribute = "ignore" # Cython-backed filter attributes + +# --- TODO REMOVE AFTER DEPENDENCY PR MERGES (commaai/dependencies#107): fork-only index of the comma-deps Windows wheels until PyPI has them; not for upstream --- +[[tool.uv.index]] +name = "comma-deps-windows" +url = "https://github.com/AmyJeanes/commaai-dependencies/releases/download/windows-post116/index.html" +format = "flat" +explicit = true + +[tool.uv.sources] +comma-deps-eigen = [{ index = "comma-deps-windows", marker = "sys_platform == 'win32'" }] diff --git a/rednose/helpers/__init__.py b/rednose/helpers/__init__.py index 3acc14a..e826507 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 = "dylib" if platform.system() == "Darwin" else "dll" if platform.system() == "Windows" else "so" shared_fn = os.path.join(folder, f"lib{name}.{shared_ext}") header_fn = os.path.join(folder, f"{name}.h") diff --git a/rednose/helpers/ekf_load.cc b/rednose/helpers/ekf_load.cc index 882b0b4..b1c7114 100644 --- a/rednose/helpers/ekf_load.cc +++ b/rednose/helpers/ekf_load.cc @@ -26,6 +26,8 @@ void ekf_load_and_register(const std::string& ekf_directory, const std::string& #ifdef __APPLE__ std::string dylib_ext = ".dylib"; +#elif defined(_WIN32) + std::string dylib_ext = ".dll"; #else std::string dylib_ext = ".so"; #endif diff --git a/site_scons/site_tools/cython.py b/site_scons/site_tools/cython.py index 702a6e9..59370e2 100644 --- a/site_scons/site_tools/cython.py +++ b/site_scons/site_tools/cython.py @@ -70,5 +70,11 @@ def generate(env): create_builder(env) + # Python imports extension modules as .pyd on Windows; the SConscripts name them .so + if env["PLATFORM"] == "win32": + 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 + env.Append(PROGEMITTER=[pyd_emitter]) + def exists(env): return True diff --git a/site_scons/site_tools/rednose_filter.py b/site_scons/site_tools/rednose_filter.py index a3f8a9a..7958627 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,9 @@ 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) + # the interpreter running SCons has the deps; cmd.exe cannot start a script by its shebang 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'"{sys.executable}" {File(generator_file).relpath} {target} {Dir(output_dir).relpath}') generated_cc_file = File(generated_src_files[:1])