diff --git a/.gitignore b/.gitignore index 8469e2e..8ca0d63 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ CMakePresets.json CMakeUserPresets.json +.DS_Store +/build diff --git a/CMake/PackageMacOS.cmake b/CMake/PackageMacOS.cmake new file mode 100644 index 0000000..afeef39 --- /dev/null +++ b/CMake/PackageMacOS.cmake @@ -0,0 +1,63 @@ +# macOS packaging: runs macdeployqt after build, fixes bundled-dylib rpaths, +# and configures CPack to produce a drag-and-drop .dmg installer. + +# Locate macdeployqt relative to where Qt6 was found. +# Qt6_DIR is typically /lib/cmake/Qt6; macdeployqt lives at: +# conda layout → /lib/qt6/bin/macdeployqt +# standard layout → /bin/macdeployqt +find_program(MACDEPLOYQT_EXECUTABLE macdeployqt + HINTS + "${Qt6_DIR}/../../../lib/qt6/bin" + "${Qt6_DIR}/../../../bin" + NO_DEFAULT_PATH +) + +if(NOT MACDEPLOYQT_EXECUTABLE) + message(WARNING "macdeployqt not found — the .app bundle will not be self-contained. " + "Hint: set Qt6_DIR to the cmake directory inside your Qt installation.") + return() +endif() + +message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}") + +# Determine the conda/prefix lib path from CMAKE_PREFIX_PATH (first entry wins). +# This is the rpath that macdeployqt leaves behind in bundled dylibs; we replace +# it with @loader_path so the bundle works on machines without the conda env. +list(GET CMAKE_PREFIX_PATH 0 _prefix) +set(_conda_lib "${_prefix}/lib") + +# After the build, deploy Qt into the .app, fix the remaining rpaths, then +# re-sign the whole bundle with an ad-hoc identity. The signing step is +# required because install_name_tool (used by both macdeployqt and the rpath +# fix) invalidates any existing code signatures, and macOS will SIGKILL the +# process at load time if the signature doesn't match the binary pages. +add_custom_command(TARGET RhizoVisionExplorer POST_BUILD + COMMAND ${MACDEPLOYQT_EXECUTABLE} + "$" + -verbose=1 + -no-strip + -no-codesign + "-libpath=${_conda_lib}" + COMMAND ${CMAKE_COMMAND} + "-DBUNDLE_DIR=$" + "-DCONDA_LIB_PATH=${_conda_lib}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/CMake/fix_bundle_rpaths.cmake" + COMMAND codesign + --force + --deep + --sign - + "$" + COMMENT "Deploying Qt frameworks, fixing bundle rpaths, and ad-hoc signing" + VERBATIM +) + +# CPack: DragNDrop generator produces a .dmg with a symlink to /Applications. +set(CPACK_GENERATOR "DragNDrop") +set(CPACK_PACKAGE_NAME "RhizoVisionExplorer") +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_VENDOR "Oak Ridge National Laboratory") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "RhizoVision Explorer — root image analysis tool") +set(CPACK_DMG_VOLUME_NAME "RhizoVision Explorer ${PROJECT_VERSION}") +set(CPACK_DMG_FORMAT "UDZO") +set(CPACK_PACKAGE_FILE_NAME "RhizoVisionExplorer-${PROJECT_VERSION}-macOS-arm64") +include(CPack) diff --git a/CMake/cvutilInstallTargets.cmake b/CMake/cvutilInstallTargets.cmake index 9e01b54..981c6d7 100644 --- a/CMake/cvutilInstallTargets.cmake +++ b/CMake/cvutilInstallTargets.cmake @@ -163,7 +163,7 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") # Install additional files. install(FILES README.md COPYING CONFIGURATIONS Debug Release DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/RhizoVisionExplorer) - install(FILES + install(FILES licenses/LICENSE_Qt6 licenses/LICENSE_opencv.txt licenses/LICENSE_FFMPEG.txt @@ -176,7 +176,7 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") install(FILES manual/RhizoVisionExplorerManualv2.pdf CONFIGURATIONS Debug Release DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/RhizoVisionExplorer/manual) - install(FILES + install(FILES imageexamples/crowns/crown1.png imageexamples/crowns/crown2.png imageexamples/crowns/crown3.png @@ -185,7 +185,7 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/RhizoVisionExplorer/imageexamples/crowns ) - install(FILES + install(FILES imageexamples/scans/scan1.jpg imageexamples/scans/scan2.jpg imageexamples/scans/scan3.jpg @@ -193,4 +193,53 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") CONFIGURATIONS Debug Release DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/RhizoVisionExplorer/imageexamples/scans ) + +elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") + # Install the .app bundle and the CLI tool + install(TARGETS RhizoVisionExplorer + CONFIGURATIONS Debug Release + BUNDLE DESTINATION . + ) + install(TARGETS rv + CONFIGURATIONS Debug Release + RUNTIME DESTINATION . + ) + + # Resources installed inside the .app bundle + set(_bundle_res "RhizoVisionExplorer.app/Contents/Resources") + + install(FILES README.md COPYING + CONFIGURATIONS Debug Release + DESTINATION "${_bundle_res}" + ) + install(FILES + licenses/LICENSE_Qt6 + licenses/LICENSE_opencv.txt + licenses/LICENSE_FFMPEG.txt + licenses/LICENSE_cvutil + licenses/LICENSE.indicators + licenses/LICENSE.termcolor + CONFIGURATIONS Debug Release + DESTINATION "${_bundle_res}/licenses" + ) + install(FILES manual/RhizoVisionExplorerManualv2.pdf + CONFIGURATIONS Debug Release + DESTINATION "${_bundle_res}/manual" + ) + install(FILES + imageexamples/crowns/crown1.png + imageexamples/crowns/crown2.png + imageexamples/crowns/crown3.png + imageexamples/crowns/wheatcrown_settings.csv + CONFIGURATIONS Debug Release + DESTINATION "${_bundle_res}/imageexamples/crowns" + ) + install(FILES + imageexamples/scans/scan1.jpg + imageexamples/scans/scan2.jpg + imageexamples/scans/scan3.jpg + imageexamples/scans/wheatscan_settings.csv + CONFIGURATIONS Debug Release + DESTINATION "${_bundle_res}/imageexamples/scans" + ) endif() diff --git a/CMake/fix_bundle_rpaths.cmake b/CMake/fix_bundle_rpaths.cmake new file mode 100644 index 0000000..7a6e538 --- /dev/null +++ b/CMake/fix_bundle_rpaths.cmake @@ -0,0 +1,61 @@ +# CMake script to fix rpaths in dylibs bundled by macdeployqt. +# +# macdeployqt rewrites the main binary's LC_RPATH to @executable_path/../Frameworks, +# but the bundled dylibs still carry the original build-tree rpath (e.g. the conda +# env's lib directory). This script replaces that absolute path with @loader_path so +# the dylibs resolve their own dependencies from within the bundle. +# +# Required variables (pass via -D on the command line): +# BUNDLE_DIR — path to the .app bundle +# CONDA_LIB_PATH — the absolute rpath to replace (e.g. /path/to/conda/envs/.../lib) + +if(NOT BUNDLE_DIR) + message(FATAL_ERROR "fix_bundle_rpaths.cmake: BUNDLE_DIR not set") +endif() +if(NOT CONDA_LIB_PATH) + message(FATAL_ERROR "fix_bundle_rpaths.cmake: CONDA_LIB_PATH not set") +endif() + +file(GLOB FRAMEWORK_DYLIBS "${BUNDLE_DIR}/Contents/Frameworks/*.dylib") + +foreach(dylib ${FRAMEWORK_DYLIBS}) + execute_process( + COMMAND otool -l "${dylib}" + OUTPUT_VARIABLE otool_out + ERROR_QUIET + ) + if(otool_out MATCHES "${CONDA_LIB_PATH}") + execute_process( + COMMAND install_name_tool -rpath "${CONDA_LIB_PATH}" "@loader_path" "${dylib}" + RESULT_VARIABLE result + ) + if(NOT result EQUAL 0) + message(WARNING "fix_bundle_rpaths: failed to patch ${dylib}") + else() + message(STATUS "Patched rpath in: ${dylib}") + endif() + endif() +endforeach() + +# Plugins sit two directories deeper (Contents/PlugIns//), so they need +# to reach back up to Contents/Frameworks via @loader_path/../../Frameworks. +file(GLOB_RECURSE PLUGIN_DYLIBS "${BUNDLE_DIR}/Contents/PlugIns/*.dylib") + +foreach(dylib ${PLUGIN_DYLIBS}) + execute_process( + COMMAND otool -l "${dylib}" + OUTPUT_VARIABLE otool_out + ERROR_QUIET + ) + if(otool_out MATCHES "${CONDA_LIB_PATH}") + execute_process( + COMMAND install_name_tool -rpath "${CONDA_LIB_PATH}" "@loader_path/../../Frameworks" "${dylib}" + RESULT_VARIABLE result + ) + if(NOT result EQUAL 0) + message(WARNING "fix_bundle_rpaths: failed to patch plugin ${dylib}") + else() + message(STATUS "Patched rpath in plugin: ${dylib}") + endif() + endif() +endforeach() diff --git a/CMakeLists.txt b/CMakeLists.txt index 91f4917..06a5b64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,18 @@ endif() option(ENABLE_AVX512 "Enable AVX-512 support" OFF) # To be enabled on x86_64 server processors option(ENABLE_AVX2_FMA "Enable AVX2 and FMA support" ON) # Default support for x86_64 desktop processors +# Disable x86-specific SIMD flags on non-x86 architectures (e.g., Apple Silicon arm64) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|ARM64") + if(ENABLE_AVX512) + message(STATUS "Disabling AVX-512: not supported on ${CMAKE_SYSTEM_PROCESSOR}") + set(ENABLE_AVX512 OFF CACHE BOOL "Enable AVX-512 support" FORCE) + endif() + if(ENABLE_AVX2_FMA) + message(STATUS "Disabling AVX2/FMA: not supported on ${CMAKE_SYSTEM_PROCESSOR}") + set(ENABLE_AVX2_FMA OFF CACHE BOOL "Enable AVX2 and FMA support" FORCE) + endif() +endif() + # Detect the OS if(WIN32) message(STATUS "Configuring for Windows") @@ -141,11 +153,12 @@ if(WIN32) endif() # Define the target as an executable. -add_executable(RhizoVisionExplorer WIN32 +# WIN32 suppresses the console window on Windows; MACOSX_BUNDLE creates a .app on macOS. +add_executable(RhizoVisionExplorer WIN32 MACOSX_BUNDLE RhizoVisionExplorer/main.cpp ${SOURCES} ${HEADERS}) -add_executable(rv +add_executable(rv RhizoVisionExplorer/rv.cpp RhizoVisionExplorer/indicators/indicators.hpp ${SOURCES} @@ -154,6 +167,16 @@ add_executable(rv set_target_properties(RhizoVisionExplorer PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) set_target_properties(rv PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX}) +if(APPLE) + set_target_properties(RhizoVisionExplorer PROPERTIES + MACOSX_BUNDLE_BUNDLE_NAME "RhizoVisionExplorer" + MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" + MACOSX_BUNDLE_GUI_IDENTIFIER "gov.ornl.RhizoVisionExplorer" + MACOSX_BUNDLE_INFO_STRING "RhizoVision Explorer ${PROJECT_VERSION}" + ) +endif() + # Add the generated resource .cpp file to the target target_sources(RhizoVisionExplorer PRIVATE ${RESOURCES}) @@ -207,7 +230,7 @@ include(CMake/cvutilInstallTargets.cmake) # Include the CPack module if(CMAKE_SYSTEM_NAME STREQUAL "Windows") include(CMake/PackageWindows.cmake) -else(CMAKE_SYSTEM_NAME STREQUAL "Linux") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") # Check if we are using a conda env or not if (DEFINED ENV{CONDA_PREFIX}) message(STATUS "Using conda environment: $ENV{CONDA_PREFIX} not generating a Debian package.") @@ -216,4 +239,6 @@ else(CMAKE_SYSTEM_NAME STREQUAL "Linux") message(STATUS "Not using conda environment, use CPack to create a Debian package.") include(CMake/PackageLinux.cmake) endif() +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + include(CMake/PackageMacOS.cmake) endif()