diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml new file mode 100644 index 0000000..e96f934 --- /dev/null +++ b/.github/workflows/build-wheels.yml @@ -0,0 +1,185 @@ +name: Build and Test Wheels + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +jobs: + build_wheel: + name: Build wheel on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Checkout CFD C library + - name: Checkout CFD C library + uses: actions/checkout@v4 + with: + repository: ${{ github.repository_owner }}/cfd + path: cfd + fetch-depth: 0 + + - name: Set up Python 3.8 + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build scikit-build-core setuptools-scm + + - name: Build CFD library (Unix) + if: runner.os != 'Windows' + run: | + cmake -S cfd -B cfd/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON + cmake --build cfd/build --config Release + echo "=== CFD library built ===" + ls -la cfd/build/lib/ + + - name: Build CFD library (Windows) + if: runner.os == 'Windows' + run: | + cmake -S cfd -B cfd/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF + cmake --build cfd/build --config Release + echo "=== CFD library built ===" + dir cfd\build\lib\Release + + - name: Build wheel (Unix) + if: runner.os != 'Windows' + env: + CFD_ROOT: ${{ github.workspace }}/cfd + CFD_STATIC_LINK: "ON" + run: | + echo "CFD_ROOT=$CFD_ROOT" + echo "Checking CFD library location..." + ls -la $CFD_ROOT/build/lib/ || echo "Library dir not found" + pip wheel . --no-deps --wheel-dir dist/ + echo "=== Wheel built ===" + ls -la dist/ + + - name: Build wheel (Windows) + if: runner.os == 'Windows' + env: + CFD_ROOT: ${{ github.workspace }}/cfd + CFD_STATIC_LINK: "ON" + run: | + echo "CFD_ROOT=$env:CFD_ROOT" + echo "Checking CFD library location..." + dir "$env:CFD_ROOT\build\lib\Release" + pip wheel . --no-deps --wheel-dir dist/ + echo "=== Wheel built ===" + dir dist + + - name: Inspect wheel contents + run: | + python -c " + import glob, zipfile + for wheel in glob.glob('dist/*.whl'): + print(f'=== Contents of {wheel} ===') + with zipfile.ZipFile(wheel) as zf: + for name in zf.namelist(): + print(name) + " + + - uses: actions/upload-artifact@v4 + with: + name: wheel-${{ matrix.os }} + path: dist/*.whl + + test_wheel: + name: Test wheel on ${{ matrix.os }} with Python ${{ matrix.python }} + needs: [build_wheel] + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python: ["3.8", "3.12"] + + steps: + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: "${{ matrix.python }}" + + - uses: actions/download-artifact@v4 + with: + name: wheel-${{ matrix.os }} + path: dist + + - name: Install wheel (Unix) + if: runner.os != 'Windows' + run: | + python -m pip install --upgrade pip + pip install dist/*.whl + pip install pytest numpy + + - name: Install wheel (Windows) + if: runner.os == 'Windows' + run: | + python -m pip install --upgrade pip + pip install (Get-ChildItem dist/*.whl).FullName + pip install pytest numpy + + - name: Debug Windows DLL (Windows) + if: runner.os == 'Windows' + run: | + # Find package directory via pip + $pkg_dir = python -c "import sysconfig; print(sysconfig.get_paths()['purelib'])" + $pkg_dir = "$pkg_dir\cfd_python" + echo "Package directory: $pkg_dir" + dir $pkg_dir + # Check what the .pyd file depends on + $pyd_file = Get-ChildItem "$pkg_dir\*.pyd" | Select-Object -First 1 + echo "PYD file: $pyd_file" + if ($pyd_file) { + # Use dumpbin to show dependencies + $dumpbin = Get-ChildItem "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe" | Select-Object -First 1 + echo "Using dumpbin: $dumpbin" + & $dumpbin.FullName /dependents $pyd_file.FullName + } + + - name: Test import (Unix) + if: runner.os != 'Windows' + run: | + cd /tmp + python -c " + import cfd_python + print('Package loaded:', cfd_python.__file__) + print('Version:', cfd_python.__version__) + print('Has list_solvers:', hasattr(cfd_python, 'list_solvers')) + if hasattr(cfd_python, 'list_solvers'): + print('Solvers:', cfd_python.list_solvers()) + " + + - name: Test import (Windows) + if: runner.os == 'Windows' + run: | + cd $env:TEMP + python -c "import cfd_python; print('Package loaded:', cfd_python.__file__); print('Version:', cfd_python.__version__); print('Has list_solvers:', hasattr(cfd_python, 'list_solvers')); print('Solvers:', cfd_python.list_solvers()) if hasattr(cfd_python, 'list_solvers') else None" + + # Checkout only tests directory for running tests + - uses: actions/checkout@v4 + with: + sparse-checkout: tests + sparse-checkout-cone-mode: false + + - name: Run tests (Unix) + if: runner.os != 'Windows' + run: | + cd /tmp + pytest $GITHUB_WORKSPACE/tests/ -v + + - name: Run tests (Windows) + if: runner.os == 'Windows' + run: | + cd $env:TEMP + pytest "$env:GITHUB_WORKSPACE\tests" -v diff --git a/.gitignore b/.gitignore index 3b0e77b..5a02d47 100644 --- a/.gitignore +++ b/.gitignore @@ -144,6 +144,10 @@ _deps/ # Build directories _skbuild/ *.egg-info/ +wheelhouse/ + +# Local copy of CFD library (created during builds) +src/cfd_lib/ # VTK output files *.vtk diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f2e89d..d871edb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,137 +1,124 @@ cmake_minimum_required(VERSION 3.15...3.27) -project(cfd_python LANGUAGES C CXX) +project(cfd_python LANGUAGES C) # Option to control static vs dynamic linking -option(CFD_STATIC_LINK "Statically link the CFD library" ON) +if(DEFINED ENV{CFD_STATIC_LINK}) + set(CFD_STATIC_LINK_DEFAULT $ENV{CFD_STATIC_LINK}) +else() + set(CFD_STATIC_LINK_DEFAULT ON) +endif() +option(CFD_STATIC_LINK "Statically link the CFD library" ${CFD_STATIC_LINK_DEFAULT}) -# Option to disable stable ABI (useful for development with Windows Store Python) -option(CFD_USE_STABLE_ABI "Use Python stable ABI (abi3)" OFF) +# Option to use stable ABI (abi3) +option(CFD_USE_STABLE_ABI "Build with Python stable ABI for cross-version compatibility" ON) -# Find Python and required components -find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development.Module) +# Debug output +message(STATUS "CFD_STATIC_LINK: ${CFD_STATIC_LINK}") +message(STATUS "CFD_USE_STABLE_ABI: ${CFD_USE_STABLE_ABI}") +message(STATUS "CFD_ROOT env: $ENV{CFD_ROOT}") -# Enable stable ABI (abi3) for Python 3.8+ if requested -if(CFD_USE_STABLE_ABI AND Python_VERSION VERSION_GREATER_EQUAL "3.8") - set(CMAKE_C_VISIBILITY_PRESET hidden) - set(CMAKE_CXX_VISIBILITY_PRESET hidden) - add_definitions(-DPy_LIMITED_API=0x03080000) - message(STATUS "Using Python stable ABI (abi3)") -endif() +# Find Python +find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development.Module) +message(STATUS "Python version: ${Python_VERSION}") +message(STATUS "Python executable: ${Python_EXECUTABLE}") -# Find the CFD library from the parent project -# Check for CFD_ROOT environment variable, fall back to default location +# Find CFD library if(DEFINED ENV{CFD_ROOT}) set(CFD_ROOT_DIR "$ENV{CFD_ROOT}") - message(STATUS "Using CFD_ROOT from environment: ${CFD_ROOT_DIR}") else() set(CFD_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../cfd") - message(STATUS "Using default CFD_ROOT: ${CFD_ROOT_DIR}") endif() -set(CFD_INCLUDE_DIR "${CFD_ROOT_DIR}/lib/include") +message(STATUS "CFD_ROOT_DIR: ${CFD_ROOT_DIR}") -# Determine library directory based on build type -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set(CFD_LIBRARY_DIR "${CFD_ROOT_DIR}/build/lib/Debug") -else() - set(CFD_LIBRARY_DIR "${CFD_ROOT_DIR}/build/lib/Release") -endif() +set(CFD_INCLUDE_DIR "${CFD_ROOT_DIR}/lib/include") +set(CFD_LIBRARY_DIRS + "${CFD_ROOT_DIR}/build/lib/Release" + "${CFD_ROOT_DIR}/build/lib" +) -# Check if CFD library exists (prefer static library for packaging) -if(CFD_STATIC_LINK) - # Look for static library first - if(WIN32) - find_library(CFD_LIBRARY - NAMES cfd_library_static cfd_library - PATHS ${CFD_LIBRARY_DIR} - NO_DEFAULT_PATH - ) - else() - find_library(CFD_LIBRARY - NAMES libcfd_library.a cfd_library - PATHS ${CFD_LIBRARY_DIR} - NO_DEFAULT_PATH - ) - endif() -else() - # Look for shared library - find_library(CFD_LIBRARY - NAMES cfd_library - PATHS ${CFD_LIBRARY_DIR} - NO_DEFAULT_PATH - ) -endif() +find_library(CFD_LIBRARY + NAMES cfd_library cfd_library_static + PATHS ${CFD_LIBRARY_DIRS} + NO_DEFAULT_PATH +) if(NOT CFD_LIBRARY) - message(FATAL_ERROR "CFD library not found in ${CFD_LIBRARY_DIR}. Please build the C library first.") + message(FATAL_ERROR "CFD library not found in ${CFD_LIBRARY_DIRS}") endif() message(STATUS "Found CFD library: ${CFD_LIBRARY}") -message(STATUS "CFD include directory: ${CFD_INCLUDE_DIR}") -message(STATUS "Static linking: ${CFD_STATIC_LINK}") +message(STATUS "CFD include dir: ${CFD_INCLUDE_DIR}") # Create the Python extension module -# Use WITH_SOABI to get proper ABI suffix for stable ABI builds -Python_add_library(cfd_python MODULE WITH_SOABI - src/cfd_python.c -) - -# Set properties for stable ABI -set_target_properties(cfd_python PROPERTIES - C_STANDARD 11 - CXX_STANDARD 17 - INTERPROCEDURAL_OPTIMIZATION ON -) +# For stable ABI on Windows, we need to manually create the library +# to avoid Python_add_library linking against version-specific python3X.lib +if(CFD_USE_STABLE_ABI AND WIN32) + # Create module manually for Windows stable ABI + add_library(cfd_python MODULE src/cfd_python.c) + + set_target_properties(cfd_python PROPERTIES + C_STANDARD 11 + PREFIX "" + SUFFIX ".pyd" + ) -# Use stable ABI if requested and available -if(CFD_USE_STABLE_ABI AND Python_VERSION VERSION_GREATER_EQUAL "3.8") + # Define Py_LIMITED_API for stable ABI target_compile_definitions(cfd_python PRIVATE Py_LIMITED_API=0x03080000) -endif() -# Set proper extension name -if(WIN32) - set_target_properties(cfd_python PROPERTIES SUFFIX ".pyd") + # Find and link python3.lib (stable ABI library) + get_filename_component(PYTHON_DIR "${Python_EXECUTABLE}" DIRECTORY) + # Try multiple possible locations for python3.lib + find_library(PYTHON3_STABLE_LIB + NAMES python3 + PATHS + "${PYTHON_DIR}/libs" + "${PYTHON_DIR}/../libs" + "${Python_LIBRARY_DIRS}" + NO_DEFAULT_PATH + ) + if(NOT PYTHON3_STABLE_LIB) + # Last resort: construct path directly + if(EXISTS "${PYTHON_DIR}/libs/python3.lib") + set(PYTHON3_STABLE_LIB "${PYTHON_DIR}/libs/python3.lib") + elseif(EXISTS "${PYTHON_DIR}/../libs/python3.lib") + set(PYTHON3_STABLE_LIB "${PYTHON_DIR}/../libs/python3.lib") + endif() + endif() + + if(PYTHON3_STABLE_LIB) + target_link_libraries(cfd_python PRIVATE ${PYTHON3_STABLE_LIB}) + message(STATUS "Linking against stable ABI library: ${PYTHON3_STABLE_LIB}") + else() + message(FATAL_ERROR "Could not find python3.lib for stable ABI linking") + endif() + + message(STATUS "Building Windows stable ABI extension") +else() + # Use Python_add_library for Unix or non-stable-ABI builds + Python_add_library(cfd_python MODULE WITH_SOABI + src/cfd_python.c + ) + + set_target_properties(cfd_python PROPERTIES + C_STANDARD 11 + ) + + if(CFD_USE_STABLE_ABI) + target_compile_definitions(cfd_python PRIVATE Py_LIMITED_API=0x03080000) + set_target_properties(cfd_python PROPERTIES SUFFIX ".abi3.so") + message(STATUS "Building Unix stable ABI extension") + endif() endif() -# Include directories target_include_directories(cfd_python PRIVATE ${CFD_INCLUDE_DIR} ${Python_INCLUDE_DIRS} ) -# Link libraries target_link_libraries(cfd_python PRIVATE ${CFD_LIBRARY} ) -# On Windows, handle stable ABI linking explicitly -if(WIN32) - if(CFD_USE_STABLE_ABI) - # For stable ABI, we need to link against python3.lib - # Find the Python library directory and link python3.lib explicitly - get_filename_component(Python_LIBRARY_DIR "${Python_LIBRARY_DIRS}" ABSOLUTE) - if(NOT Python_LIBRARY_DIR) - # Fallback: derive from Python executable path - get_filename_component(Python_ROOT "${Python_EXECUTABLE}" DIRECTORY) - set(Python_LIBRARY_DIR "${Python_ROOT}/libs") - endif() - find_library(Python3_LIBRARY - NAMES python3 - PATHS "${Python_LIBRARY_DIR}" - NO_DEFAULT_PATH - ) - if(Python3_LIBRARY) - message(STATUS "Using stable ABI library: ${Python3_LIBRARY}") - target_link_libraries(cfd_python PRIVATE ${Python3_LIBRARY}) - else() - message(WARNING "python3.lib not found, falling back to versioned library") - target_link_libraries(cfd_python PRIVATE Python::Module) - endif() - else() - # For non-stable ABI, use the versioned library - target_link_libraries(cfd_python PRIVATE Python::Module) - endif() -endif() - -# Install the extension module in the cfd_python package directory -install(TARGETS cfd_python DESTINATION cfd_python) \ No newline at end of file +# Install the extension module +install(TARGETS cfd_python DESTINATION cfd_python) diff --git a/backups/build-wheels.yml.bak b/backups/build-wheels.yml.bak new file mode 100644 index 0000000..3082e4d --- /dev/null +++ b/backups/build-wheels.yml.bak @@ -0,0 +1,251 @@ +name: Build and Test Wheels + +on: + push: + branches: [main, master] + tags: + - "v*" + pull_request: + branches: [main, master] + release: + types: [published] + # Allow manual triggering or remote triggering via GitHub CLI/API + # (used by cfd repo's version-release.yml via: gh workflow run build-wheels.yml) + workflow_dispatch: + inputs: + cfd_ref: + description: "CFD library ref (tag/branch/commit). Leave empty to auto-detect." + required: false + default: "" + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + # Build only on Linux for now (simplified debugging) + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Determine which version of CFD library to use + # Priority: workflow_dispatch input > latest cfd release tag + # This ensures we always build against stable, tested cfd releases + - name: Determine CFD library version + id: cfd-version + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + # Check for workflow_dispatch input first + if [[ -n "${{ github.event.inputs.cfd_ref }}" ]]; then + echo "ref=${{ github.event.inputs.cfd_ref }}" >> $GITHUB_OUTPUT + echo "Using CFD library ref from input: ${{ github.event.inputs.cfd_ref }}" + else + # Fetch the latest release tag from cfd repo + LATEST_TAG=$(gh api repos/${{ github.repository_owner }}/cfd/releases/latest --jq '.tag_name' 2>/dev/null || echo "") + + if [[ -n "$LATEST_TAG" ]]; then + echo "ref=$LATEST_TAG" >> $GITHUB_OUTPUT + echo "Using latest CFD release: $LATEST_TAG" + else + # Fallback to master if no releases exist + echo "ref=master" >> $GITHUB_OUTPUT + echo "No CFD releases found, falling back to master branch" + fi + fi + + # Checkout the CFD C library into 'src/cfd_lib' subdirectory + # This location is inside the source tree so it's accessible from cibuildwheel + # build environments (including Linux Docker containers where source is mounted) + - name: Checkout CFD C library + uses: actions/checkout@v4 + with: + repository: ${{ github.repository_owner }}/cfd + path: src/cfd_lib + ref: ${{ steps.cfd-version.outputs.ref }} + fetch-depth: 0 + + # Build wheels using cibuildwheel + # Configuration is in pyproject.toml - we only override CFD_ROOT for CI + # (pyproject.toml defaults to ../cfd for local dev, CI uses ./src/cfd_lib) + - name: Build wheels + uses: pypa/cibuildwheel@v2.21.3 + env: + # Override CFD_ROOT to point to the cfd checkout inside the source tree + # All other settings (build flags, before-build, etc.) come from pyproject.toml + CIBW_ENVIRONMENT: "CMAKE_BUILD_TYPE=Release CFD_STATIC_LINK=ON CFD_USE_STABLE_ABI=ON CFD_ROOT=./src/cfd_lib" + # Enable verbose output to see CMake and compiler commands + CIBW_BUILD_VERBOSITY: "3" + + - name: List wheel contents for debugging + shell: bash + run: | + echo "=== Wheels built ===" + ls -la wheelhouse/ + echo "" + echo "=== Inspecting wheel contents ===" + for wheel in wheelhouse/*.whl; do + echo "--- $wheel ---" + python -m zipfile -l "$wheel" | grep -E '\.(so|pyd|dylib)$' || echo "No extension found!" + done + + - uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build sdist + run: pipx run build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + + test_package: + name: Test package installation + needs: [build_wheels] + runs-on: ${{ matrix.os }} + strategy: + matrix: + # Test on Linux with Python 3.8 (matches abi3 wheel) and 3.12 (to verify abi3 compat) + os: [ubuntu-latest] + python-version: ["3.8", "3.12"] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/download-artifact@v4 + with: + name: wheels-${{ matrix.os }} + path: wheelhouse + + - name: List available wheels + shell: bash + run: | + echo "=== Available wheels ===" + ls -la wheelhouse/ + echo "" + echo "=== Python version ===" + python --version + echo "" + echo "=== Wheel contents (looking for .so/.pyd files) ===" + for wheel in wheelhouse/*.whl; do + echo "--- $wheel ---" + python -m zipfile -l "$wheel" | grep -E '\.(so|pyd|dylib)$' || echo "No extension found in wheel!" + done + + - name: Install wheel and test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -v --find-links wheelhouse cfd-python + python -m pip install pytest numpy + + - name: Verify installation + shell: bash + run: | + echo "=== Installed package location ===" + python -c "import cfd_python; print('Package __file__:', cfd_python.__file__)" + echo "" + echo "=== Package directory contents ===" + python -c "import cfd_python, os; pkgdir=os.path.dirname(cfd_python.__file__); print('Contents:', os.listdir(pkgdir)); exts=[f for f in os.listdir(pkgdir) if '.so' in f or '.pyd' in f]; print('Extensions found:', exts)" + echo "" + echo "=== Try importing C extension directly ===" + python -c " + import sys, os + import cfd_python + pkgdir = os.path.dirname(cfd_python.__file__) + print('Package dir:', pkgdir) + print('Has list_solvers:', hasattr(cfd_python, 'list_solvers')) + # Try direct import of the extension + try: + from cfd_python import cfd_python as ext + print('Direct extension import OK') + print('Extension has PyInit:', hasattr(ext, '__name__')) + print('list_solvers available:', hasattr(ext, 'list_solvers')) + except ImportError as e: + print('Direct extension import FAILED:', e) + # Check __all__ + print('__all__:', getattr(cfd_python, '__all__', 'NOT DEFINED')) + print('__version__:', getattr(cfd_python, '__version__', 'NOT DEFINED')) + " + + - name: Run full test suite + run: pytest tests/ -v + + upload_pypi: + name: Upload to PyPI + needs: [build_wheels, build_sdist, test_package] + runs-on: ubuntu-latest + # Publish on: release event, tag push (v*), or workflow_dispatch on a tag + if: | + github.event_name == 'release' || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || + (github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')) + environment: + name: pypi + url: https://pypi.org/p/cfd-python + permissions: + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@v1.12.4 # Pin to specific version for security + with: + packages-dir: dist/ + + upload_test_pypi: + name: Upload to Test PyPI + needs: [build_wheels, build_sdist, test_package] + runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + environment: + name: testpypi + url: https://test.pypi.org/p/cfd-python + permissions: + id-token: write + + steps: + - uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist + merge-multiple: true + + - uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + + - name: Publish to Test PyPI + uses: pypa/gh-action-pypi-publish@v1.12.4 # Pin to specific version for security + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: dist/ diff --git a/backups/pyproject.toml.bak b/backups/pyproject.toml.bak new file mode 100644 index 0000000..76264b3 --- /dev/null +++ b/backups/pyproject.toml.bak @@ -0,0 +1,143 @@ +[build-system] +requires = [ + "scikit-build-core>=0.4.3", + "setuptools-scm>=6.2", +] +build-backend = "scikit_build_core.build" + +[project] +name = "cfd-python" +dynamic = ["version"] +description = "High-performance CFD simulation library with Python bindings" +readme = "README.md" +license = {text = "MIT"} +authors = [ + {name = "CFD Team"}, +] +requires-python = ">=3.8" +keywords = ["cfd", "fluid-dynamics", "simulation", "numerical-methods", "physics"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: C", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Software Development :: Libraries :: Python Modules", + "Operating System :: OS Independent", +] +dependencies = [] + +# TODO: Add project URLs before release +# [project.urls] +# Homepage = "https://github.com/your-org/cfd-python" +# Documentation = "https://cfd-python.readthedocs.io" +# Repository = "https://github.com/your-org/cfd-python.git" +# Issues = "https://github.com/your-org/cfd-python/issues" +# Changelog = "https://github.com/your-org/cfd-python/blob/main/CHANGELOG.md" + +[project.optional-dependencies] +test = [ + "pytest>=7.0", + "pytest-benchmark", + "numpy", +] +docs = [ + "sphinx>=5.0", + "sphinx-rtd-theme", + "myst-parser", +] +dev = [ + "black", + "isort", + "flake8", + "mypy", + "pre-commit", +] +viz = [ + "matplotlib", + "plotly", + "vtk", +] + +[tool.scikit-build] +# Protect the configuration against future changes in scikit-build-core +minimum-version = "0.4" + +# Setuptools-style build caching in a local directory +build-dir = "build/{wheel_tag}" + +# Build stable ABI wheels for CPython 3.8+ +# This creates cp38-abi3 wheels that work with Python 3.8+ +wheel.py-api = "cp38" +wheel.expand-macos-universal-tags = true + +[tool.scikit-build.cmake.define] +CMAKE_BUILD_TYPE = "Release" +CFD_STATIC_LINK = "ON" +CFD_USE_STABLE_ABI = "ON" +# Note: CFD_ROOT is passed via CIBW_ENVIRONMENT since it differs between local dev and CI + +[tool.scikit-build.metadata] +[tool.scikit-build.metadata.version] +provider = "scikit_build_core.metadata.setuptools_scm" + +[tool.cibuildwheel] +# Build wheels for Python 3.8 on Linux only for now (simplified debugging) +# Using cp38 since we're building abi3 wheels with wheel.py-api = "cp38" +build = ["cp38-manylinux_x86_64"] +skip = ["*-musllinux_*", "pp*"] # Skip PyPy and musl Linux + +# Test command to verify wheels work +# First list package contents to debug extension location, then try import +test-command = "python -c \"import os,sys; import importlib.util; spec=importlib.util.find_spec('cfd_python'); print('Package location:', spec.submodule_search_locations); pkgdir=spec.submodule_search_locations[0]; print('Contents:', os.listdir(pkgdir)); exts=[f for f in os.listdir(pkgdir) if f.endswith('.so') or f.endswith('.pyd')]; print('Extensions:', exts); import cfd_python; print('Version:', cfd_python.__version__)\"" + +# Global environment - static linking for self-contained wheels +# CFD_ROOT can be set to override the default ../cfd location +# CFD_USE_STABLE_ABI is enabled for wheel builds to support multiple Python versions +environment = { CMAKE_BUILD_TYPE = "Release", CFD_STATIC_LINK = "ON", CFD_USE_STABLE_ABI = "ON", CFD_ROOT = "../cfd" } + +[tool.cibuildwheel.windows] +# Windows-specific configuration +# Build CFD library first (static), then build Python extension +# Only build 64-bit wheels (AMD64) - skip x86 (32-bit) +archs = ["AMD64"] +before-build = [ + "cmake -S %CFD_ROOT% -B %CFD_ROOT%/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded", + "cmake --build %CFD_ROOT%/build --config Release", +] + +[tool.cibuildwheel.macos] +# macOS-specific configuration +# CFD_ROOT environment variable specifies the CFD library location +before-build = [ + "cmake -S $CFD_ROOT -B $CFD_ROOT/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF", + "cmake --build $CFD_ROOT/build --config Release", +] +# delocate handles any remaining dynamic dependencies (system libs) +repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}" + +[tool.cibuildwheel.linux] +# Linux-specific configuration +# manylinux images already have cmake and gcc installed +# CFD_ROOT environment variable specifies the CFD library location +# -DCMAKE_POSITION_INDEPENDENT_CODE=ON is required for static lib linked into shared object +before-build = [ + "echo 'DEBUG: CFD_ROOT='$CFD_ROOT && echo 'DEBUG: pwd='$(pwd) && ls -la $CFD_ROOT || echo 'CFD_ROOT not found'", + "cmake -S $CFD_ROOT -B $CFD_ROOT/build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON", + "cmake --build $CFD_ROOT/build --config Release", + "echo 'DEBUG: Library built:' && ls -la $CFD_ROOT/build/lib/ || echo 'No lib directory'", +] +# auditwheel handles any remaining dynamic dependencies (system libs) +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] \ No newline at end of file diff --git a/cfd_python/__init__.py b/cfd_python/__init__.py index 1ff4b9f..04ca5ee 100644 --- a/cfd_python/__init__.py +++ b/cfd_python/__init__.py @@ -97,8 +97,27 @@ # Build complete __all__ list __all__ = _CORE_EXPORTS + _solver_constants -except ImportError: - # Development mode - module not yet built - __all__ = _CORE_EXPORTS - if __version__ is None: - __version__ = "0.0.0-dev" \ No newline at end of file +except ImportError as e: + # Check if this is a development environment (source checkout without built extension) + # vs a broken installation (extension exists but fails to load) + import os as _os + _package_dir = _os.path.dirname(__file__) + + # Look for compiled extension files + _extension_exists = any( + f.startswith('cfd_python') and (f.endswith('.pyd') or f.endswith('.so')) + for f in _os.listdir(_package_dir) + ) + + if _extension_exists: + # Extension file exists but failed to load - this is an error + raise ImportError( + f"Failed to load cfd_python C extension: {e}\n" + "The extension file exists but could not be imported. " + "This may indicate a missing dependency or ABI incompatibility." + ) from e + else: + # Development mode - module not yet built + __all__ = _CORE_EXPORTS + if __version__ is None: + __version__ = "0.0.0-dev" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f2ad85a..fb54f4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,14 +35,6 @@ classifiers = [ ] dependencies = [] -# TODO: Add project URLs before release -# [project.urls] -# Homepage = "https://github.com/your-org/cfd-python" -# Documentation = "https://cfd-python.readthedocs.io" -# Repository = "https://github.com/your-org/cfd-python.git" -# Issues = "https://github.com/your-org/cfd-python/issues" -# Changelog = "https://github.com/your-org/cfd-python/blob/main/CHANGELOG.md" - [project.optional-dependencies] test = [ "pytest>=7.0", @@ -68,75 +60,18 @@ viz = [ ] [tool.scikit-build] -# Protect the configuration against future changes in scikit-build-core minimum-version = "0.4" - -# Setuptools-style build caching in a local directory build-dir = "build/{wheel_tag}" - -# Build stable ABI wheels for CPython 3.8+ -wheel.expand-macos-universal-tags = true +wheel.py-api = "cp38" [tool.scikit-build.cmake.define] CMAKE_BUILD_TYPE = "Release" CFD_STATIC_LINK = "ON" +CFD_USE_STABLE_ABI = "ON" -[tool.scikit-build.metadata] [tool.scikit-build.metadata.version] provider = "scikit_build_core.metadata.setuptools_scm" -[tool.cibuildwheel] -# Build wheels for Python 3.8+ -build = ["cp38-*", "cp39-*", "cp310-*", "cp311-*", "cp312-*"] -skip = ["*-musllinux_*", "pp*"] # Skip PyPy and musl Linux - -# Test command to verify wheels work -# Use double quotes for Windows compatibility -test-command = "python -c \"import cfd_python; print(cfd_python.__version__); print(cfd_python.list_solvers()); r=cfd_python.run_simulation(5,5,3); print(len(r))\"" - -# Global environment - static linking for self-contained wheels -# CFD_ROOT can be set to override the default ../cfd location -# CFD_USE_STABLE_ABI is enabled for wheel builds to support multiple Python versions -environment = { CMAKE_BUILD_TYPE = "Release", CFD_STATIC_LINK = "ON", CFD_USE_STABLE_ABI = "ON", CFD_ROOT = "../cfd" } - -[tool.cibuildwheel.windows] -# Windows-specific configuration -# Build CFD library first (static), then build Python extension -# CFD_ROOT is set in environment below -# Only build 64-bit wheels (AMD64) - skip x86 (32-bit) -archs = ["AMD64"] -before-build = [ - "cd %CFD_ROOT% && cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF", - "cd %CFD_ROOT% && cmake --build build --config Release", -] -# No repair needed for statically linked wheels -environment = { CMAKE_BUILD_TYPE = "Release", CFD_STATIC_LINK = "ON", CFD_USE_STABLE_ABI = "ON", CFD_ROOT = "../cfd" } - -[tool.cibuildwheel.macos] -# macOS-specific configuration -# CFD_ROOT environment variable specifies the CFD library location (default: ../cfd) -before-build = [ - "cd ${CFD_ROOT:-../cfd} && cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF", - "cd ${CFD_ROOT:-../cfd} && cmake --build build --config Release", -] -# delocate handles any remaining dynamic dependencies (system libs) -repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} {wheel}" -environment = { CMAKE_BUILD_TYPE = "Release", CFD_STATIC_LINK = "ON", CFD_USE_STABLE_ABI = "ON", CFD_ROOT = "../cfd" } - -[tool.cibuildwheel.linux] -# Linux-specific configuration -before-all = [ - "yum install -y cmake3 gcc-c++ || apt-get update && apt-get install -y cmake g++", -] -# CFD_ROOT environment variable specifies the CFD library location (default: ../cfd) -before-build = [ - "cd ${CFD_ROOT:-../cfd} && cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF", - "cd ${CFD_ROOT:-../cfd} && cmake --build build --config Release", -] -# auditwheel handles any remaining dynamic dependencies (system libs) -repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" -environment = { CMAKE_BUILD_TYPE = "Release", CFD_STATIC_LINK = "ON", CFD_USE_STABLE_ABI = "ON", CFD_ROOT = "../cfd" } - [tool.pytest.ini_options] testpaths = ["tests"] -python_files = ["test_*.py"] \ No newline at end of file +python_files = ["test_*.py"] diff --git a/tests/conftest.py b/tests/conftest.py index cd5afa5..f6d88e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,9 +18,23 @@ def _check_module_import(module_name, error_message): pytest.skip(f"{error_message} (ImportError: {e})") return False -# Verify cfd_python C extension is available +# Verify cfd_python C extension is available and functional try: import cfd_python + # Check if extension actually loaded (not just dev mode stub) + if not hasattr(cfd_python, 'list_solvers'): + # In CI (indicated by CI env var), fail instead of skip + if os.environ.get('CI'): + raise RuntimeError( + "CFD Python C extension not built. " + "The wheel may be missing the compiled extension." + ) + else: + pytest.skip( + "CFD Python C extension not built (development mode). " + "Run 'pip install -e .' to build the extension.", + allow_module_level=True + ) except ImportError as e: error_str = str(e) if "DLL load failed" in error_str or "cannot open shared object" in error_str: @@ -29,4 +43,7 @@ def _check_module_import(module_name, error_message): reason = "CFD Python C extension not built. Run 'pip install -e .' first." else: reason = f"CFD Python import failed: {e}" + # In CI, fail instead of skip for ImportError + if os.environ.get('CI'): + raise RuntimeError(reason) from e pytest.skip(reason, allow_module_level=True) diff --git a/tests/test_import_handling.py b/tests/test_import_handling.py new file mode 100644 index 0000000..089f090 --- /dev/null +++ b/tests/test_import_handling.py @@ -0,0 +1,183 @@ +""" +Tests for import error handling in cfd_python/__init__.py + +These tests verify the behavior when: +1. Extension exists but fails to load (broken installation) +2. No extension exists (development mode) +""" +import os +import sys +import tempfile +import shutil +import pytest + + +class TestImportErrorHandling: + """Test import error handling logic""" + + def test_broken_extension_raises_import_error(self, tmp_path): + """Test that a broken extension file raises ImportError with helpful message""" + # Create a fake package directory with a broken extension + # Use a unique name to avoid conflicts with real cfd_python + fake_package = tmp_path / "fake_cfd_broken" + fake_package.mkdir() + + # Create __init__.py with the same logic as the real one + # but importing from fake_cfd_broken submodule + init_content = ''' +import os as _os + +_CORE_EXPORTS = ["run_simulation", "list_solvers"] +__version__ = None + +try: + from .fake_cfd_broken import run_simulation, list_solvers +except ImportError as e: + _package_dir = _os.path.dirname(__file__) + _extension_exists = any( + f.startswith('fake_cfd_broken') and (f.endswith('.pyd') or f.endswith('.so')) + for f in _os.listdir(_package_dir) + ) + if _extension_exists: + raise ImportError( + f"Failed to load C extension: {e}\\n" + "The extension file exists but could not be imported. " + "This may indicate a missing dependency or ABI incompatibility." + ) from e + else: + __all__ = _CORE_EXPORTS + if __version__ is None: + __version__ = "0.0.0-dev" +''' + (fake_package / "__init__.py").write_text(init_content) + + # Create a fake broken extension file + if sys.platform == "win32": + (fake_package / "fake_cfd_broken.pyd").write_text("not a real extension") + else: + (fake_package / "fake_cfd_broken.so").write_text("not a real extension") + + # Add to path and try to import + sys.path.insert(0, str(tmp_path)) + try: + with pytest.raises(ImportError) as exc_info: + import fake_cfd_broken + + # Verify the error message is helpful + assert "Failed to load C extension" in str(exc_info.value) + assert "missing dependency or ABI incompatibility" in str(exc_info.value) + finally: + sys.path.remove(str(tmp_path)) + # Clean up module cache + if "fake_cfd_broken" in sys.modules: + del sys.modules["fake_cfd_broken"] + + def test_dev_mode_no_extension(self, tmp_path): + """Test that missing extension falls back to dev mode gracefully""" + # Create a fake package directory without extension + fake_package = tmp_path / "fake_cfd_dev" + fake_package.mkdir() + + # Create __init__.py with the same logic + init_content = ''' +import os as _os + +_CORE_EXPORTS = ["run_simulation", "list_solvers"] +__version__ = None +__all__ = [] + +try: + from .cfd_python import run_simulation, list_solvers + __all__ = _CORE_EXPORTS +except ImportError as e: + _package_dir = _os.path.dirname(__file__) + _extension_exists = any( + f.startswith('cfd_python') and (f.endswith('.pyd') or f.endswith('.so')) + for f in _os.listdir(_package_dir) + ) + if _extension_exists: + raise ImportError( + f"Failed to load cfd_python C extension: {e}\\n" + "The extension file exists but could not be imported. " + "This may indicate a missing dependency or ABI incompatibility." + ) from e + else: + __all__ = _CORE_EXPORTS + if __version__ is None: + __version__ = "0.0.0-dev" +''' + (fake_package / "__init__.py").write_text(init_content) + + # Add to path and import + sys.path.insert(0, str(tmp_path)) + try: + import fake_cfd_dev + + # Should have dev version + assert fake_cfd_dev.__version__ == "0.0.0-dev" + # Should have __all__ set + assert fake_cfd_dev.__all__ == ["run_simulation", "list_solvers"] + finally: + sys.path.remove(str(tmp_path)) + if "fake_cfd_dev" in sys.modules: + del sys.modules["fake_cfd_dev"] + + def test_real_module_loads_successfully(self): + """Test that the real cfd_python module loads without error""" + # This verifies the actual installed module works + import cfd_python + + # Should have version + assert hasattr(cfd_python, '__version__') + assert cfd_python.__version__ is not None + + # Should have core functions + assert hasattr(cfd_python, 'list_solvers') + assert callable(cfd_python.list_solvers) + + # list_solvers should return a non-empty list + solvers = cfd_python.list_solvers() + assert isinstance(solvers, list) + assert len(solvers) > 0 + + def test_extension_detection_logic(self, tmp_path): + """Test that extension detection correctly identifies .pyd and .so files""" + test_dir = tmp_path / "test_detection" + test_dir.mkdir() + + # Test with no extension files + files = list(test_dir.iterdir()) + has_extension = any( + f.name.startswith('cfd_python') and (f.name.endswith('.pyd') or f.name.endswith('.so')) + for f in files + ) + assert not has_extension + + # Test with .pyd file + (test_dir / "cfd_python.cp311-win_amd64.pyd").touch() + files = list(test_dir.iterdir()) + has_extension = any( + f.name.startswith('cfd_python') and (f.name.endswith('.pyd') or f.name.endswith('.so')) + for f in files + ) + assert has_extension + + # Clean and test with .so file + (test_dir / "cfd_python.cp311-win_amd64.pyd").unlink() + (test_dir / "cfd_python.cpython-311-x86_64-linux-gnu.so").touch() + files = list(test_dir.iterdir()) + has_extension = any( + f.name.startswith('cfd_python') and (f.name.endswith('.pyd') or f.name.endswith('.so')) + for f in files + ) + assert has_extension + + # Test with unrelated .so file (should not match) + (test_dir / "cfd_python.cpython-311-x86_64-linux-gnu.so").unlink() + (test_dir / "other_module.so").touch() + files = list(test_dir.iterdir()) + has_extension = any( + f.name.startswith('cfd_python') and (f.name.endswith('.pyd') or f.name.endswith('.so')) + for f in files + ) + assert not has_extension diff --git a/tests/test_integration.py b/tests/test_integration.py index 9efcafe..012d697 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -4,8 +4,18 @@ import pytest import cfd_python +# Check if extension is available (not in dev mode) +_EXTENSION_AVAILABLE = hasattr(cfd_python, 'list_solvers') + # Get solver list at module load time for parametrization -_AVAILABLE_SOLVERS = cfd_python.list_solvers() +# Returns empty list if extension not built (dev mode) +_AVAILABLE_SOLVERS = cfd_python.list_solvers() if _EXTENSION_AVAILABLE else [] + +# Skip all tests in this module if extension not available +pytestmark = pytest.mark.skipif( + not _EXTENSION_AVAILABLE, + reason="C extension not built (development mode)" +) class TestIntegration: