diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d1dc8f3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,339 @@ +# GitHub Actions CI for GWmodelS (QGIS 3.28 LTR secondary development) +# +# Targets: +# - Windows (x64) +# - macOS (Intel, x86_64) +# - macOS (Apple Silicon, arm64) +# +# Pipeline: +# 1. Install the libgwmodel kernel (https://github.com/GWmodel-Lab/libgwmodel.git) +# 2. Create a conda environment: qgis=3.28, armadillo, catch2, openblas, nomkl +# 3. Patch the missing qgswebframe.h header from the QGIS source tree +# 4. Configure + build GWmodelS +# 5. Package with CPack (INSTALL/CPack rules live in src/CMakeLists.txt) +# - macOS: ad-hoc sign the .app before packaging +# +name: CI + +on: + push: + branches: + - master + - feat/ci + - 'ci/*' + tags: + - '**' + pull_request: + branches: + - master + +permissions: + contents: read + +env: + CONDA_ENV: gwmodels + QGIS_VER: '3.28' + # qgswebframe.h is fetched from the QGIS 3.28 LTR source tree + QGIS_HEADER_REF: final-3_28_1 + +jobs: + # --------------------------------------------------------------------------- + # Windows + # --------------------------------------------------------------------------- + build-windows: + name: Windows (x64) + runs-on: windows-2022 + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Set up Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + auto-update-conda: true + python-version: '3.10' + channels: conda-forge, defaults + channel-priority: flexible + activate-environment: ${{ env.CONDA_ENV }} + + # qgis/armadillo/catch2/openblas/gsl come from conda-forge; nomkl (no + # MKL) is only available in the 'defaults' channel. + # + # NOTE: the conda-forge gsl package ships no CMake config files, so + # libgwmodel's `find_package(GSL)` relies on CMake's built-in FindGSL, + # which only provides the GSL::gsl/GSL::gslcblas targets since CMake 3.27. + # Install a recent cmake from conda-forge to guarantee that. + - name: Install conda dependencies + shell: pwsh + run: | + conda install -n $env:CONDA_ENV -q -y ` + -c conda-forge qgis=$env:QGIS_VER armadillo catch2 openblas gsl cmake nomkl + conda list -n $env:CONDA_ENV -q + + - name: Clone libgwmodel + shell: pwsh + run: | + git clone --depth 1 https://github.com/GWmodel-Lab/libgwmodel.git + + # libgwmodel is the spatial-statistics kernel required by GWmodelS. + # Build first (the conda deps above are already in place). + - name: Install libgwmodel kernel + shell: pwsh + run: | + cmake -S . -B build ` + -A x64 ` + -DCMAKE_INSTALL_PREFIX="$env:CONDA_PREFIX\Library" ` + -DCMAKE_PREFIX_PATH="$env:CONDA_PREFIX\Library" ` + -DWITH_TESTS=OFF + cmake --build build --config Release + cmake --install build --config Release + working-directory: ${{ github.workspace }}/libgwmodel + + # The conda QGIS package is missing qgswebframe.h; download it from the + # QGIS 3.28 LTR source tree and drop it into the include directory. + - name: Patch missing qgswebframe.h + shell: pwsh + run: | + $QGIS_INC = "$env:CONDA_PREFIX\Library\include" + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/qgis/QGIS/$env:QGIS_HEADER_REF/src/core/qgswebframe.h" ` + -OutFile "$QGIS_INC\qgswebframe.h" + + - name: Enable MSVC toolchain + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Configure (cmake) + shell: pwsh + run: | + $env:PATH = "$env:CONDA_PREFIX\Library\bin;$env:PATH" + $env:LIB_DIR = "$env:CONDA_PREFIX\Library" + $env:INCLUDE = "$env:CONDA_PREFIX\Library\include;$env:INCLUDE" + cmake -S . -B build ` + -DCMAKE_PREFIX_PATH="$env:CONDA_PREFIX\Library" ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_INSTALL_PREFIX="build\staged" ` + -DDATADIR="share" + + - name: Build GWmodelS + shell: pwsh + run: | + $env:PATH = "$env:CONDA_PREFIX\Library\bin;$env:PATH" + cmake --build build --config Release --target GWmodelS + + # CPack configuration is provided by the INSTALL/CPack section in + # src/CMakeLists.txt; reconfigure is a no-op and regenerates the + # CPackConfig.cmake before producing the NSIS64 installer. + - name: Package with CPack + shell: pwsh + run: | + $env:PATH = "$env:CONDA_PREFIX\Library\bin;$env:PATH" + $env:LIB_DIR = "$env:CONDA_PREFIX\Library" + $env:INCLUDE = "$env:CONDA_PREFIX\Library\include;$env:INCLUDE" + cpack -G NSIS64 -C Release + working-directory: ${{ github.workspace }}/build + + - name: Upload package + uses: actions/upload-artifact@v4 + with: + name: GWmodelS-Windows-x64 + path: build/*.exe + if-no-files-found: error + + # --------------------------------------------------------------------------- + # macOS (Intel + Apple Silicon) + # --------------------------------------------------------------------------- + build-macos: + name: macOS (${{ matrix.arch }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-15-intel # Intel + arch: x64 + - os: macos-15 # Apple Silicon + arch: arm64 + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + # Non-QGIS build deps via Homebrew. Note: the brew formula for + # llvm-openmp is called 'libomp'. Homebrew's gsl formula ships + # GSLConfig.cmake, which satisfies libgwmodel's `find_package(GSL)`. + - name: Install brew dependencies + shell: bash + run: | + brew install --quiet armadillo openblas catch2 libomp gsl cmake + + - name: Set up Miniconda + uses: conda-incubator/setup-miniconda@v3 + with: + auto-update-conda: true + python-version: '3.10' + channels: conda-forge, defaults + channel-priority: flexible + activate-environment: ${{ env.CONDA_ENV }} + + # QGIS itself is installed via conda-forge; all other (non-QGIS) build + # dependencies are provided by Homebrew instead, to keep the QGIS + # toolchain self-contained inside the conda environment. + # + # gsl is installed via Homebrew because libgwmodel uses + # `find_package(GSL)` and requires a GSL CMake package config. + - name: Install conda dependencies (QGIS) + shell: bash -l {0} + run: | + conda install -n "$CONDA_ENV" -q -y \ + -c conda-forge qgis=$QGIS_VER qtkeychain=0.14.1 nomkl + conda list -n "$CONDA_ENV" -q + + - name: Clone libgwmodel + run: | + git clone --depth 1 https://github.com/GWmodel-Lab/libgwmodel.git + + - name: Install libgwmodel kernel + shell: bash -l {0} + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$CONDA_PREFIX" \ + -DENABLE_OpenMP=OFF \ + -DWITH_TESTS=OFF + cmake --build build --config Release + cmake --install build + working-directory: ${{ github.workspace }}/libgwmodel + + # qgswebframe.h is missing from the conda QGIS package; fetch it from the + # QGIS 3.28 LTR source tree into the conda include directory. + - name: Patch missing qgswebframe.h + shell: bash -l {0} + run: | + QGIS_INC="$CONDA_PREFIX/include/qgis" + curl -fsSL -o "$QGIS_INC/qgswebframe.h" \ + "https://raw.githubusercontent.com/qgis/QGIS/$QGIS_HEADER_REF/src/core/qgswebframe.h" + + - name: Configure (cmake) + shell: bash -l {0} + run: | + QGIS_INC="$CONDA_PREFIX/include/qgis" + cmake -S . -B build \ + -DENABLE_OpenMP=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$CONDA_PREFIX;$(brew --prefix)" \ + -DQGIS_PREFIX_PATH="$CONDA_PREFIX" \ + -DQGIS_GUI_INCLUDE_DIR="$QGIS_INC" \ + -DQGIS_ANALYSIS_INCLUDE_DIR="$QGIS_INC" + + - name: Build GWmodelS + shell: bash -l {0} + run: | + cmake --build build --config Release --target GWmodelS + + # CPack runs the complete install pipeline: deploy Qt, collect and fix + # runtime dependencies, ad-hoc sign the app, then create the DMG. + - name: Package with CPack + shell: bash -l {0} + run: | + cpack --config CPackConfig.cmake -C Release -G DragNDrop + working-directory: ${{ github.workspace }}/build + + # Both matrix jobs produce the same CPack base name. Add the architecture + # so both DMGs can coexist as assets on the same GitHub Release. + - name: Add architecture to package filename + shell: bash + run: | + for package in build/*.dmg; do + mv "$package" "${package%.dmg}-${{ matrix.arch }}.dmg" + done + + - name: Upload package + uses: actions/upload-artifact@v4 + with: + name: GWmodelS-macOS-${{ matrix.arch }} + path: build/*.dmg + if-no-files-found: error + + # --------------------------------------------------------------------------- + # GitHub Release (tag pushes only) + # --------------------------------------------------------------------------- + release: + name: Publish GitHub Release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + needs: + - build-windows + - build-macos + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download installation packages + uses: actions/download-artifact@v4 + with: + pattern: GWmodelS-* + path: release-assets + merge-multiple: true + + - name: Locate installation packages + id: packages + shell: bash + run: | + windows_path=$(find release-assets -maxdepth 1 -type f -name '*.exe' -print -quit) + macos_x64_path=$(find release-assets -maxdepth 1 -type f -name '*-x64.dmg' -print -quit) + macos_arm64_path=$(find release-assets -maxdepth 1 -type f -name '*-arm64.dmg' -print -quit) + + test -n "$windows_path" + test -n "$macos_x64_path" + test -n "$macos_arm64_path" + + echo "windows_path=$windows_path" >> "$GITHUB_OUTPUT" + echo "windows_name=$(basename "$windows_path")" >> "$GITHUB_OUTPUT" + echo "macos_x64_path=$macos_x64_path" >> "$GITHUB_OUTPUT" + echo "macos_x64_name=$(basename "$macos_x64_path")" >> "$GITHUB_OUTPUT" + echo "macos_arm64_path=$macos_arm64_path" >> "$GITHUB_OUTPUT" + echo "macos_arm64_name=$(basename "$macos_arm64_path")" >> "$GITHUB_OUTPUT" + + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + tag_name: ${{ github.ref_name }} + release_name: GWmodelS ${{ github.ref_name }} + body: Automated installation packages for GWmodelS ${{ github.ref_name }}. + draft: false + prerelease: false + + - name: Upload Windows x64 package + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.packages.outputs.windows_path }} + asset_name: ${{ steps.packages.outputs.windows_name }} + asset_content_type: application/vnd.microsoft.portable-executable + + - name: Upload macOS x64 package + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.packages.outputs.macos_x64_path }} + asset_name: ${{ steps.packages.outputs.macos_x64_name }} + asset_content_type: application/x-apple-diskimage + + - name: Upload macOS arm64 package + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ${{ steps.packages.outputs.macos_arm64_path }} + asset_name: ${{ steps.packages.outputs.macos_arm64_name }} + asset_content_type: application/x-apple-diskimage diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c3439a3..3ea3202 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -242,7 +242,11 @@ endif(WIN32) if(UNIX AND NOT CYGWIN) if(APPLE) add_executable(GWmodelS MACOSX_BUNDLE ${SOURCES} ${HEADERS} ${FORMS_H} ${app_MACICON} ${GWMODEL_ICONS}) - set_target_properties(GWmodelS PROPERTIES FRAMEWORK TRUE MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/macOS/Info.plist.in) + set_target_properties(GWmodelS PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/macOS/Info.plist.in + RESOURCE "${app_MACICON}" + ) else(APPLE) set(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -no-pie") add_executable(GWmodelS ${SOURCES} ${HEADERS} ${FORMS_H} ${app_WINRC} ${GWMODEL_ICONS}) @@ -285,3 +289,110 @@ target_link_libraries(GWmodelS ${GWMODELCUDA_LIBRARIES} ) endif(ENABLE_CUDA) + +# INSTALL + +if (WIN32) +get_filename_component(qgis_dir ${QGIS_INCLUDE_DIR} DIRECTORY) +install(TARGETS GWmodelS + COMPONENT Runtime + RUNTIME_DEPENDENCIES DIRECTORIES "${qgis_dir}/bin" POST_EXCLUDE_REGEXES ".*system32.*" POST_INCLUDE_REGEXES "libomp140.x86_64.dll" + DESTINATION . +) +get_target_property(uic_location Qt5::uic IMPORTED_LOCATION) +get_filename_component( _dir ${uic_location} DIRECTORY) +set(deployqt "${_dir}/windeployqt.exe") +install(CODE "set(deployqt \"${deployqt}\")" COMPONENT Runtime) +install(CODE [===[ +execute_process(COMMAND "${deployqt}" "${CMAKE_INSTALL_PREFIX}/GWmodelS.exe") +]===] COMPONENT Runtime) +install(DIRECTORY "${qgis_dir}/resources" DESTINATION .) +install(DIRECTORY "${qgis_dir}/share/proj" DESTINATION "./${DATADIR}") +elseif(APPLE) +install(TARGETS GWmodelS + BUNDLE DESTINATION . COMPONENT Runtime + RESOURCE DESTINATION "./${CMAKE_PROJECT_NAME}.app/Contents/Resources" COMPONENT Runtime +) +get_filename_component(qgis_dir ${QGIS_UI_INCLUDE_DIR} DIRECTORY) +get_filename_component(qgis_dir ${qgis_dir} DIRECTORY) +install(DIRECTORY "${qgis_dir}/share/proj" + DESTINATION "./${CMAKE_PROJECT_NAME}.app/Contents/Resources" + COMPONENT Runtime +) +install(DIRECTORY "${qgis_dir}/share/qgis/resources" + DESTINATION "./${CMAKE_PROJECT_NAME}.app/Contents/Resources" + COMPONENT Runtime +) + +# Locate the deployment tool from the same Qt installation used to build the +# application. The complete bundle is assembled by the install step, so both +# `cmake --install` and CPack follow exactly the same deployment path. +get_target_property(qmake_location Qt5::qmake IMPORTED_LOCATION) +get_filename_component(qt_bin_dir "${qmake_location}" DIRECTORY) +find_program(MACDEPLOYQT_EXECUTABLE + NAMES macdeployqt + HINTS "${qt_bin_dir}" "${QGIS_PREFIX_PATH}/bin" + NO_DEFAULT_PATH +) +if(NOT MACDEPLOYQT_EXECUTABLE) + message(FATAL_ERROR "macdeployqt was not found next to the Qt installation") +endif() + +install(CODE "set(APP_BUNDLE \"\${CMAKE_INSTALL_PREFIX}/${PROJECT_NAME}.app\")" COMPONENT Runtime) +install(CODE "set(MACDEPLOYQT_EXECUTABLE \"${MACDEPLOYQT_EXECUTABLE}\")" COMPONENT Runtime) +install(CODE "set(QGIS_LIBRARY_DIR \"${QGIS_PREFIX_PATH}/lib\")" COMPONENT Runtime) +install(CODE [===[ + execute_process( + COMMAND "${MACDEPLOYQT_EXECUTABLE}" "${APP_BUNDLE}" -always-overwrite + RESULT_VARIABLE _macdeployqt_result + ) + if(NOT _macdeployqt_result EQUAL 0) + message(FATAL_ERROR "macdeployqt failed with exit code ${_macdeployqt_result}") + endif() + + include(BundleUtilities) + + # In particular, this search path lets BundleUtilities resolve conda + # libraries referenced through @rpath, including PDAL's libpdal_util. + set(_bundle_search_dirs "${QGIS_LIBRARY_DIR}") + if(DEFINED ENV{CONDA_PREFIX}) + list(APPEND _bundle_search_dirs "$ENV{CONDA_PREFIX}/lib") + endif() + + fixup_bundle("${APP_BUNDLE}" "" "${_bundle_search_dirs}") + + # BundleUtilities and macdeployqt modify Mach-O files, so signing must be + # the final operation performed on the assembled application. + execute_process( + COMMAND codesign --force --deep --sign - "${APP_BUNDLE}" + RESULT_VARIABLE _codesign_result + ) + if(NOT _codesign_result EQUAL 0) + message(FATAL_ERROR "ad-hoc codesign failed with exit code ${_codesign_result}") + endif() + + execute_process( + COMMAND codesign --verify --deep --strict --verbose=2 "${APP_BUNDLE}" + RESULT_VARIABLE _codesign_verify_result + ) + if(NOT _codesign_verify_result EQUAL 0) + message(FATAL_ERROR "codesign verification failed with exit code ${_codesign_verify_result}") + endif() +]===] COMPONENT Runtime) +endif() + +# CPack +set(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME}) +set(CPACK_PACKAGE_VENDOR "GWmodel Lab") +set(CPACK_PACKAGE_VERSION ${CMAKE_PROJECT_VERSION}) +set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/icons/GWmodelS.ico") +set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CMAKE_PROJECT_NAME}) +set(CPACK_PACKAGE_EXECUTABLES "GWmodelS;GWmodelS") +set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") +if(APPLE) + set(CPACK_GENERATOR "DragNDrop") + set(CPACK_DMG_VOLUME_NAME "GWmodelS") + set(CPACK_DMG_FORMAT "UDZO") + set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}-${CMAKE_PROJECT_VERSION}-macOS") +endif() +include(CPack)