diff --git a/src/build-scripts/gh-win-installdeps.bash b/src/build-scripts/gh-win-installdeps.bash index ec766c532..085f9ff9c 100755 --- a/src/build-scripts/gh-win-installdeps.bash +++ b/src/build-scripts/gh-win-installdeps.bash @@ -21,14 +21,18 @@ export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$DEP_DIR/bin:$VCPKG_INSTALLATION_ROOT/i export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$DEP_DIR/lib:$VCPKG_INSTALLATION_ROOT/installed/x64-windows-release/lib" -if [[ "$PYTHON_VERSION" == "3.7" ]] ; then - export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;/c/hostedtoolcache/windows/Python/3.7.9/x64" - export Python_EXECUTABLE="/c/hostedtoolcache/windows/Python/3.7.9/x64/python.exe" - export PYTHONPATH=$OpenImageIO_ROOT/lib/python${PYTHON_VERSION}/site-packages -elif [[ "$PYTHON_VERSION" == "3.9" ]] ; then +if [[ "$PYTHON_VERSION" == "3.9" ]] ; then export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;/c/hostedtoolcache/windows/Python/3.9.13/x64" export Python_EXECUTABLE="/c/hostedtoolcache/windows/Python/3.9.13/x64/python3.exe" export PYTHONPATH=$OpenImageIO_ROOT/lib/python${PYTHON_VERSION}/site-packages +elif [[ "$PYTHON_VERSION" == "3.12" ]] ; then + export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;/c/hostedtoolcache/windows/Python/3.12.10/x64" + export Python_EXECUTABLE="/c/hostedtoolcache/windows/Python/3.12.10/x64/python3.exe" + export PYTHONPATH=$OpenImageIO_ROOT/lib/python${PYTHON_VERSION}/site-packages +elif [[ "$PYTHON_VERSION" == "3.14" ]] ; then + export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;/c/hostedtoolcache/windows/Python/3.14.3/x64" + export Python_EXECUTABLE="/c/hostedtoolcache/windows/Python/3.14.3/x64/python3.exe" + export PYTHONPATH=$OpenImageIO_ROOT/lib/python${PYTHON_VERSION}/site-packages fi pip install numpy diff --git a/src/cmake/compiler.cmake b/src/cmake/compiler.cmake index e52cd9a6c..0c8bc464e 100644 --- a/src/cmake/compiler.cmake +++ b/src/cmake/compiler.cmake @@ -20,6 +20,7 @@ message (VERBOSE "CMAKE_SYSTEM_VERSION = ${CMAKE_SYSTEM_VERSION}") message (VERBOSE "CMAKE_SYSTEM_PROCESSOR = ${CMAKE_SYSTEM_PROCESSOR}") message (STATUS "CMAKE_CXX_COMPILER = ${CMAKE_CXX_COMPILER}") message (STATUS "CMAKE_CXX_COMPILER_ID = ${CMAKE_CXX_COMPILER_ID}") +message (VERBOSE "CMAKE_CXX_COMPILE_FEATURES = ${CMAKE_CXX_COMPILE_FEATURES}") ########################################################################### @@ -72,6 +73,8 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER MATCHES "[Cc]lan message (VERBOSE "The compiler is Clang: ${CMAKE_CXX_COMPILER_ID} version ${APPLECLANG_VERSION_STRING}") if (APPLECLANG_VERSION_STRING VERSION_LESS 5.0) message (ERROR "Apple clang minimum version is 5.0") + elseif (APPLECLANG_VERSION_STRING VERSION_LESS 10.0) + message (WARNING "Apple clang minimum version is 10.0. Older versions might work, but we don't test or support them.") endif () elseif (CMAKE_CXX_COMPILER_ID MATCHES "IntelLLVM") set (CMAKE_COMPILER_IS_INTELCLANG 1) @@ -85,12 +88,17 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER MATCHES "[Cc]lan message (VERBOSE "The compiler is Clang: ${CMAKE_CXX_COMPILER_ID} version ${CLANG_VERSION_STRING}") if (CLANG_VERSION_STRING VERSION_LESS 5.0) message (ERROR "clang minimum version is 5.0") + elseif (CLANG_VERSION_STRING VERSION_LESS 10.0) + message (WARNING "clang minimum version is 10.0. Older versions might work, but we don't test or support them.") endif () endif () elseif (CMAKE_CXX_COMPILER_ID MATCHES "Intel") set (CMAKE_COMPILER_IS_INTEL 1) message (VERBOSE "Using Intel as the compiler") endif () +if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_CLANG) + set (COMPILER_IS_GCC_OR_ANY_CLANG TRUE) +endif () ########################################################################### @@ -107,12 +115,12 @@ else () endif() option (EXTRA_WARNINGS "Enable lots of extra pedantic warnings" OFF) if (NOT MSVC) - add_compile_options ("-Wall") + add_compile_options (-Wall) if (EXTRA_WARNINGS) - add_compile_options ("-Wextra") + add_compile_options (-Wextra) endif () if (STOP_ON_WARNING) - add_compile_options ("-Werror") + add_compile_options (-Werror) endif () endif () @@ -232,18 +240,31 @@ endif () # logic here makes it work even if the user is unaware of ccache. If it's # not found on the system, it will simply be silently not used. option (USE_CCACHE "Use ccache if found" ON) -find_program (CCACHE_EXE ccache) -if (CCACHE_EXE AND USE_CCACHE) - if (CMAKE_COMPILER_IS_CLANG AND USE_QT AND (NOT DEFINED ENV{CCACHE_CPP2})) - message (STATUS "Ignoring ccache because clang + Qt + env CCACHE_CPP2 is not set") - else () - if (NOT ${CXX_COMPILER_LAUNCHER} MATCHES "ccache") - set (CXX_COMPILER_LAUNCHER ${CCACHE_EXR} ${CXX_COMPILER_LAUNCHER}) - endif () - if (NOT ${C_COMPILER_LAUNCHER} MATCHES "ccache") - set (C_COMPILER_LAUNCHER ${CCACHE_EXR} ${C_COMPILER_LAUNCHER}) +if (USE_CCACHE) + find_program (CCACHE_EXE ccache + PATHS "${PROJECT_SOURCE_DIR}/ext/dist/" + "${PROJECT_SOURCE_DIR}/ext/dist/bin") + if (CCACHE_EXE) + if (CMAKE_COMPILER_IS_CLANG AND USE_QT AND (NOT DEFINED ENV{CCACHE_CPP2})) + message (STATUS "Ignoring ccache because clang + Qt + env CCACHE_CPP2 is not set") + else () + message (STATUS "CMAKE_CXX_COMPILER_LAUNCHER: ${CMAKE_CXX_COMPILER_LAUNCHER}") + + if (NOT CMAKE_CXX_COMPILER_LAUNCHER MATCHES "ccache") + set (CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_EXE}) + message (STATUS "first if CMAKE_CXX_COMPILER_LAUNCHER: ${CMAKE_CXX_COMPILER_LAUNCHER}") + else () + message (STATUS "first else CMAKE_CXX_COMPILER_LAUNCHER: '${CMAKE_CXX_COMPILER_LAUNCHER}'") + endif () + if (NOT CMAKE_C_COMPILER_LAUNCHER MATCHES "ccache") + set (CMAKE_C_COMPILER_LAUNCHER ${CCACHE_EXE}) + endif () + message (STATUS "ccache enabled: ${CCACHE_EXE}") + message (STATUS "CCACHE_DIR env: $ENV{CCACHE_DIR}") + message (STATUS "CMAKE_CXX_COMPILER_LAUNCHER: ${CMAKE_CXX_COMPILER_LAUNCHER}") endif () - message (STATUS "ccache enabled: ${CCACHE_EXE}") + else () + message (STATUS "ccache not found") endif () endif () @@ -256,8 +277,8 @@ endif () # set `-j 1` or CMAKE_BUILD_PARALLEL_LEVEL to 1. option (TIME_COMMANDS "Time each compile and link command" OFF) if (TIME_COMMANDS) - set (CXX_COMPILER_LAUNCHER ${CMAKE_COMMAND} -E time ${CXX_COMPILER_LAUNCHER}) - set (C_COMPILER_LAUNCHER ${CMAKE_COMMAND} -E time ${C_COMPILER_LAUNCHER}) + set (CMAKE_CXX_COMPILER_LAUNCHER ${CMAKE_COMMAND} -E time ${CMAKE_CXX_COMPILER_LAUNCHER}) + set (CMAKE_C_COMPILER_LAUNCHER ${CMAKE_COMMAND} -E time ${CMAKE_C_COMPILER_LAUNCHER}) endif () @@ -296,20 +317,38 @@ endif () # the proper compiler directives added to generate code for those ISA # capabilities. # -set (USE_SIMD "" CACHE STRING "Use SIMD directives (0, sse2, sse3, ssse3, sse4.1, sse4.2, avx, avx2, avx512f, f16c, aes)") +set_cache (USE_SIMD "" "Use SIMD directives (0, sse2, sse3, ssse3, sse4.1, sse4.2, avx, avx2, avx512f, f16c, aes)") set (SIMD_COMPILE_FLAGS "") +message (STATUS "Compiling with SIMD level ${USE_SIMD}") if (NOT USE_SIMD STREQUAL "") - message (STATUS "Compiling with SIMD level ${USE_SIMD}") if (USE_SIMD STREQUAL "0") - set (SIMD_COMPILE_FLAGS ${SIMD_COMPILE_FLAGS} "-DOIIO_NO_SSE=1") + set (SIMD_COMPILE_FLAGS ${SIMD_COMPILE_FLAGS} "-DOIIO_NO_SIMD=1") else () - string (REPLACE "," ";" SIMD_FEATURE_LIST ${USE_SIMD}) + set(_highest_msvc_arch 0) + string (REPLACE "," ";" SIMD_FEATURE_LIST "${USE_SIMD}") foreach (feature ${SIMD_FEATURE_LIST}) message (VERBOSE "SIMD feature: ${feature}") if (MSVC OR CMAKE_COMPILER_IS_INTEL) - set (SIMD_COMPILE_FLAGS ${SIMD_COMPILE_FLAGS} "/arch:${feature}") + if (feature STREQUAL "sse2") + list (APPEND SIMD_COMPILE_FLAGS "/D__SSE2__") + endif () + if (feature STREQUAL "sse4.1") + list (APPEND SIMD_COMPILE_FLAGS "/D__SSE2__" "/D__SSE4_1__") + endif () + if (feature STREQUAL "sse4.2") + list (APPEND SIMD_COMPILE_FLAGS "/D__SSE2__" "/D__SSE4_2__") + endif () + if (feature STREQUAL "avx" AND _highest_msvc_arch LESS 1) + set(_highest_msvc_arch 1) + endif () + if (feature STREQUAL "avx2" AND _highest_msvc_arch LESS 2) + set(_highest_msvc_arch 2) + endif () + if (feature STREQUAL "avx512f" AND _highest_msvc_arch LESS 3) + set(_highest_msvc_arch 3) + endif () else () - set (SIMD_COMPILE_FLAGS ${SIMD_COMPILE_FLAGS} "-m${feature}") + list (APPEND SIMD_COMPILE_FLAGS "-m${feature}") endif () if (feature STREQUAL "fma" AND (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_CLANG)) # If fma is requested, for numerical accuracy sake, turn it @@ -319,10 +358,25 @@ if (NOT USE_SIMD STREQUAL "") add_compile_options ("-ffp-contract=off") endif () endforeach() + + # Only add a single /arch flag representing the highest level of support. + if (MSVC OR CMAKE_COMPILER_IS_INTEL) + if (_highest_msvc_arch EQUAL 1) + list (APPEND SIMD_COMPILE_FLAGS "/arch:AVX") + endif () + if (_highest_msvc_arch EQUAL 2) + list (APPEND SIMD_COMPILE_FLAGS "/arch:AVX2") + endif () + if (_highest_msvc_arch EQUAL 3) + list (APPEND SIMD_COMPILE_FLAGS "/arch:AVX512") + endif () + endif () + unset(_highest_msvc_arch) endif () add_compile_options (${SIMD_COMPILE_FLAGS}) endif () + ########################################################################### # Batched SIMD shader execution options. # @@ -395,7 +449,7 @@ endif () ########################################################################### # Sanitizer options # -set (SANITIZE "" CACHE STRING "Build code using sanitizer (address, thread)") +set_cache (SANITIZE "" "Build code using sanitizer (address, thread, undefined)") if (SANITIZE AND (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_CLANG)) message (STATUS "Compiling for sanitizer=${SANITIZE}") string (REPLACE "," ";" SANITIZE_FEATURE_LIST ${SANITIZE}) @@ -433,9 +487,7 @@ if (CLANG_TIDY) DOC "Path to clang-tidy executable") message (STATUS "CLANG_TIDY_EXE ${CLANG_TIDY_EXE}") if (CLANG_TIDY_EXE) - set (CMAKE_CXX_CLANG_TIDY - "${CLANG_TIDY_EXE}" - ) + set (CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") if (CLANG_TIDY_ARGS) list (APPEND CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_ARGS}) endif () @@ -511,7 +563,7 @@ endif () set (EXTRA_CPP_ARGS "" CACHE STRING "Extra C++ command line definitions") if (EXTRA_CPP_ARGS) message (STATUS "Extra C++ args: ${EXTRA_CPP_ARGS}") - add_compile_options ("${EXTRA_CPP_ARGS}") + add_compile_options (${EXTRA_CPP_ARGS}) endif() set (EXTRA_DSO_LINK_ARGS "" CACHE STRING "Extra command line definitions when building DSOs") @@ -519,13 +571,13 @@ set (EXTRA_DSO_LINK_ARGS "" CACHE STRING "Extra command line definitions when bu ########################################################################### # Set the versioning for shared libraries. # -if (${PROJECT_NAME}_SUPPORTED_RELEASE) +if (${PROJECT_NAME}_SUPPORTED_RELEASE AND NOT SKBUILD) # Supported releases guarantee ABI back-compatibility within the release # family, so SO versioning is major.minor. set (SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} CACHE STRING "Set the SO version for dynamic libraries") else () - # Development master makes no ABI stability guarantee, so we make the + # Main development branch makes no ABI stability guarantee, so we make the # SO naming capture down to the major.minor.patch level. set (SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH} CACHE STRING "Set the SO version for dynamic libraries") @@ -537,7 +589,7 @@ message(VERBOSE "Setting SOVERSION to: ${SOVERSION}") # BUILD_SHARED_LIBS, if turned off, will disable building of .so/.dll # dynamic libraries and instead only build static libraries. # -option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) +set_option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) if (NOT BUILD_SHARED_LIBS) add_compile_definitions (${PROJECT_NAME}_STATIC_DEFINE=1) endif () @@ -601,6 +653,11 @@ set (CMAKE_SKIP_BUILD_RPATH FALSE) set (CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) +########################################################################### +# Generate compile_commands.json for use by editors and tools. +set (CMAKE_EXPORT_COMPILE_COMMANDS ON) + + ########################################################################### # Macro to install targets to the appropriate locations. Use this instead # of the install(TARGETS ...) signature. Note that it adds it to the diff --git a/src/cmake/dependency_utils.cmake b/src/cmake/dependency_utils.cmake index 398ad562d..984c78c06 100644 --- a/src/cmake/dependency_utils.cmake +++ b/src/cmake/dependency_utils.cmake @@ -11,7 +11,7 @@ set_option (${PROJECT_NAME}_ALWAYS_PREFER_CONFIG "Prefer a dependency's exported config file if it's available" OFF) set_cache (${PROJECT_NAME}_BUILD_MISSING_DEPS "" - "Try to download and build any of these missing dependencies (or 'all')") + "Try to download and build any of these missing dependencies (or 'all' or 'required')") set_cache (${PROJECT_NAME}_BUILD_LOCAL_DEPS "" "Force local builds of these dependencies if possible (or 'all')") @@ -38,6 +38,15 @@ else () DOC "Should a local dependency build, if necessary, build shared libraries" ADVANCED) endif () +# Search for regular libraries before searching for macOS frameworks. +if (APPLE) + set_cache (CMAKE_FIND_FRAMEWORK LAST + DOC "Set relative priority of finding frameworks vs. regular libraries" ADVANCED) +endif () + +set_option (${PROJECT_NAME}_DEPENDENCY_BUILD_ALLOW_UNVERIFIED_TAGS + "Allow dependency auto-build to use unverified tags -- Dangerous" OFF) + # Track all build deps we find with checked_find_package set (CFP_ALL_BUILD_DEPS_FOUND "") @@ -71,7 +80,18 @@ function (dump_matching_variables pattern) endfunction () -# Helper: Print a report about missing dependencies and give insructions on + +# Utility: if `condition` is true, append `addition` to variable `var` +macro (string_append_if var condition addition) + # message (STATUS "string_append_if ${var} ${condition}='${${condition}}' '${addition}'") + if (${condition}) + string (APPEND ${var} ${addition}) + endif () +endmacro() + + + +# Helper: Print a report about missing dependencies and give instructions on # how to turn on automatic local dependency building. function (print_package_notfound_report) message (STATUS) @@ -84,7 +104,9 @@ function (print_package_notfound_report) list (SORT CFP_EXTERNAL_BUILD_DEPS_FOUND CASE INSENSITIVE) list (REMOVE_DUPLICATES CFP_EXTERNAL_BUILD_DEPS_FOUND) foreach (_pkg IN LISTS CFP_EXTERNAL_BUILD_DEPS_FOUND) - message (STATUS " ${_pkg} ${${_pkg}_VERSION}") + set (_msg "${_pkg} ${${_pkg}_VERSION} ") + string_append_if (_msg ${_pkg}_REQUIRED " (REQUIRED)") + message (STATUS " ${_msg}") endforeach () endif () if (CFP_ALL_BUILD_DEPS_BADVERSION) @@ -92,11 +114,13 @@ function (print_package_notfound_report) list (SORT CFP_ALL_BUILD_DEPS_BADVERSION CASE INSENSITIVE) list (REMOVE_DUPLICATES CFP_ALL_BUILD_DEPS_BADVERSION) foreach (_pkg IN LISTS CFP_ALL_BUILD_DEPS_BADVERSION) + set (_msg "${_pkg}") + string_append_if (_msg ${_pkg}_REQUIRED " (REQUIRED)") + string_append_if (_msg ${_pkg}_NOT_FOUND_EXPLANATION " ${_pkg}_NOT_FOUND_EXPLANATION") if (_pkg IN_LIST CFP_LOCALLY_BUILT_DEPS) - message (STATUS " ${_pkg} ${${_pkg}_NOT_FOUND_EXPLANATION} ${ColorMagenta}(${${_pkg}_VERSION} BUILT LOCALLY)${ColorReset}") - else () - message (STATUS " ${_pkg} ${${_pkg}_NOT_FOUND_EXPLANATION}") + string (APPEND _msg " ${ColorMagenta}(${${_pkg}_VERSION} BUILT LOCALLY)${ColorReset} in ${${_pkg}_build_elapsed_time}s)${ColorReset}") endif () + message (STATUS " ${_msg}") endforeach () endif () if (CFP_ALL_BUILD_DEPS_NOTFOUND) @@ -104,11 +128,13 @@ function (print_package_notfound_report) list (SORT CFP_ALL_BUILD_DEPS_NOTFOUND CASE INSENSITIVE) list (REMOVE_DUPLICATES CFP_ALL_BUILD_DEPS_NOTFOUND) foreach (_pkg IN LISTS CFP_ALL_BUILD_DEPS_NOTFOUND) + set (_msg "${_pkg} ${_${_pkg}_version_range}") + string_append_if (_msg ${_pkg}_REQUIRED " (REQUIRED)") + string_append_if (_msg ${_pkg}_NOT_FOUND_EXPLANATION " ${_pkg}_NOT_FOUND_EXPLANATION") if (_pkg IN_LIST CFP_LOCALLY_BUILT_DEPS) - message (STATUS " ${_pkg} ${_${_pkg}_version_range} ${${_pkg}_NOT_FOUND_EXPLANATION} ${ColorMagenta}(${${_pkg}_VERSION} BUILT LOCALLY)${ColorReset}") - else () - message (STATUS " ${_pkg} ${_${_pkg}_version_range} ${${_pkg}_NOT_FOUND_EXPLANATION}") + string (APPEND _msg " ${ColorMagenta}(${${_pkg}_VERSION} BUILT LOCALLY in ${${_pkg}_build_elapsed_time}s)${ColorReset}") endif () + message (STATUS " ${_msg}") endforeach () endif () if (CFP_LOCALLY_BUILDABLE_DEPS_NOTFOUND OR CFP_LOCALLY_BUILDABLE_DEPS_BADVERSION) @@ -118,6 +144,8 @@ function (print_package_notfound_report) message (STATUS " ${_pkg}") endforeach () message (STATUS "${ColorBoldWhite}To build them automatically if not found, build again with option:${ColorReset}") + message (STATUS " ${ColorBoldGreen}-D${PROJECT_NAME}_BUILD_MISSING_DEPS=required${ColorReset}") + message (STATUS " or") message (STATUS " ${ColorBoldGreen}-D${PROJECT_NAME}_BUILD_MISSING_DEPS=all${ColorReset}") endif () message (STATUS) @@ -281,7 +309,7 @@ macro (checked_find_package pkgname) # cmake_parse_arguments(_pkg # prefix # noValueKeywords: - "REQUIRED;CONFIG;PREFER_CONFIG;DEBUG;NO_RECORD_NOTFOUND;NO_FP_RANGE_CHECK" + "REQUIRED;OPTIONAL;CONFIG;PREFER_CONFIG;DEBUG;NO_RECORD_NOTFOUND;NO_FP_RANGE_CHECK" # singleValueKeywords: "ENABLE;ISDEPOF;VERSION_MIN;VERSION_MAX;RECOMMEND_MIN;RECOMMEND_MIN_REASON;BUILD_LOCAL" # multiValueKeywords: @@ -298,18 +326,27 @@ macro (checked_find_package pkgname) set (${pkgname}_FIND_QUIETLY true) set (${pkgname_upper}_FIND_QUIETLY true) endif () - if ("${pkgname}" IN_LIST ${PROJECT_NAME}_REQUIRED_DEPS OR "ALL" IN_LIST ${PROJECT_NAME}_REQUIRED_DEPS) + if ("${pkgname}" IN_LIST ${PROJECT_NAME}_REQUIRED_DEPS + OR "ALL" IN_LIST ${PROJECT_NAME}_REQUIRED_DEPS + OR "all" IN_LIST ${PROJECT_NAME}_REQUIRED_DEPS) set (_pkg_REQUIRED 1) endif () - if ("${pkgname}" IN_LIST ${PROJECT_NAME}_OPTIONAL_DEPS OR "ALL" IN_LIST ${PROJECT_NAME}_OPTIONAL_DEPS) + if ("${pkgname}" IN_LIST ${PROJECT_NAME}_OPTIONAL_DEPS + OR "ALL" IN_LIST ${PROJECT_NAME}_OPTIONAL_DEPS + OR "all" IN_LIST ${PROJECT_NAME}_OPTIONAL_DEPS) set (_pkg_REQUIRED 0) + set (_pkg_OPTIONAL 1) endif () # string (TOLOWER "${_pkg_BUILD_LOCAL}" _pkg_BUILD_LOCAL) if ("${pkgname}" IN_LIST ${PROJECT_NAME}_BUILD_LOCAL_DEPS + OR ${PROJECT_NAME}_BUILD_LOCAL_DEPS STREQUAL "ALL" OR ${PROJECT_NAME}_BUILD_LOCAL_DEPS STREQUAL "all") set (_pkg_BUILD_LOCAL "always") elseif ("${pkgname}" IN_LIST ${PROJECT_NAME}_BUILD_MISSING_DEPS - OR ${PROJECT_NAME}_BUILD_MISSING_DEPS STREQUAL "all") + OR ${PROJECT_NAME}_BUILD_MISSING_DEPS STREQUAL "ALL" + OR ${PROJECT_NAME}_BUILD_MISSING_DEPS STREQUAL "all" + OR ("required" IN_LIST ${PROJECT_NAME}_BUILD_MISSING_DEPS + AND _pkg_REQUIRED)) set_if_not (_pkg_BUILD_LOCAL "missing") endif () set (${pkgname}_local_build_script "${PROJECT_SOURCE_DIR}/src/cmake/build_${pkgname}.cmake") @@ -336,6 +373,12 @@ macro (checked_find_package pkgname) set (_quietskip true) endif () endif () + if (NOT _enable) + set (_pkg_OPTIONAL 1) + set (_pkg_REQUIRED 0) + message(STATUS "Forcing optional of disabled ${pkgname}") + endif () + set (${pkgname}_REQUIRED ${_pkg_REQUIRED}) set (_config_status "") unset (_${pkgname}_version_range) if (_pkg_BUILD_LOCAL AND NOT _pkg_NO_FP_RANGE_CHECK) @@ -356,7 +399,7 @@ macro (checked_find_package pkgname) # set (${pkgname}_FOUND FALSE) set (${pkgname}_LOCAL_BUILD FALSE) - if (_enable OR _pkg_REQUIRED) + if (_enable OR (_pkg_REQUIRED AND NOT _pkg_OPTIONAL)) # Unless instructed not to, try to find the package externally # installed. if (${pkgname}_FOUND OR ${pkgname_upper}_FOUND OR _pkg_BUILD_LOCAL STREQUAL "always") @@ -426,7 +469,7 @@ macro (checked_find_package pkgname) # ${pkgname}_REFIND_ARGS : additional arguments to pass to find_package if (${pkgname}_REFIND) message (STATUS "Refinding ${pkgname} with ${pkgname}_ROOT=${${pkgname}_ROOT}") - find_package (${pkgname} ${_pkg_UNPARSED_ARGUMENTS} ${${pkgname}_REFIND_ARGS}) + find_package (${pkgname} ${${pkgname}_REFIND_VERSION} REQUIRED ${_pkg_UNPARSED_ARGUMENTS} ${${pkgname}_REFIND_ARGS}) unset (${pkgname}_REFIND) endif() # It's all downhill from here: if we found the package, follow the @@ -483,6 +526,74 @@ endmacro() + +# Function: remove_prefixes_from_variable +# Removes specified directory prefixes from a given environment variable or CMake variable. +# +# Parameters: +# VAR_TYPE - Type of variable to modify: +# 'ENV' for environment variables +# 'CMAKE' for CMake variables +# VAR_NAME - Name of the variable to modify. +# PREFIXES - List of directory prefixes to remove from the variable's value. +# +# Description: +# This function updates the specified variable by removing any paths that start +# with the given prefixes. It is useful for excluding certain directories +# (e.g., Homebrew paths) from environment variables or CMake variables to prevent +# unintended dependencies during the build process. +# +# Usage Example: +# remove_prefixes_from_variable(ENV LD_LIBRARY_PATH "/opt/homebrew" "/usr/local") +# remove_prefixes_from_variable(CMAKE CMAKE_PREFIX_PATH "${HOMEBREW_PREFIXES}") +function(remove_prefixes_from_variable VAR_TYPE VAR_NAME PREFIXES) + # Retrieve the original value based on the variable type + if(VAR_TYPE STREQUAL "ENV") + if(DEFINED ENV{${VAR_NAME}}) + set(_original_value "$ENV{${VAR_NAME}}") + else() + return() + endif() + elseif(VAR_TYPE STREQUAL "CMAKE") + if(DEFINED ${VAR_NAME}) + set(_original_value "${${VAR_NAME}}") + else() + return() + endif() + else() + message(FATAL_ERROR "Invalid VAR_TYPE: ${VAR_TYPE}. Expected 'ENV' or 'CMAKE'.") + endif() + + # Convert the variable value into a list of paths + string(REPLACE ":" ";" _path_list "${_original_value}") + + foreach(_prefix ${PREFIXES}) + # Normalize the prefix path + file(TO_CMAKE_PATH "${_prefix}" _norm_prefix) + foreach(_path IN LISTS _path_list) + # Normalize paths to avoid issues with different formats + file(TO_CMAKE_PATH "${_path}" _norm_path) + # Check if the normalized path starts with the normalized prefix + string(FIND "${_norm_path}" "${_norm_prefix}" _pos) + if(_pos EQUAL 0) + list(REMOVE_ITEM _path_list "${_path}") + endif() + endforeach() + endforeach() + + # Convert the list back to the appropriate separator + string(REPLACE ";" ":" _new_value "${_path_list}") + + # Update the variable based on the variable type + if(VAR_TYPE STREQUAL "ENV") + set(ENV{${VAR_NAME}} "${_new_value}") + message(STATUS "${ColorYellow}Updated environment variable ${VAR_NAME}: ${_new_value}") + else() + set(${VAR_NAME} "${_new_value}" PARENT_SCOPE) + message(STATUS "${ColorYellow}Updated CMake variable ${VAR_NAME}: ${_new_value}") + endif() +endfunction() + # Helper to build a dependency with CMake. Given a package name, git repo and # tag, and optional cmake args, it will clone the repo into the surrounding # project's build area, configures, and build sit, and installs it into a @@ -502,35 +613,92 @@ macro (build_dependency_with_cmake pkgname) # noValueKeywords: "NOINSTALL" # singleValueKeywords: - "GIT_REPOSITORY;GIT_TAG;VERSION" + "GIT_REPOSITORY;GIT_TAG;GIT_COMMIT;VERSION;SOURCE_SUBDIR;QUIET" # multiValueKeywords: "CMAKE_ARGS" # argsToParse: ${ARGN}) message (STATUS "Building local ${pkgname} ${_pkg_VERSION} from ${_pkg_GIT_REPOSITORY}") + string (TIMESTAMP ${pkgname}_build_start_time "%s") + + if(DEFINED ${pkgname}_CMAKELISTS_TEMPLATE_PATH AND ${pkgname}_CMAKELISTS_TEMPLATE_PATH) + message (STATUS "cmakelist template provided on: ${${pkgname}_CMAKELISTS_TEMPLATE_PATH}") + endif() set (${pkgname}_LOCAL_SOURCE_DIR "${${PROJECT_NAME}_LOCAL_DEPS_ROOT}/${pkgname}") set (${pkgname}_LOCAL_BUILD_DIR "${${PROJECT_NAME}_LOCAL_DEPS_ROOT}/${pkgname}-build") set (${pkgname}_LOCAL_INSTALL_DIR "${${PROJECT_NAME}_LOCAL_DEPS_ROOT}/dist") message (STATUS "Downloading local ${_pkg_GIT_REPOSITORY}") - set (_pkg_quiet OUTPUT_QUIET) + unset (${pkgname}_GIT_CLONE_ARGS) + unset (_pkg_exec_quiet) + if (NOT "${_pkg_GIT_TAG}" STREQUAL "") + # If a tag or branch is specified, do a shallow clone for efficiency. + list (APPEND ${pkgname}_GIT_CLONE_ARGS -b ${_pkg_GIT_TAG} --depth 1) + endif () + if (_pkg_QUIET OR "${_pkg_QUIET}" STREQUAL "") + list (APPEND ${pkgname}_GIT_CLONE_ARGS -q ERROR_VARIABLE ${pkgname}_clone_errors) + set (_pkg_exec_quiet OUTPUT_QUIET) + endif () # Clone the repo if we don't already have it find_package (Git REQUIRED) if (NOT IS_DIRECTORY ${${pkgname}_LOCAL_SOURCE_DIR}) + message (STATUS "COMMAND ${GIT_EXECUTABLE} clone ${_pkg_GIT_REPOSITORY} " + "${${pkgname}_LOCAL_SOURCE_DIR} " + "${${pkgname}_GIT_CLONE_ARGS} " + "${_pkg_exec_quiet}") execute_process(COMMAND ${GIT_EXECUTABLE} clone ${_pkg_GIT_REPOSITORY} - -b ${_pkg_GIT_TAG} --depth 1 -q ${${pkgname}_LOCAL_SOURCE_DIR} - ${_pkg_quiet}) + ${${pkgname}_GIT_CLONE_ARGS} + ${_pkg_exec_quiet}) if (NOT IS_DIRECTORY ${${pkgname}_LOCAL_SOURCE_DIR}) message (FATAL_ERROR "Could not download ${_pkg_GIT_REPOSITORY}") endif () endif () - execute_process(COMMAND ${GIT_EXECUTABLE} checkout ${_pkg_GIT_TAG} - WORKING_DIRECTORY ${${pkgname}_LOCAL_SOURCE_DIR} - ${_pkg_quiet}) + # Checkout and verify the source against the expected commit hash to + # guard against tag tampering in upstream repositories. + if (${PROJECT_NAME}_DEPENDENCY_BUILD_ALLOW_UNVERIFIED_TAGS + AND NOT "${_pkg_GIT_TAG}" STREQUAL "") + # Special case for CI bleeding edge test, which sets + # ${PROJECT_NAME}_DEPENDENCY_BUILD_ALLOW_UNVERIFIED_TAGS to force + # the unsafe practice of allowing main/master testing. + execute_process(COMMAND ${GIT_EXECUTABLE} checkout ${_pkg_GIT_TAG} + WORKING_DIRECTORY ${${pkgname}_LOCAL_SOURCE_DIR} + ${_pkg_exec_quiet}) + elseif (NOT "${_pkg_GIT_TAG}" STREQUAL "" AND NOT "${_pkg_GIT_COMMIT}" STREQUAL "") + # Both tag and commit: checkout tag, verify it matches expected commit. + execute_process(COMMAND ${GIT_EXECUTABLE} checkout ${_pkg_GIT_TAG} + WORKING_DIRECTORY ${${pkgname}_LOCAL_SOURCE_DIR} + ${_pkg_exec_quiet}) + execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY ${${pkgname}_LOCAL_SOURCE_DIR} + OUTPUT_VARIABLE _pkg_actual_commit + OUTPUT_STRIP_TRAILING_WHITESPACE) + if (NOT "${_pkg_actual_commit}" STREQUAL "${_pkg_GIT_COMMIT}") + message (FATAL_ERROR + "${pkgname}: Tag ${_pkg_GIT_TAG} resolved to commit " + "${_pkg_actual_commit}, but expected ${_pkg_GIT_COMMIT}. " + "This may indicate the tag was tampered with or moved.") + endif () + elseif (NOT "${_pkg_GIT_COMMIT}" STREQUAL "") + # Only commit hash specified: checkout that commit directly. + execute_process(COMMAND ${GIT_EXECUTABLE} checkout ${_pkg_GIT_COMMIT} + WORKING_DIRECTORY ${${pkgname}_LOCAL_SOURCE_DIR} + ${_pkg_exec_quiet}) + elseif (NOT "${_pkg_GIT_TAG}" STREQUAL "") + # Only tag, no commit pin — warn about missing verification. + message (WARNING + "${pkgname}: No GIT_COMMIT specified to verify tag ${_pkg_GIT_TAG}. " + "Consider pinning a commit hash for supply chain security.") + execute_process(COMMAND ${GIT_EXECUTABLE} checkout ${_pkg_GIT_TAG} + WORKING_DIRECTORY ${${pkgname}_LOCAL_SOURCE_DIR} + ${_pkg_exec_quiet}) + else () + message (FATAL_ERROR + "${pkgname}: Neither GIT_TAG nor GIT_COMMIT was specified.") + endif () # Configure the package if (${PROJECT_NAME}_DEPENDENCY_BUILD_VERBOSE) @@ -546,28 +714,52 @@ macro (build_dependency_with_cmake pkgname) ) endif () + + # if a CMakeLists.txt path is specified, add it to the repository. This will replace existing ones + # this should be set before calling the macro + if(DEFINED ${pkgname}_CMAKELISTS_TEMPLATE_PATH AND NOT "${${pkgname}_CMAKELISTS_TEMPLATE_PATH}" STREQUAL "") + message(STATUS "Adding custom CMakeLists.txt for ${pkgname}") + configure_file("${${pkgname}_CMAKELISTS_TEMPLATE_PATH}" + "${${pkgname}_LOCAL_SOURCE_DIR}/${_pkg_SOURCE_SUBDIR}/CMakeLists.txt" + @ONLY) + endif() + + # Make sure to inherit CMAKE_IGNORE_PATH + set(_pkg_CMAKE_ARGS ${_pkg_CMAKE_ARGS} ${_pkg_CMAKE_ARGS}) + if (CMAKE_IGNORE_PATH) + string(REPLACE ";" "\\;" CMAKE_IGNORE_PATH_ESCAPED "${CMAKE_IGNORE_PATH}") + list(APPEND _pkg_CMAKE_ARGS "-DCMAKE_IGNORE_PATH=${CMAKE_IGNORE_PATH_ESCAPED}") + endif() + + # Pass along any CMAKE_MSVC_RUNTIME_LIBRARY + if (WIN32 AND CMAKE_MSVC_RUNTIME_LIBRARY) + list (APPEND _pkg_CMAKE_ARGS -DCMAKE_MSVC_RUNTIME_LIBRARY=${CMAKE_MSVC_RUNTIME_LIBRARY}) + endif () + execute_process (COMMAND ${CMAKE_COMMAND} # Put things in our special local build areas - -S ${${pkgname}_LOCAL_SOURCE_DIR} + -S ${${pkgname}_LOCAL_SOURCE_DIR}/${_pkg_SOURCE_SUBDIR} -B ${${pkgname}_LOCAL_BUILD_DIR} -DCMAKE_INSTALL_PREFIX=${${pkgname}_LOCAL_INSTALL_DIR} # Same build type as us -DCMAKE_BUILD_TYPE=${${PROJECT_NAME}_DEPENDENCY_BUILD_TYPE} + -DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER} + -DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER} # Shhhh -DCMAKE_MESSAGE_INDENT=" " -DCMAKE_COMPILE_WARNING_AS_ERROR=OFF ${_pkg_cmake_verbose} # Build args passed by caller ${_pkg_CMAKE_ARGS} - ${pkg_quiet} + ${_pkg_exec_quiet} ) # Build the package execute_process (COMMAND ${CMAKE_COMMAND} --build ${${pkgname}_LOCAL_BUILD_DIR} --config ${${PROJECT_NAME}_DEPENDENCY_BUILD_TYPE} - ${pkg_quiet} + ${_pkg_exec_quiet} ) # Install the project, unless instructed not to do so @@ -576,11 +768,14 @@ macro (build_dependency_with_cmake pkgname) --build ${${pkgname}_LOCAL_BUILD_DIR} --config ${${PROJECT_NAME}_DEPENDENCY_BUILD_TYPE} --target install - ${pkg_quiet} + ${_pkg_exec_quiet} ) set (${pkgname}_ROOT ${${pkgname}_LOCAL_INSTALL_DIR}) list (APPEND CMAKE_PREFIX_PATH ${${pkgname}_LOCAL_INSTALL_DIR}) endif () + string (TIMESTAMP ${pkgname}_build_end_time "%s") + math (EXPR ${pkgname}_build_elapsed_time "${${pkgname}_build_end_time} - ${${pkgname}_build_start_time}") + # message (STATUS "Build time of ${${pkgname}_build_elapsed_time}s") endmacro () @@ -595,7 +790,7 @@ macro (install_local_dependency_libs pkgname libname) "${${pkgname}_LOCAL_INSTALL_DIR}/lib/*${libname}*" "${${pkgname}_LOCAL_INSTALL_DIR}/lib/${${PROJECT_NAME}_DEPENDENCY_BUILD_TYPE}/*${libname}*" ) - install (FILES ${_lib_files} TYPE LIB) + install (FILES ${_lib_files} TYPE LIB COMPONENT user) # message("${pkgname}_LOCAL_INSTALL_DIR = ${${pkgname}_LOCAL_INSTALL_DIR}") # message(" lib files = ${_lib_files}") if (WIN32) @@ -605,7 +800,7 @@ macro (install_local_dependency_libs pkgname libname) "${${pkgname}_LOCAL_INSTALL_DIR}/bin/${${PROJECT_NAME}_DEPENDENCY_BUILD_TYPE}/*${libname}*.dll" ) # message(" dll files = ${_lib_files}") - install (FILES ${_lib_files} TYPE BIN) + install (FILES ${_lib_files} TYPE BIN COMPONENT user) endif () unset (_lib_files) endmacro ()