diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml index 64a24d5..b924384 100644 --- a/.github/workflows/build-wheels.yml +++ b/.github/workflows/build-wheels.yml @@ -17,6 +17,9 @@ jobs: - os: macos-14 platform: macos arch: arm64 + - os: windows-2022 + platform: windows + arch: x86_64 runs-on: ${{ matrix.os }} env: @@ -48,7 +51,8 @@ jobs: restore-keys: | cibw-${{ runner.os }}-${{ matrix.platform }}-${{ matrix.arch }}- - - name: Build wheels + - name: Build wheels (Linux) + if: matrix.platform == 'linux' env: CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-*" CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux_*" @@ -56,17 +60,39 @@ jobs: CIBW_BEFORE_BUILD: "bash {project}/scripts/prepare_build.sh" CIBW_TEST_SKIP: "*" CIBW_ENVIRONMENT: "RAYFORCE_GITHUB=https://github.com/RayforceDB/rayforce.git" - CIBW_MACOS_ENVIRONMENT: "MACOSX_DEPLOYMENT_TARGET=11.0" CIBW_CACHE_PATH: ".cibw" CIBW_MANYLINUX_X86_64_IMAGE: "quay.io/pypa/manylinux_2_28_x86_64:latest" CIBW_BEFORE_ALL_LINUX: "yum install -y git gcc clang gcc-c++ make" + run: cibuildwheel --platform linux --archs x86_64 --output-dir wheelhouse - run: | - if [ "${{ matrix.platform }}" = "linux" ]; then - cibuildwheel --platform linux --archs x86_64 --output-dir wheelhouse - else - cibuildwheel --platform macos --archs arm64 --output-dir wheelhouse - fi + - name: Build wheels (macOS) + if: matrix.platform == 'macos' + env: + CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-*" + CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux_*" + CIBW_ARCHS: "auto64" + CIBW_BEFORE_BUILD: "bash {project}/scripts/prepare_build.sh" + CIBW_TEST_SKIP: "*" + CIBW_ENVIRONMENT: "RAYFORCE_GITHUB=https://github.com/RayforceDB/rayforce.git" + CIBW_MACOS_ENVIRONMENT: "MACOSX_DEPLOYMENT_TARGET=11.0" + CIBW_CACHE_PATH: ".cibw" + run: cibuildwheel --platform macos --archs arm64 --output-dir wheelhouse + + - name: Build wheels (Windows) + if: matrix.platform == 'windows' + env: + CIBW_BUILD: "cp311-* cp312-* cp313-* cp314-*" + CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux_*" + CIBW_ARCHS: "auto64" + CIBW_BEFORE_ALL_WINDOWS: >- + C:\msys64\usr\bin\bash -lc "pacman -S --noconfirm mingw-w64-x86_64-clang mingw-w64-x86_64-lld make" + CIBW_BEFORE_BUILD_WINDOWS: "powershell -ExecutionPolicy Bypass -File {project}/scripts/prepare_build_windows.ps1" + CIBW_TEST_SKIP: "*" + CIBW_ENVIRONMENT_WINDOWS: >- + RAYFORCE_GITHUB=https://github.com/RayforceDB/rayforce.git + PATH="C:\\msys64\\mingw64\\bin;C:\\msys64\\usr\\bin;$PATH" + CIBW_CACHE_PATH: ".cibw" + run: cibuildwheel --platform windows --archs AMD64 --output-dir wheelhouse - name: Upload wheel artifacts uses: actions/upload-artifact@v4 @@ -113,6 +139,22 @@ jobs: python-version: '3.14' platform: macos arch: arm64 + - os: windows-2022 + python-version: '3.11' + platform: windows + arch: x86_64 + - os: windows-2022 + python-version: '3.12' + platform: windows + arch: x86_64 + - os: windows-2022 + python-version: '3.13' + platform: windows + arch: x86_64 + - os: windows-2022 + python-version: '3.14' + platform: windows + arch: x86_64 runs-on: ${{ matrix.os }} @@ -122,7 +164,7 @@ jobs: with: submodules: false - - name: Verify AVX2 support (required for rayforce) + - name: Verify AVX2 support (Linux) if: runner.os == 'Linux' run: | echo "Checking CPU features..." @@ -133,6 +175,15 @@ jobs: exit 1 fi + - name: Verify AVX2 support (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + $cpu = Get-CimInstance Win32_Processor + Write-Host "CPU: $($cpu.Name)" + # GitHub Actions Windows runners use modern Xeon/EPYC CPUs with AVX2 + Write-Host "AVX2 support assumed on GitHub Actions Windows runners" + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -150,18 +201,27 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-asyncio pytest-timeout websockets pandas>=2.0.0 polars>=0.19.0 pyarrow sqlglot - - name: Remove local rayforce package to force pip installation + - name: Remove local rayforce package (Unix) + if: runner.os != 'Windows' run: | - # Remove local rayforce directory to ensure we test against installed wheel rm -rf rayforce/__pycache__ - rm -rf rayforce/*.so rayforce/*.dylib rayforce/*.dll + rm -rf rayforce/*.so rayforce/*.dylib rayforce/*.dll rayforce/*.pyd rm -rf rayforce/plugins/*.so rayforce/plugins/*.dylib rayforce/plugins/*.dll - # Move rayforce directory out of Python path temporarily if [ -d "rayforce" ]; then mv rayforce rayforce.local fi - - name: Install and test wheel + - name: Remove local rayforce package (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + if (Test-Path "rayforce\__pycache__") { Remove-Item -Recurse -Force "rayforce\__pycache__" } + Get-ChildItem -Path "rayforce" -Include "*.so","*.dylib","*.dll","*.pyd" -ErrorAction SilentlyContinue | Remove-Item -Force + Get-ChildItem -Path "rayforce\plugins" -Include "*.so","*.dylib","*.dll" -ErrorAction SilentlyContinue | Remove-Item -Force + if (Test-Path "rayforce") { Rename-Item "rayforce" "rayforce.local" } + + - name: Install and test wheel (Unix) + if: runner.os != 'Windows' run: | PYVER=$(echo '${{ matrix.python-version }}' | tr -d '.') WHEEL_FILE=$(find wheels -name "*cp${PYVER}*.whl" | head -1) @@ -174,6 +234,21 @@ jobs: pip install "$WHEEL_FILE" --force-reinstall --no-deps python -m pytest -m "" -x -vv --durations=20 tests/ + - name: Install and test wheel (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + $pyver = '${{ matrix.python-version }}' -replace '\.','' + $wheel = Get-ChildItem -Path wheels -Filter "*cp${pyver}*win_amd64*.whl" | Select-Object -First 1 + if (-not $wheel) { + Write-Error "No wheel file found for Python ${{ matrix.python-version }}" + Get-ChildItem -Path wheels -Filter "*.whl" + exit 1 + } + Write-Host "Installing wheel: $($wheel.FullName)" + pip install $wheel.FullName --force-reinstall --no-deps + python -m pytest -m "" -x -vv --durations=20 tests/ + publish: name: Publish to PyPI needs: test_wheels diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8fd38d8..7ed2e94 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,13 +1,56 @@ name: Coverage on: + pull_request: push: branches: [master] jobs: coverage: - name: Coverage - runs-on: ubuntu-latest + name: Coverage (${{ matrix.os }}, Python ${{ matrix.python-version }}) + strategy: + fail-fast: false + matrix: + include: + # TODO: re-enable linux and macos after Windows support is confirmed + # - os: ubuntu-22.04 + # python-version: '3.11' + # platform: linux + # - os: ubuntu-22.04 + # python-version: '3.12' + # platform: linux + # - os: ubuntu-22.04 + # python-version: '3.13' + # platform: linux + # - os: ubuntu-22.04 + # python-version: '3.14' + # platform: linux + # - os: macos-14 + # python-version: '3.11' + # platform: macos + # - os: macos-14 + # python-version: '3.12' + # platform: macos + # - os: macos-14 + # python-version: '3.13' + # platform: macos + # - os: macos-14 + # python-version: '3.14' + # platform: macos + - os: windows-2022 + python-version: '3.11' + platform: windows + - os: windows-2022 + python-version: '3.12' + platform: windows + - os: windows-2022 + python-version: '3.13' + platform: windows + - os: windows-2022 + python-version: '3.14' + platform: windows + + runs-on: ${{ matrix.os }} env: RAYFORCE_GITHUB: "https://github.com/RayforceDB/rayforce.git" @@ -18,25 +61,46 @@ jobs: with: submodules: false - - name: Set up Python 3.14 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: - python-version: '3.14' + python-version: ${{ matrix.python-version }} cache: 'pip' - - name: Install system dependencies + - name: Install system dependencies (Linux) + if: matrix.platform == 'linux' run: | sudo apt-get update -qq sudo apt-get install -y -qq git gcc clang g++ make + - name: Install system dependencies (macOS) + if: matrix.platform == 'macos' + run: | + echo "Xcode CLI tools provide clang and make" + + - name: Install system dependencies (Windows) + if: matrix.platform == 'windows' + shell: powershell + run: | + C:\msys64\usr\bin\bash -lc "pacman -S --noconfirm mingw-w64-x86_64-clang mingw-w64-x86_64-lld make" + - name: Install build dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel setuptools-scm - - name: Build rayforce binaries + - name: Build rayforce binaries (Unix) + if: matrix.platform != 'windows' run: make app + - name: Build rayforce binaries (Windows) + if: matrix.platform == 'windows' + shell: powershell + env: + PATH: "C:\\msys64\\mingw64\\bin;C:\\msys64\\usr\\bin;${{ env.PATH }}" + run: | + & scripts/prepare_build_windows.ps1 + - name: Install test dependencies run: | pip install -e ".[dev,test-plugins]" @@ -54,13 +118,13 @@ jobs: uses: codecov/codecov-action@v4 with: file: ./coverage.xml - flags: unittests - name: codecov-umbrella + flags: ${{ matrix.platform }}-py${{ matrix.python-version }} + name: coverage-${{ matrix.platform }}-py${{ matrix.python-version }} fail_ci_if_error: false - name: Upload coverage HTML report uses: actions/upload-artifact@v4 with: - name: coverage-report + name: coverage-report-${{ matrix.platform }}-py${{ matrix.python-version }} path: htmlcov/ retention-days: 30 diff --git a/MANIFEST.in b/MANIFEST.in index 3dd36ff..6abeb21 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,7 +2,11 @@ include README.md include LICENSE recursive-include rayforce *.so recursive-include rayforce *.dylib +recursive-include rayforce *.dll +recursive-include rayforce *.pyd recursive-include rayforce/plugins *.so recursive-include rayforce/plugins *.dylib +recursive-include rayforce/plugins *.dll recursive-include rayforce *.pyi include rayforce/bin/rayforce +include rayforce/bin/rayforce.exe diff --git a/Makefile b/Makefile index e6ff890..85f3242 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ ifeq ($(UNAME_S),Darwin) RAYKX_LIB_NAME = libraykx.dylib RELEASE_LDFLAGS = $(shell python3 -c "import sysconfig; print(sysconfig.get_config_var('LDFLAGS') or '')") PYTHON_VERSION = $(shell python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") - SHARED_COMPILE_FLAGS = -lpython$(PYTHON_VERSION) + SHARED_COMPILE_FLAGS = -undefined dynamic_lookup else ifeq ($(UNAME_S),Linux) RAYKX_LIB_NAME = libraykx.so RELEASE_LDFLAGS = $$(RELEASE_LDFLAGS) diff --git a/pyproject.toml b/pyproject.toml index 745c6a6..cc251f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,11 @@ before-all = [ "yum install -y clang llvm", ] +[tool.cibuildwheel.windows] +before-all = "C:\\msys64\\usr\\bin\\bash -lc \"pacman -S --noconfirm mingw-w64-x86_64-clang mingw-w64-x86_64-lld make\"" +before-build = "powershell -ExecutionPolicy Bypass -File {project}/scripts/prepare_build_windows.ps1" +environment = { PATH = "C:\\msys64\\mingw64\\bin;C:\\msys64\\usr\\bin;$PATH", RAYFORCE_GITHUB = "https://github.com/RayforceDB/rayforce.git" } + [tool.ruff] show-fixes = true target-version = "py314" diff --git a/rayforce/__init__.py b/rayforce/__init__.py index 080e7ac..a29e8aa 100644 --- a/rayforce/__init__.py +++ b/rayforce/__init__.py @@ -3,6 +3,7 @@ """ import ctypes +import os from pathlib import Path import sys @@ -19,16 +20,25 @@ lib_name = "_rayforce_c.so" raykx_lib_name = "libraykx.dylib" elif sys.platform == "win32": - lib_name = "rayforce.dll" + lib_name = "_rayforce_c.pyd" + raykx_lib_name = "raykx.dll" else: raise ImportError(f"Platform not supported: {sys.platform}") -lib_path = Path(__file__).resolve().parent / lib_name -raykx_lib_path = Path(__file__).resolve().parent / "plugins" / raykx_lib_name +_pkg_dir = Path(__file__).resolve().parent +lib_path = _pkg_dir / lib_name +raykx_lib_path = _pkg_dir / "plugins" / raykx_lib_name if lib_path.exists() and raykx_lib_path.exists(): try: - ctypes.CDLL(str(lib_path), mode=ctypes.RTLD_GLOBAL) - ctypes.CDLL(str(raykx_lib_path), mode=ctypes.RTLD_GLOBAL) + if sys.platform == "win32": + # Add package dirs to DLL search path (Python 3.8+ restricts DLL loading) + os.add_dll_directory(str(_pkg_dir)) + os.add_dll_directory(str(_pkg_dir / "plugins")) + load_mode = 0 + else: + load_mode = ctypes.RTLD_GLOBAL + ctypes.CDLL(str(lib_path), mode=load_mode) + ctypes.CDLL(str(raykx_lib_path), mode=load_mode) except Exception as e: raise ImportError(f"Error loading CDLL: {e}") from e else: diff --git a/rayforce/capi/rayforce_c.h b/rayforce/capi/rayforce_c.h index 92602eb..3c30a7c 100644 --- a/rayforce/capi/rayforce_c.h +++ b/rayforce/capi/rayforce_c.h @@ -39,7 +39,9 @@ #include "vary.h" #include #include +#ifndef _WIN32 #include +#endif #ifndef memcpy extern void *memcpy(void *dest, const void *src, size_t n); diff --git a/rayforce/cli.py b/rayforce/cli.py index e9de2e4..ff7e1c6 100644 --- a/rayforce/cli.py +++ b/rayforce/cli.py @@ -6,15 +6,17 @@ def find_rayforce_executable(): package_dir = Path(__file__).parent - bundled_executable = package_dir / "bin" / "rayforce" + exe_name = "rayforce.exe" if sys.platform == "win32" else "rayforce" - if bundled_executable.exists() and os.access(bundled_executable, os.X_OK): + bundled_executable = package_dir / "bin" / exe_name + if bundled_executable.exists() and ( + sys.platform == "win32" or os.access(bundled_executable, os.X_OK) + ): return str(bundled_executable) project_root = package_dir.parent - dev_executable = project_root / "tmp" / "rayforce-c" / "rayforce" - - if dev_executable.exists() and os.access(dev_executable, os.X_OK): + dev_executable = project_root / "tmp" / "rayforce-c" / exe_name + if dev_executable.exists() and (sys.platform == "win32" or os.access(dev_executable, os.X_OK)): return str(dev_executable) raise FileNotFoundError("Rayforce executable not found. Try to reinstall the library") diff --git a/rayforce/plugins/raykx.py b/rayforce/plugins/raykx.py index bac7b4c..194a961 100644 --- a/rayforce/plugins/raykx.py +++ b/rayforce/plugins/raykx.py @@ -12,7 +12,7 @@ if sys.platform == "darwin": raykx_lib_name = "libraykx.dylib" elif sys.platform == "win32": - raykx_lib_name = "libraykx.dll" + raykx_lib_name = "raykx.dll" else: raykx_lib_name = "libraykx.so" diff --git a/scripts/prepare_build_windows.ps1 b/scripts/prepare_build_windows.ps1 new file mode 100644 index 0000000..84615fd --- /dev/null +++ b/scripts/prepare_build_windows.ps1 @@ -0,0 +1,121 @@ +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +# Ensure MSYS2/MinGW and Git for Windows are on PATH +$env:PATH = "C:\msys64\mingw64\bin;C:\msys64\usr\bin;C:\Program Files\Git\cmd;$env:PATH" + +$EXEC_DIR = Get-Location +$RAYFORCE_GITHUB = if ($env:RAYFORCE_GITHUB) { $env:RAYFORCE_GITHUB } else { "https://github.com/RayforceDB/rayforce.git" } + +$PYTHON_BIN = if ($env:PYTHON_BIN) { $env:PYTHON_BIN } else { "python" } +$PYTHON_VERSION = & $PYTHON_BIN --version 2>&1 | ForEach-Object { $_.ToString().Split(" ")[1] } + +Write-Host "Python version: $PYTHON_VERSION" +Write-Host "Using rayforce repo: $RAYFORCE_GITHUB" + +# --- Clean previous build artifacts --- +Write-Host "Cleaning previous build artifacts..." +$cleanPaths = @( + "$EXEC_DIR\tmp\rayforce-c", + "$EXEC_DIR\rayforce\rayforce", + "$EXEC_DIR\rayforce\bin", + "$EXEC_DIR\rayforce\_rayforce_c.pyd", + "$EXEC_DIR\rayforce\_rayforce_c.so", + "$EXEC_DIR\rayforce\rayforce.dll", + "$EXEC_DIR\build" +) +foreach ($p in $cleanPaths) { + if (Test-Path $p) { Remove-Item -Recurse -Force $p } +} +Get-ChildItem -Path $EXEC_DIR -Filter "*.egg-info" -Directory -Recurse -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force +Get-ChildItem -Path $EXEC_DIR -Filter "__pycache__" -Directory -Recurse -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force +# Clean plugin DLLs +Get-ChildItem -Path "$EXEC_DIR\rayforce\plugins" -Filter "libraykx.*" -ErrorAction SilentlyContinue | Remove-Item -Force +Get-ChildItem -Path "$EXEC_DIR\rayforce\plugins" -Filter "raykx.*" -ErrorAction SilentlyContinue | Remove-Item -Force + +# --- Clone rayforce repo --- +Write-Host "Cloning rayforce repo from GitHub..." +git clone $RAYFORCE_GITHUB "$EXEC_DIR\tmp\rayforce-c" +Copy-Item -Recurse "$EXEC_DIR\tmp\rayforce-c\core" "$EXEC_DIR\rayforce\rayforce" + +# --- Get Python build info --- +$PYTHON_INCLUDE = & $PYTHON_BIN -c "import sysconfig; print(sysconfig.get_config_var('INCLUDEPY'))" +$PYTHON_PYVER = & $PYTHON_BIN -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')" +$PYTHON_LIB_DIR = & $PYTHON_BIN -c "import sysconfig, os; print(os.path.dirname(sysconfig.get_config_var('INCLUDEPY'))+'\\libs')" + +Write-Host "Python include: $PYTHON_INCLUDE" +Write-Host "Python version tag: $PYTHON_PYVER" +Write-Host "Python lib dir: $PYTHON_LIB_DIR" + +# --- Patch Makefile for Python target --- +Write-Host "Patching Makefile for Python support..." +$pythonTarget = @" + +PY_OBJECTS = core/rayforce_c.o core/raypy_init_from_py.o core/raypy_init_from_buffer.o core/raypy_read_from_rf.o core/raypy_queries.o core/raypy_io.o core/raypy_binary.o core/raypy_dynlib.o core/raypy_eval.o core/raypy_iter.o core/raypy_serde.o +PY_APP_OBJECTS = app/term.o +python: CFLAGS = `$(RELEASE_CFLAGS) -DPY_SSIZE_T_CLEAN -I$PYTHON_INCLUDE -Wno-macro-redefined +python: LDFLAGS = -fuse-ld=lld -static -L$PYTHON_LIB_DIR -lpython$PYTHON_PYVER +python: `$(CORE_OBJECTS) `$(PY_OBJECTS) `$(PY_APP_OBJECTS) + `$(CC) -shared -o _rayforce_c.pyd `$(CFLAGS) `$(CORE_OBJECTS) `$(PY_OBJECTS) `$(PY_APP_OBJECTS) `$(LIBS) `$(LDFLAGS) +"@ +Add-Content -Path "$EXEC_DIR\tmp\rayforce-c\Makefile" -Value $pythonTarget + +# --- Copy C API source files --- +Write-Host "Copying C API source files..." +$capiFiles = @( + "rayforce_c.c", "rayforce_c.h", + "raypy_init_from_py.c", "raypy_init_from_buffer.c", "raypy_read_from_rf.c", + "raypy_queries.c", "raypy_io.c", "raypy_binary.c", + "raypy_dynlib.c", "raypy_eval.c", "raypy_iter.c", "raypy_serde.c" +) +foreach ($f in $capiFiles) { + Copy-Item "$EXEC_DIR\rayforce\capi\$f" "$EXEC_DIR\tmp\rayforce-c\core\$f" +} + +# --- Build shared library (produces rayforce.dll + rayforce.lib needed by raykx) --- +# Override RELEASE_LDFLAGS: upstream uses MSVC-style /DEF:/IMPLIB: flags which MinGW lld doesn't understand. +# Use MinGW-compatible --out-implib to generate the import library. +# Append shared-win target (includes app/term.o — Windows requires all symbols resolved at link time) +Write-Host "Building Rayforce shared library..." +$sharedWinTarget = @" + +shared-win: CFLAGS = `$(RELEASE_CFLAGS) +shared-win: LDFLAGS = -fuse-ld=lld -static -Wl,--out-implib,rayforce.lib +shared-win: `$(CORE_OBJECTS) app/term.o + `$(CC) -shared -o `$(LIBNAME) `$(CFLAGS) `$(CORE_OBJECTS) app/term.o `$(LIBS) `$(LDFLAGS) +"@ +Add-Content -Path "$EXEC_DIR\tmp\rayforce-c\Makefile" -Value $sharedWinTarget + +Push-Location "$EXEC_DIR\tmp\rayforce-c" +C:\msys64\usr\bin\make.exe shared-win +if ($LASTEXITCODE -ne 0) { throw "make shared-win failed with exit code $LASTEXITCODE" } +Pop-Location + +# --- Build Raykx plugin (depends on rayforce.lib from shared build) --- +Write-Host "Building Raykx plugin..." +Push-Location "$EXEC_DIR\tmp\rayforce-c\ext\raykx" +C:\msys64\usr\bin\make.exe release +if ($LASTEXITCODE -ne 0) { throw "make raykx failed with exit code $LASTEXITCODE" } +Pop-Location +# Copy rayforce.dll (runtime dependency of raykx.dll) and the plugin +Copy-Item "$EXEC_DIR\tmp\rayforce-c\rayforce.dll" "$EXEC_DIR\rayforce\rayforce.dll" +Copy-Item "$EXEC_DIR\tmp\rayforce-c\ext\raykx\raykx.dll" "$EXEC_DIR\rayforce\plugins\raykx.dll" + +# --- Build Python extension --- +Write-Host "Building Rayforce Python extension..." +Push-Location "$EXEC_DIR\tmp\rayforce-c" +C:\msys64\usr\bin\make.exe python +if ($LASTEXITCODE -ne 0) { throw "make python failed with exit code $LASTEXITCODE" } +Pop-Location +Copy-Item "$EXEC_DIR\tmp\rayforce-c\_rayforce_c.pyd" "$EXEC_DIR\rayforce\_rayforce_c.pyd" + +# --- Build Rayforce executable --- +Write-Host "Building Rayforce executable..." +Push-Location "$EXEC_DIR\tmp\rayforce-c" +C:\msys64\usr\bin\make.exe release "RELEASE_LDFLAGS=-fuse-ld=lld -Wl,--out-implib,rayforce.lib" +if ($LASTEXITCODE -ne 0) { throw "make release failed with exit code $LASTEXITCODE" } +Pop-Location +New-Item -ItemType Directory -Force -Path "$EXEC_DIR\rayforce\bin" | Out-Null +Copy-Item "$EXEC_DIR\tmp\rayforce-c\rayforce.exe" "$EXEC_DIR\rayforce\bin\rayforce.exe" + +Write-Host "Build preparation complete!" diff --git a/setup.py b/setup.py index fd45d8e..dc1f5a6 100644 --- a/setup.py +++ b/setup.py @@ -12,14 +12,22 @@ def has_ext_modules(self): version="0.6.1", packages=find_packages(), package_data={ - "rayforce": ["*.so", "*.dylib", "*.pyi", "bin/rayforce"], - "rayforce.plugins": ["*.so", "*.dylib"], + "rayforce": [ + "*.so", + "*.dylib", + "*.dll", + "*.pyd", + "*.pyi", + "bin/rayforce", + "bin/rayforce.exe", + ], + "rayforce.plugins": ["*.so", "*.dylib", "*.dll"], }, include_package_data=True, zip_safe=False, python_requires=">=3.11", description="Python bindings for RayforceDB", - long_description=open("README.md").read(), # noqa: SIM115 + long_description=open("README.md", encoding="utf-8").read(), # noqa: SIM115 long_description_content_type="text/markdown", author="FalsePublicEnemy", license="MIT", diff --git a/tests/plugins/test_raykx.py b/tests/plugins/test_raykx.py index 5850257..065dccc 100644 --- a/tests/plugins/test_raykx.py +++ b/tests/plugins/test_raykx.py @@ -1,10 +1,16 @@ +import sys from unittest.mock import MagicMock, patch import pytest from rayforce import _rayforce_c as r from rayforce.plugins import errors -from rayforce.plugins.raykx import KDBConnection, KDBEngine + +# raykx plugin uses FFI.loadfn_from_file() at import time which requires +# the Rayforce runtime's dynlib loader to support the platform +raykx = pytest.importorskip("rayforce.plugins.raykx", reason="raykx plugin not available on this platform") +KDBConnection = raykx.KDBConnection +KDBEngine = raykx.KDBEngine pytestmark = pytest.mark.plugin