diff --git a/.github/workflows/conda-forge.yml b/.github/workflows/conda-forge.yml new file mode 100644 index 0000000..d2cee06 --- /dev/null +++ b/.github/workflows/conda-forge.yml @@ -0,0 +1,81 @@ +name: conda-forge BLAS + +# Builds against conda-forge's BLAS packages, which ship no vendor CMake config +# (Windows) and split CBLAS into libcblas (Linux), as the sparsediffpy +# conda-forge recipe does. + +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + defaults: + run: + shell: bash -el {0} + + steps: + - uses: actions/checkout@v5 + + - uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-version: latest + activate-environment: build + conda-remove-defaults: "true" + + - name: Install conda-forge packages (Linux) + if: runner.os == 'Linux' + run: > + conda install -y cmake ninja c-compiler blas-devel + "libblas=*=*netlib" "libcblas=*=*netlib" + + - name: Install conda-forge packages (Windows) + if: runner.os == 'Windows' + run: conda install -y cmake openblas libopenblas + + - name: Configure (Linux) + if: runner.os == 'Linux' + run: > + cmake -B build -S . -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_PREFIX_PATH="$CONDA_PREFIX" + -DCMAKE_C_FLAGS="-isystem $CONDA_PREFIX/include" + -DBLA_VENDOR=Generic + + - name: Configure (Windows) + if: runner.os == 'Windows' + run: > + cmake -B build -S . + -DCMAKE_PREFIX_PATH="$CONDA_PREFIX/Library" + + - name: Show BLAS selection + run: grep -E '^(BLAS_|CBLAS_LIBRARY|OPENBLAS_|OpenBLAS_DIR)' build/CMakeCache.txt || true + + - name: Build + run: cmake --build build --config Release + + - name: Run tests + run: cd build && ctest -C Release --output-on-failure + + # Like conda-forge's pinning, build against netlib so the binary records + # the switchable libblas.so.3/libcblas.so.3 SONAMEs (the openblas variant + # is a symlink to libopenblas.so.0). + - name: Check libcblas is linked explicitly (Linux) + if: runner.os == 'Linux' + run: | + readelf -d build/all_tests | grep NEEDED + readelf -d build/all_tests | grep -q 'libcblas\.so\.3' + + - name: Re-run tests after switching to OpenBLAS (Linux) + if: runner.os == 'Linux' + run: | + conda install -y "libblas=*=*openblas" "libcblas=*=*openblas" + cd build && ctest -C Release --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f4f961..1a368af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,9 +21,22 @@ else() message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") endif() +# MSVC warnings-as-errors: on for standalone builds, off for Python builds so +# downstream toolchains (e.g. conda-forge) aren't broken by new MSVC warnings +if(SKBUILD) + set(_sde_werror_default OFF) +else() + set(_sde_werror_default ON) +endif() +option(SDE_WARNINGS_AS_ERRORS "Treat MSVC warnings as errors (/WX)" + ${_sde_werror_default}) + # Warning flags (compiler-specific) if(MSVC) - add_compile_options(/W4 /WX) + add_compile_options(/W4) + if(SDE_WARNINGS_AS_ERRORS) + add_compile_options(/WX) + endif() else() add_compile_options( -Wall # Enable most warnings @@ -66,10 +79,36 @@ if(APPLE) find_library(ACCELERATE_FRAMEWORK Accelerate) target_link_libraries(dnlp_diff ${ACCELERATE_FRAMEWORK}) elseif(MSVC) - find_package(OpenBLAS CONFIG REQUIRED) - target_link_libraries(dnlp_diff OpenBLAS::OpenBLAS) + find_package(OpenBLAS CONFIG QUIET) + if(TARGET OpenBLAS::OpenBLAS) + # vcpkg + target_link_libraries(dnlp_diff OpenBLAS::OpenBLAS) + else() + # e.g. conda-forge's openblas ships openblas.lib and + # include/openblas/cblas.h, but no imported target + find_library(OPENBLAS_LIBRARY NAMES openblas libopenblas REQUIRED) + get_filename_component(_sde_openblas_libdir "${OPENBLAS_LIBRARY}" DIRECTORY) + get_filename_component(_sde_openblas_prefix "${_sde_openblas_libdir}" DIRECTORY) + find_path(OPENBLAS_INCLUDE_DIR cblas.h + HINTS "${_sde_openblas_prefix}/include" + PATH_SUFFIXES openblas REQUIRED) + target_link_libraries(dnlp_diff ${OPENBLAS_LIBRARY}) + target_include_directories(dnlp_diff PUBLIC ${OPENBLAS_INCLUDE_DIR}) + endif() else() find_package(BLAS REQUIRED) + # conda-forge splits the CBLAS interface into libcblas next to libblas. Link + # it explicitly (and before BLAS) so the package keeps working when users + # switch BLAS implementations, even if the build-time libblas exports cblas_*. + if(BLAS_LIBRARIES) + list(GET BLAS_LIBRARIES 0 _sde_blas_lib) + get_filename_component(_sde_blas_dir "${_sde_blas_lib}" DIRECTORY) + find_library(CBLAS_LIBRARY NAMES cblas + HINTS "${_sde_blas_dir}" NO_DEFAULT_PATH) + endif() + if(CBLAS_LIBRARY) + target_link_libraries(dnlp_diff ${CBLAS_LIBRARY}) + endif() target_link_libraries(dnlp_diff ${BLAS_LIBRARIES}) endif()