Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .github/workflows/windows-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
name: Windows Build

on:
push:
branches: [master, develop, feature/windows-support]
pull_request:
branches: [master, develop]

jobs:
build-windows:
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
compiler: [msvc, mingw]
python-version: ['3.11', '3.12']

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install scons

- name: Install SWIG
run: |
choco install swig -y
shell: cmd

- name: Set up MSVC
if: matrix.compiler == 'msvc'
uses: ilammy/msvc-dev-cmd@v1

- name: Set up MinGW
if: matrix.compiler == 'mingw'
uses: egor-tensin/setup-mingw@v2
with:
platform: x64

- name: Install Perl (Strawberry)
run: |
choco install strawberryperl -y
refreshenv
shell: cmd

- name: Install R
uses: r-lib/actions/setup-r@v2
with:
r-version: 'release'

- name: Install R packages
run: |
Rscript -e "install.packages(c('Rcpp', 'RInside'), repos='https://cloud.r-project.org')"
shell: cmd

- name: Display build environment
run: |
echo "Python version:"
python --version
echo "SCons version:"
scons --version
echo "SWIG version:"
swig -version
echo "R version:"
Rscript --version
echo "Perl version:"
perl -v | head -2
shell: bash

- name: Build PluMA (without R and Perl for initial test)
run: |
scons --without-r --without-perl
shell: cmd
continue-on-error: true

- name: Build PluMA (Python only)
if: failure()
run: |
scons --without-r --without-perl
shell: cmd

- name: Check build artifacts
run: |
echo "Checking for built files..."
if (Test-Path "pluma.exe") { echo "pluma.exe exists" } else { echo "pluma.exe not found" }
if (Test-Path "_PyPluMA.dll") { echo "_PyPluMA.dll exists" } else { echo "_PyPluMA.dll not found" }
if (Test-Path "PluGen/plugen.exe") { echo "plugen.exe exists" } else { echo "plugen.exe not found" }
Get-ChildItem -Filter "*.dll" | ForEach-Object { echo $_.Name }
Get-ChildItem -Filter "*.exe" | ForEach-Object { echo $_.Name }
shell: pwsh

build-windows-full:
runs-on: windows-latest
needs: build-windows
if: success()

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install scons

- name: Install SWIG
run: choco install swig -y
shell: cmd

- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@v1

- name: Install Perl (Strawberry)
run: choco install strawberryperl -y
shell: cmd

- name: Install R
uses: r-lib/actions/setup-r@v2
with:
r-version: 'release'

- name: Install R packages
run: |
Rscript -e "install.packages(c('Rcpp', 'RInside'), repos='https://cloud.r-project.org')"
shell: cmd

- name: Full build with all languages
run: scons
shell: cmd
continue-on-error: true

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: pluma-windows-build
path: |
pluma.exe
*.dll
PluGen/plugen.exe
if-no-files-found: warn
85 changes: 71 additions & 14 deletions SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,18 @@ from os import environ, getenv
from os.path import relpath

from resolve_requirements import resolve_and_install
from build_config import include_search_path, platform_id, is_darwin, is_alpine
from build_config import (
include_search_path,
platform_id,
is_darwin,
is_alpine,
is_windows,
is_msvc,
is_mingw,
shared_lib_ext,
shared_lib_prefix,
executable_ext,
)
from build_support import (
CheckPerl,
CheckRPackages,
Expand Down Expand Up @@ -130,24 +141,70 @@ def get_platform_config():
"""Return platform-specific configuration as a dict."""
if is_darwin:
return {"CCFLAGS": ["-DAPPLE"]}
if is_windows:
# Windows linkers don't accept --rdynamic; the executable's symbols
# are exported via the .lib/.exp import library instead. Define
# PLUMA_PLATFORM_WINDOWS so src/platform.h selects the LoadLibrary /
# GetProcAddress code path over dlopen / dlsym.
if is_msvc:
return {
"CPPDEFINES": ["PLUMA_PLATFORM_WINDOWS", "WIN32_LEAN_AND_MEAN", "NOMINMAX"],
}
# MinGW: keep gcc-style flags but add Win32 LoadLibrary linkage.
return {
"CPPDEFINES": ["PLUMA_PLATFORM_WINDOWS", "WIN32_LEAN_AND_MEAN", "NOMINMAX"],
"LINKFLAGS": ["-Wl,--export-all-symbols"],
"LIBS": ["psapi"],
}
return {"LINKFLAGS": ["-rdynamic"], "LIBS": ["rt"]}


def _msvc_compile_flags():
"""Compile flags appropriate for MSVC's cl.exe."""
return ["/EHsc", "/utf-8", "/O2", "/I."]


def _gcc_compile_flags(shared):
"""gcc/clang-style flags, with -fPIC only meaningful for shared objects."""
base = ["-fpermissive", "-I.", "-O2"]
if shared:
return base + ["-fPIC"]
return base if is_windows else base + ["-fPIC"]


def create_base_environment():
"""Create and configure the base SCons environment."""
env = Environment(
ENV=environ,
CC=getenv("CC", "cc"),
CXX=getenv("CXX", "c++"),
CPPDEFINES=[],
SHCCFLAGS=["-fpermissive", "-fPIC", "-I.", "-O2"],
SHCXXFLAGS=[CXX_STANDARD, "-fPIC", "-I.", "-O2"],
CCFLAGS=["-fpermissive", "-fPIC", "-I.", "-O2"],
CXXFLAGS=[CXX_STANDARD, "-fPIC", "-O2"],
CPPPATH=include_search_path,
LICENSE=[LICENSE],
SHLIBPREFIX="",
)
if is_msvc:
env_kwargs = dict(
CC=getenv("CC", "cl"),
CXX=getenv("CXX", "cl"),
CPPDEFINES=[],
CCFLAGS=_msvc_compile_flags(),
CXXFLAGS=[CXX_STANDARD] + _msvc_compile_flags(),
SHCCFLAGS=_msvc_compile_flags(),
SHCXXFLAGS=[CXX_STANDARD] + _msvc_compile_flags(),
CPPPATH=include_search_path,
LICENSE=[LICENSE],
SHLIBPREFIX=shared_lib_prefix,
SHLIBSUFFIX=shared_lib_ext,
PROGSUFFIX=executable_ext,
)
else:
env_kwargs = dict(
CC=getenv("CC", "cc"),
CXX=getenv("CXX", "c++"),
CPPDEFINES=[],
SHCCFLAGS=_gcc_compile_flags(shared=True),
SHCXXFLAGS=[CXX_STANDARD] + _gcc_compile_flags(shared=True),
CCFLAGS=_gcc_compile_flags(shared=False),
CXXFLAGS=[CXX_STANDARD] + _gcc_compile_flags(shared=False),
CPPPATH=include_search_path,
LICENSE=[LICENSE],
SHLIBPREFIX=shared_lib_prefix,
SHLIBSUFFIX=shared_lib_ext,
PROGSUFFIX=executable_ext,
)
env = Environment(ENV=environ, **env_kwargs)

# Apply platform-specific settings
for key, value in get_platform_config().items():
Expand Down
70 changes: 58 additions & 12 deletions build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
used during the build process.
"""

import os
import subprocess
import sys
from os.path import abspath, relpath
Expand Down Expand Up @@ -49,24 +50,62 @@ def _detect_platform_id() -> Optional[str]:

is_darwin: bool = platform.startswith("darwin")
is_linux: bool = platform.startswith("linux")
is_windows: bool = platform.startswith("win")
is_windows: bool = platform.startswith("win") or platform == "cygwin"
is_alpine: bool = platform_id == "alpine"
is_unix: bool = is_darwin or is_linux or platform.startswith(("freebsd", "openbsd", "netbsd"))

# Windows toolchain detection. MSVC is selected when the user has loaded a
# Visual Studio developer environment (VSCMD_ARG_TGT_ARCH is exported by
# vcvarsall.bat) or has explicitly pointed CC/CXX at cl.exe. MinGW is the
# fallback Windows toolchain.
_cc = os.environ.get("CC", "").lower()
is_msvc: bool = is_windows and (
"vscmd_arg_tgt_arch" in {k.lower() for k in os.environ}
or _cc.endswith("cl") or _cc.endswith("cl.exe")
)
is_mingw: bool = is_windows and not is_msvc

# =============================================================================
# Search Paths
# Platform-Specific Filename Conventions
# =============================================================================

lib_search_path: List[str] = [
"/lib",
"/usr/lib",
"/usr/local/lib",
]
if is_windows:
shared_lib_ext: str = ".dll"
shared_lib_prefix: str = ""
executable_ext: str = ".exe"
path_separator: str = "\\"
elif is_darwin:
shared_lib_ext = ".dylib"
shared_lib_prefix = "lib"
executable_ext = ""
path_separator = "/"
else:
shared_lib_ext = ".so"
shared_lib_prefix = "lib"
executable_ext = ""
path_separator = "/"

include_search_path: List[str] = [
"/usr/include",
"/usr/local/include",
relpath("./src"),
]
# =============================================================================
# Search Paths
# =============================================================================

if is_windows:
# Windows doesn't have the Unix /lib / /usr/lib convention; downstream
# detectors (Python, Perl, R, Java) discover headers and import libraries
# through their respective tooling instead.
lib_search_path: List[str] = []
include_search_path: List[str] = [relpath("./src")]
else:
lib_search_path = [
"/lib",
"/usr/lib",
"/usr/local/lib",
]
include_search_path = [
"/usr/include",
"/usr/local/include",
relpath("./src"),
]

# =============================================================================
# Build Directories
Expand All @@ -88,6 +127,13 @@ def _detect_platform_id() -> Optional[str]:
"is_linux",
"is_windows",
"is_alpine",
"is_unix",
"is_msvc",
"is_mingw",
"shared_lib_ext",
"shared_lib_prefix",
"executable_ext",
"path_separator",
"lib_search_path",
"include_search_path",
"source_base_dir",
Expand Down
Loading
Loading