diff --git a/CMakeLists.txt b/CMakeLists.txt index c2b844e..b2f692d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,376 @@ set(PACKAGE_PROJECT_ROOT_PATH CACHE INTERNAL "The path to the PackageProject directory" ) +function(_package_project_parse_version VERSION MAJOR_VAR MINOR_VAR PATCH_VAR TWEAK_VAR) + # clear previous matches + unset(CMAKE_MATCH_1) + unset(CMAKE_MATCH_3) + unset(CMAKE_MATCH_5) + unset(CMAKE_MATCH_7) + + string(REGEX MATCH "^([0-9]+)(\\.([0-9]+))?(\\.([0-9]+))?(\\.([0-9]+))?$" _ "${VERSION}") + + set(MAJOR ${CMAKE_MATCH_1}) + set(MINOR ${CMAKE_MATCH_3}) + set(PATCH ${CMAKE_MATCH_5}) + set(TWEAK ${CMAKE_MATCH_7}) + + if(NOT DEFINED MAJOR) + set(MAJOR "0") + endif() + if(NOT DEFINED MINOR) + set(MINOR "0") + endif() + if(NOT DEFINED PATCH) + set(PATCH "0") + endif() + if(NOT DEFINED TWEAK) + set(TWEAK "0") + endif() + + set(${MAJOR_VAR} + "${MAJOR}" + PARENT_SCOPE + ) + set(${MINOR_VAR} + "${MINOR}" + PARENT_SCOPE + ) + set(${PATCH_VAR} + "${PATCH}" + PARENT_SCOPE + ) + set(${TWEAK_VAR} + "${TWEAK}" + PARENT_SCOPE + ) +endfunction() + +function(_package_project_resolve_target TARGET UNDERLYING_TARGET_VAR ALIAS_VAR) + if(NOT TARGET ${TARGET}) + message(FATAL_ERROR "Expected '${TARGET}' to be a valid target, but it isn't") + endif() + + get_target_property(ALIASED_TO "${TARGET}" ALIASED_TARGET) + + if(ALIASED_TO) + set(${UNDERLYING_TARGET_VAR} + "${ALIASED_TO}" + PARENT_SCOPE + ) + set(${ALIAS_VAR} + "${TARGET}" + PARENT_SCOPE + ) + else() + set(${UNDERLYING_TARGET_VAR} + "${TARGET}" + PARENT_SCOPE + ) + unset(${ALIAS_VAR} PARENT_SCOPE) + endif() +endfunction() + +function(_package_project_determine_visibility_flag TARGET OUTPUT_VARIABLE) + get_target_property(TARGET_TYPE "${TARGET}" TYPE) + + if("${TARGET_TYPE}" STREQUAL "INTERFACE_LIBRARY") + set(${OUTPUT_VARIABLE} + "INTERFACE" + PARENT_SCOPE + ) + else() + set(${OUTPUT_VARIABLE} + "PUBLIC" + PARENT_SCOPE + ) + endif() +endfunction() + +function(_package_project_normalize_alias_name NAME NAMESPACE TARGET_LIST ALIAS_LIST + OUTPUT_VARIABLE +) + if(TARGET "${NAME}") + _package_project_resolve_target("${NAME}" TARGET_NAME ALIAS_NAME) + + if(ALIAS_NAME AND NAMESPACE) + string(REGEX REPLACE "^${NAMESPACE}" "" ALIAS_NAME "${ALIAS_NAME}") + endif() + + list(FIND TARGET_LIST "${TARGET_NAME}" TARGET_IDX) + + if(TARGET_IDX LESS 0) + set(NAME "${ALIAS_NAME}") + else() + list(GET ALIAS_LIST ${TARGET_IDX} NAME) + + if(NOT NAME) + set(NAME "${ALIAS_NAME}") + endif() + endif() + + if(ALIAS_NAME AND NOT ("${ALIAS_NAME}" STREQUAL "${NAME}")) + message(FATAL_ERROR "Inconsistent alias names: '${ALIAS_NAME}' and '${NAME}'") + endif() + else() + # If NAME is not a target, it can't be part of the target list (as all entries in there are, by + # definition, targets). Looking up in ALIAS_LIST is no good as this would either result in a + # match, in which case NAME already is the normalized alias name. If there is no match the only + # thing we can try to do is to strip the namespace, which we do below anyway. + endif() + + if(NAMESPACE) + string(REGEX REPLACE "^${NAMESPACE}" "" NAME "${NAME}") + endif() + + set(${OUTPUT_VARIABLE} + "${NAME}" + PARENT_SCOPE + ) +endfunction() + +function(_package_project_normalize_target_name NAME NAMESPACE TARGET_LIST ALIAS_LIST + OUTPUT_VARIABLE +) + if(TARGET "${NAME}") + _package_project_resolve_target("${NAME}" TARGET_NAME ALIAS_NAME) + set(NAME "${TARGET_NAME}") + else() + if(NAMESPACE AND NAME MATCHES "^${NAMESPACE}") + # Remove namespace + string(REGEX REPLACE "^${NAMESPACE}" "" NAME "${NAME}") + set(REMOVED_NAMESPACE ON) + endif() + + list(FIND TARGET_LIST "${NAME}" TARGET_IDX) + + if(TARGET_IDX LESS 0) + # Check if the given target + list(FIND ALIAS_LIST "${NAME}" TARGET_IDX) + endif() + + if(TARGET_IDX LESS 0) + # return unchanged + if(NOT REMOVED_NAMESPACE AND NOT TARGET "${NAME}") + # This assumes that everything that started with the given namespace is an internal target + # and is therefore not required to be an existing target (in the current form) + message( + FATAL_ERROR + "Assumed all non-internal target names to be real cmake targets but '${NAME}' isn't" + ) + endif() + else() + # internal target -> normalize by using name of underlying target (as targets may not have an + # alias) + list(GET TARGET_LIST "${TARGET_IDX}" NAME) + endif() + endif() + + set(${OUTPUT_VARIABLE} + "${NAME}" + PARENT_SCOPE + ) +endfunction() + +function( + _package_project_parse_components + COMPONENT_SPECS + NAMESPACE + TARGET_LIST + ALIAS_LIST + OUT_TARGET_LIST_NAME + OUT_ALIAS_LIST_NAME +) + # Component specification has the syntax [AS ] that is, each component is + # given as a target and optionally an alias name (indicated by the "AS" keyword) + + # The input list simply contains all individual words (space-separated) of the component + # specification in a single list. In order to get a list with a single entry per component, we + # merge elements separated by an "AS" into a single component specication (that now may include + # the alias specification). + string(REPLACE ";AS;" "<->" COMPONENT_LIST "${COMPONENT_SPECS}") + + # Work around cmake not being able to add empty strings to empty lists + list(PREPEND ALIAS_LIST "ToBeRemoved") + + foreach(CURRENT_COMPONENT IN LISTS COMPONENT_LIST) + if("${CURRENT_COMPONENT}" MATCHES "([^<]+)<->(.+)") + # Component with alias + set(TARGET_NAME "${CMAKE_MATCH_1}") + set(ALIAS_NAME "${CMAKE_MATCH_2}") + else() + # Component without alias specification -> try to see whether we can derive alias from the + # component name itself + set(TARGET_NAME "${CURRENT_COMPONENT}") + set(ALIAS_NAME "${CURRENT_COMPONENT}") + set(INVALID_ALIAS "${CURRENT_COMPONENT}") + endif() + + _package_project_normalize_target_name( + "${TARGET_NAME}" "${NAMESPACE}" "${TARGET_LIST}" "${ALIAS_LIST}" TARGET_NAME + ) + _package_project_normalize_alias_name( + "${ALIAS_NAME}" "${NAMESPACE}" "${TARGET_LIST}" "${ALIAS_LIST}" ALIAS_NAME + ) + if("${ALIAS_NAME}" STREQUAL "${INVALID_ALIAS}") + # Deriving alias from unmodified component name failed -> there is no alias for this target + set(ALIAS_NAME "") + endif() + + list(APPEND TARGET_LIST "${TARGET_NAME}") + list(APPEND ALIAS_LIST "${ALIAS_NAME}") + endforeach() + + # Remove dummy element + list(POP_FRONT ALIAS_LIST) + + set(${OUT_TARGET_LIST_NAME} + "${TARGET_LIST}" + PARENT_SCOPE + ) + set(${OUT_ALIAS_LIST_NAME} + "${ALIAS_LIST}" + PARENT_SCOPE + ) +endfunction() + +function( + _package_project_parse_dependencies + DEPENDENCIES_SPEC + TARGET_LIST + ALIAS_LIST + DEPENDENCY_LIST + OUT_DEPENDENCY_LIST_NAME + DEPENDENCY_SEPARATOR + NAMESPACE +) + # Dependency specifications follow the syntax ON [... ] + + set(REFERENCED_COMPONENTS "") + set(COMPONENT_DEPENDENCIES "") + + # Parse the specification + list(LENGTH DEPENDENCIES_SPEC LIST_SIZE) + set(IDX 0) + while(IDX LESS LIST_SIZE) + list(GET DEPENDENCIES_SPEC ${IDX} CURRENT_COMPONENT) + math(EXPR IDX "${IDX} + 1") + list(GET DEPENDENCIES_SPEC ${IDX} ON_KEYWORD) + if(NOT ("${ON_KEYWORD}" STREQUAL "ON")) + message( + FATAL_ERROR + "Invalid dependency specification for component '${CURRENT_COMPONENT}' - expected component name to be followed by 'ON'" + ) + endif() + math(EXPR IDX "${IDX} + 1") + + list(SUBLIST DEPENDENCIES_SPEC ${IDX} -1 REMAINING) + list(FIND REMAINING "ON" ON_IDX) + if(ON_IDX LESS 0) + # No more 'ON' keyword found -> there are no additional component dependencies after the + # current one + set(CURRENT_DEPENDENCIES "${REMAINING}") + set(IDX "${LIST_SIZE}") + elseif(ON_IDX EQUAL 0) + message( + FATAL_ERROR + "Duplicated 'ON' in dependency specification for component '${CURRENT_COMPONENT}'" + ) + elseif(ON_IDX EQUAL 1) + message(FATAL_ERROR "Empty dependency specification for component '${CURRENT_COMPONENT}'") + else() + math(EXPR N_DEPS "${ON_IDX} - 1") + list(SUBLIST DEPENDENCIES_SPEC ${IDX} ${N_DEPS} CURRENT_DEPENDENCIES) + math(EXPR IDX "${IDX} + ${N_DEPS}") + endif() + + string(REPLACE ";" "${DEPENDENCY_SEPARATOR}" DEPENDENCY_STRING "${CURRENT_DEPENDENCIES}") + list(APPEND REFERENCED_COMPONENTS "${CURRENT_COMPONENT}") + list(APPEND COMPONENT_DEPENDENCIES "${DEPENDENCY_STRING}") + endwhile() + + # Process parsed results + + # Ensure that the dependency list has the same size as the target list + list(LENGTH DEPENDENCY_LIST N_DEPS) + list(LENGTH TARGET_LIST N_TARGETS) + math(EXPR N_MISSING "${N_TARGETS} - ${N_DEPS}") + foreach(_ RANGE 1 "${N_MISSING}") # start at 1 since stop is inclusive + # Empty dependency lists have to be represented as a list of two empty elements due to quirks in + # cmake regarding the representation and handling of empty lists + list(APPEND DEPENDENCY_LIST "${DEPENDENCY_SEPARATOR}") + endforeach() + + list(LENGTH COMPONENT_DEPENDENCIES N_DEPS_SPECIFIED) + if(N_DEPS_SPECIFIED GREATER 0) + math(EXPR STOP "${N_DEPS_SPECIFIED} - 1") + foreach(IDX RANGE ${STOP}) + list(GET REFERENCED_COMPONENTS ${IDX} CURRENT_COMPONENT) + list(GET COMPONENT_DEPENDENCIES ${IDX} CURRENT_DEPENDENCIES) + + _package_project_normalize_target_name( + "${CURRENT_COMPONENT}" "${NAMESPACE}" "${TARGET_LIST}" "${ALIAS_LIST}" CURRENT_COMPONENT + ) + + list(FIND TARGET_LIST "${CURRENT_COMPONENT}" COMPONENT_IDX) + + if(COMPONENT_IDX LESS 0) + message( + FATAL_ERROR + "Found dependency specification for unknown component '${CURRENT_COMPONENT}' (might have been referenced via an ALIAS)" + ) + endif() + + # Check we are not overwriting an existing dependency spec (that would be a bug) + list(GET DEPENDENCY_LIST ${COMPONENT_IDX} PREV_DEPS) + if(NOT ("${PREV_DEPS}" STREQUAL "${DEPENDENCY_SEPARATOR}")) + message( + FATAL_ERROR + "Internal error in dependency parsing - would overwrite '${PREV_DEPS}' with '${CURRENT_DEPENDENCIES}' for component '${CURRENT_COMPONENT}'" + ) + endif() + + # Transform into proper list + string(REPLACE "${DEPENDENCY_SEPARATOR}" ";" CURRENT_DEPENDENCIES "${CURRENT_DEPENDENCIES}") + + set(NORMALIZED_DEPENDENCIES "") + foreach(DEP_SPEC IN LISTS CURRENT_DEPENDENCIES) + # Turn dependency specification into a list + string(REPLACE " " ";" DEP_SPEC "${DEP_SPEC}") + list(POP_FRONT DEP_SPEC DEP_NAME) + + # Prefer using alias names for dependencies + _package_project_normalize_alias_name( + "${DEP_NAME}" "${NAMESPACE}" "${TARGET_LIST}" "${ALIAS_LIST}" NORMALIZED_DEP_NAME + ) + if(NOT NORMALIZED_DEP_NAME) + # If there is no alias, use the original name + set(NORMALIZED_DEP_NAME "${DEP_NAME}") + endif() + + list(PREPEND DEP_SPEC "${NORMALIZED_DEP_NAME}") + # Turn dependency specification back into a string + list(JOIN DEP_SPEC " " DEP_SPEC) + + list(APPEND NORMALIZED_DEPENDENCIES "${DEP_SPEC}") + endforeach() + + # Turn back into a single string separated by DEPENDENCY_SEPARATOR + list(JOIN NORMALIZED_DEPENDENCIES "${DEPENDENCY_SEPARATOR}" NORMALIZED_DEPENDENCIES) + + # Update dependency entry belonging to the current component + list(REMOVE_AT DEPENDENCY_LIST ${COMPONENT_IDX}) + list(INSERT DEPENDENCY_LIST ${COMPONENT_IDX} "${NORMALIZED_DEPENDENCIES}") + endforeach() + endif() + + set(${OUT_DEPENDENCY_LIST_NAME} + "${DEPENDENCY_LIST}" + PARENT_SCOPE + ) + message(STATUS "Final dependency list: ${DEPENDENCY_LIST}") +endfunction() + function(packageProject) include(CMakePackageConfigHelpers) include(GNUInstallDirs) @@ -12,11 +382,15 @@ function(packageProject) cmake_parse_arguments( PROJECT "" - "NAME;VERSION;INCLUDE_DIR;INCLUDE_DESTINATION;BINARY_DIR;COMPATIBILITY;EXPORT_HEADER;VERSION_HEADER;NAMESPACE;DISABLE_VERSION_SUFFIX;ARCH_INDEPENDENT;INCLUDE_HEADER_PATTERN;CPACK;RUNTIME_DESTINATION" - "DEPENDENCIES" + "NAME;VERSION;INCLUDE_DIR;INCLUDE_DESTINATION;BINARY_DIR;COMPATIBILITY;EXPORT_HEADER;VERSION_HEADER;NAMESPACE;DISABLE_VERSION_SUFFIX;ARCH_INDEPENDENT;INCLUDE_HEADER_PATTERN;CPACK;RUNTIME_DESTINATION;" + "DEPENDENCIES;COMPONENTS;COMPONENT_DEPENDENCIES;DEFAULT_COMPONENTS" ${ARGN} ) + if(DEFINED PROJECT_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "Unknown arguments '${PROJECT_UNPARSED_ARGUMENTS}' in packageProject") + endif() + # optional feature: TRUE or FALSE or UNDEFINED! These variables will then hold the respective # value from the argument list or be undefined if the associated one_value_keyword could not be # found. @@ -30,56 +404,163 @@ function(packageProject) set(PROJECT_COMPATIBILITY AnyNewerVersion) endif() - # we want to automatically add :: to our namespace, so only append if a namespace was given in the - # first place we also provide an alias to ensure that local and installed versions have the same - # name + _package_project_parse_version( + "${PROJECT_VERSION}" PROJECT_VERSION_MAJOR PROJECT_VERSION_MINOR PROJECT_VERSION_PATCH + PROJECT_VERSION_TWEAK + ) + + # cmake doesn't support appending empty elements to an empty list. Therefore, we have to add a + # dummy element to it (https://stackoverflow.com/a/58581283) + set(UNDERLYING_TARGETS "ToBeRemoved") + set(TARGET_ALIASES "ToBeRemoved") + set(PACKAGE_TARGET_NAMES "ToBeRemoved") + set(PACKAGE_TARGET_DEPENDENCIES "ToBeRemoved") + + set(TARGET_DEPENDENCY_SEPARATOR " & ") + if(DEFINED PROJECT_NAMESPACE) if(PROJECT_CPACK) set(CPACK_PACKAGE_NAMESPACE ${PROJECT_NAMESPACE}) endif() set(PROJECT_NAMESPACE ${PROJECT_NAMESPACE}::) - add_library(${PROJECT_NAMESPACE}${PROJECT_NAME} ALIAS ${PROJECT_NAME}) endif() + if(TARGET "${PROJECT_NAME}") + _package_project_normalize_target_name( + "${PROJECT_NAME}" "${PROJECT_NAMESPACE}" "${UNDERLYING_TARGETS}" "${TARGET_ALIASES}" + TARGET_NAME + ) + _package_project_normalize_alias_name( + "${PROJECT_NAME}" "${PROJECT_NAMESPACE}" "${UNDERLYING_TARGETS}" "${TARGET_ALIASES}" + ALIAS_NAME + ) + + list(APPEND UNDERLYING_TARGETS "${TARGET_NAME}") + list(APPEND TARGET_ALIASES "${ALIAS_NAME}") + + if(ALIAS_NAME) + set(PACKAGE_MAIN_TARGET "${ALIAS_TARGET}") + else() + set(PACKAGE_MAIN_TARGET "${TARGET_NAME}") + endif() + + # Adapt syntax to be applicable to the parsing routine + if(PROJECT_DEPENDENCIES) + list(PREPEND PROJECT_DEPENDENCIES "ON") + list(PREPEND PROJECT_DEPENDENCIES "${TARGET_NAME}") + endif() + _package_project_parse_dependencies( + "${PROJECT_DEPENDENCIES}" + "${UNDERLYING_TARGETS}" + "${TARGET_ALIASES}" + "${PACKAGE_TARGET_DEPENDENCIES}" + PACKAGE_TARGET_DEPENDENCIES + "${TARGET_DEPENDENCY_SEPARATOR}" + "${PROJECT_NAMESPACE}" + ) + elseif(NOT PROJECT_COMPONENTS) + message( + FATAL_ERROR "No components specified and '${PROJECT_NAME}' itself is not a valid target" + ) + endif() + + # Process specified components (if any) + if(DEFINED PROJECT_COMPONENTS) + _package_project_parse_components( + "${PROJECT_COMPONENTS}" "${PROJECT_NAMESPACE}" "${UNDERLYING_TARGETS}" "${TARGET_ALIASES}" + UNDERLYING_TARGETS TARGET_ALIASES + ) + + _package_project_parse_dependencies( + "${PROJECT_COMPONENT_DEPENDENCIES}" + "${UNDERLYING_TARGETS}" + "${TARGET_ALIASES}" + "${PACKAGE_TARGET_DEPENDENCIES}" + PACKAGE_TARGET_DEPENDENCIES + "${TARGET_DEPENDENCY_SEPARATOR}" + "${PROJECT_NAMESPACE}" + ) + endif() + + if(PROJECT_NAMESPACE) + # Strip namespace from aliases + list(TRANSFORM TARGET_ALIASES REPLACE "^${PROJECT_NAMESPACE}" "") + endif() + + list(LENGTH UNDERLYING_TARGETS STOP) + math(EXPR STOP "${STOP} - 1") + # Note: we start at index 1 to skip over the still-present dummy entries + foreach(IDX RANGE 1 "${STOP}") + list(GET UNDERLYING_TARGETS ${IDX} CURRENT_TARGET) + list(GET TARGET_ALIASES ${IDX} CURRENT_ALIAS) + + _package_project_resolve_target(${CURRENT_TARGET} UNDERLYING_TARGET ALIAS_TARGET) + if(PROJECT_NAMESPACE) + string(REGEX REPLACE "^${PROJECT_NAMESPACE}" "" ALIAS_TARGET "${ALIAS_TARGET}") + endif() + + if(ALIAS_TARGET) + if(CURRENT_ALIAS AND NOT ("${CURRENT_ALIAS}" STREQUAL "${ALIAS_TARGET})")) + message( + FATAL_ERROR + "Provided target '${CURRENT_TARGET}' is already an ALIAS target but the explicitly provided alias is different ('${CURRENT_ALIAS}')" + ) + else() + # The given target is actually an alias target -> use it as the alias and infer the aliased + # target + set(CURRENT_ALIAS "${ALIAS_TARGET}") + set(CURRENT_TARGET "${UNDERLYING_TARGET}") + endif() + endif() + + if(CURRENT_ALIAS) + if(NOT TARGET "${PROJECT_NAMESPACE}${CURRENT_ALIAS}") + # Define the alias target, if it doesn't exist yet + add_library("${PROJECT_NAMESPACE}${CURRENT_ALIAS}" ALIAS "${CURRENT_TARGET}") + endif() + + # Ensure that the export will use the ALIAS name + set_target_properties(${CURRENT_TARGET} PROPERTIES EXPORT_NAME ${CURRENT_ALIAS}) + elseif(PROJECT_NAMESPACE AND NOT TARGET "${PROJECT_NAMESPACE}${CURRENT_TARGET}") + # Ensure an appropriate namespaced target exists + add_library(${PROJECT_NAMESPACE}${CURRENT_TARGET} ALIAS ${CURRENT_TARGET}) + endif() + + # Update the entries in our lists in case CURRENT_TARGET/CURRENT_ALIAS has been modified + list(REMOVE_AT UNDERLYING_TARGETS ${IDX}) + list(INSERT UNDERLYING_TARGETS ${IDX} "${CURRENT_TARGET}") + list(REMOVE_AT TARGET_ALIASES ${IDX}) + list(INSERT TARGET_ALIASES ${IDX} "${CURRENT_ALIAS}") + endforeach() + + # Get rid of dummy entries in lists + list(POP_FRONT UNDERLYING_TARGETS) + list(POP_FRONT TARGET_ALIASES) + list(POP_FRONT PACKAGE_TARGET_NAMES) + list(POP_FRONT PACKAGE_TARGET_DEPENDENCIES) + if(DEFINED PROJECT_VERSION_HEADER OR DEFINED PROJECT_EXPORT_HEADER) + if(NOT PACKAGE_MAIN_TARGET) + message( + FATAL_ERROR + "VERSION_HEADER and EXPORT_HEADER can only be used if '${PROJECT_NAME}' is itself a target" + ) + endif() + + # In case ${PROJECT_NAME} is itself a target, its underlying target will be the first entry in + # the UNDERLYING_TARGETS list + list(GET UNDERLYING_TARGETS 0 MAIN_TARGET) + set(PROJECT_VERSION_INCLUDE_DIR ${PROJECT_BINARY_DIR}/PackageProjectInclude) if(DEFINED PROJECT_EXPORT_HEADER) include(GenerateExportHeader) generate_export_header( - ${PROJECT_NAME} EXPORT_FILE_NAME ${PROJECT_VERSION_INCLUDE_DIR}/${PROJECT_EXPORT_HEADER} + ${MAIN_TARGET} EXPORT_FILE_NAME ${PROJECT_VERSION_INCLUDE_DIR}/${PROJECT_EXPORT_HEADER} ) endif() if(DEFINED PROJECT_VERSION_HEADER) - # clear previous matches - unset(CMAKE_MATCH_1) - unset(CMAKE_MATCH_3) - unset(CMAKE_MATCH_5) - unset(CMAKE_MATCH_7) - - string(REGEX MATCH "^([0-9]+)(\\.([0-9]+))?(\\.([0-9]+))?(\\.([0-9]+))?$" _ - "${PROJECT_VERSION}" - ) - - set(PROJECT_VERSION_MAJOR ${CMAKE_MATCH_1}) - set(PROJECT_VERSION_MINOR ${CMAKE_MATCH_3}) - set(PROJECT_VERSION_PATCH ${CMAKE_MATCH_5}) - set(PROJECT_VERSION_TWEAK ${CMAKE_MATCH_7}) - - if(NOT DEFINED PROJECT_VERSION_MAJOR) - set(PROJECT_VERSION_MAJOR "0") - endif() - if(NOT DEFINED PROJECT_VERSION_MINOR) - set(PROJECT_VERSION_MINOR "0") - endif() - if(NOT DEFINED PROJECT_VERSION_PATCH) - set(PROJECT_VERSION_PATCH "0") - endif() - if(NOT DEFINED PROJECT_VERSION_TWEAK) - set(PROJECT_VERSION_TWEAK "0") - endif() - string(TOUPPER ${PROJECT_NAME} UPPERCASE_PROJECT_NAME) # ensure that the generated macro does not include invalid characters string(REGEX REPLACE [^a-zA-Z0-9] _ UPPERCASE_PROJECT_NAME ${UPPERCASE_PROJECT_NAME}) @@ -89,14 +570,9 @@ function(packageProject) ) endif() - get_target_property(target_type ${PROJECT_NAME} TYPE) - if(target_type STREQUAL "INTERFACE_LIBRARY") - set(VISIBILITY INTERFACE) - else() - set(VISIBILITY PUBLIC) - endif() + _package_project_determine_visibility_flag(${MAIN_TARGET} VISIBILITY) target_include_directories( - ${PROJECT_NAME} ${VISIBILITY} "$" + ${MAIN_TARGET} ${VISIBILITY} "$" ) install( DIRECTORY ${PROJECT_VERSION_INCLUDE_DIR}/ @@ -105,14 +581,20 @@ function(packageProject) ) endif() - set(wbpvf_extra_args "") if(NOT DEFINED PROJECT_ARCH_INDEPENDENT) - get_target_property(target_type "${PROJECT_NAME}" TYPE) - if(target_type STREQUAL "INTERFACE_LIBRARY") - set(PROJECT_ARCH_INDEPENDENT YES) - endif() + set(PROJECT_ARCH_INDEPENDENT YES) + + foreach(CURRENT_TARGET IN LISTS UNDERLYING_TARGETS) + get_target_property(CURRENT_TYPE ${CURRENT_TARGET} TYPE) + + if(NOT ("${CURRENT_TYPE}" STREQUAL "INTERFACE_LIBRARY")) + set(PROJECT_ARCH_INDEPENDENT NO) + break() + endif() + endforeach() endif() + set(wbpvf_extra_args "") if(PROJECT_ARCH_INDEPENDENT) set(wbpvf_extra_args ARCH_INDEPENDENT) # install to architecture independent (share) directory @@ -122,8 +604,9 @@ function(packageProject) set(INSTALL_DIR_FOR_CMAKE_CONFIGS ${CMAKE_INSTALL_LIBDIR}) endif() + string(TOLOWER "${PROJECT_NAME}" PROJECT_NAME_LOWER) write_basic_package_version_file( - "${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" + "${PROJECT_BINARY_DIR}/${PROJECT_NAME_LOWER}-config-version.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY ${PROJECT_COMPATIBILITY} ${wbpvf_extra_args} ) @@ -133,45 +616,79 @@ function(packageProject) set(PROJECT_RUNTIME_DESTINATION ${PROJECT_NAME}${PROJECT_VERSION_SUFFIX}) endif() - install( - TARGETS ${PROJECT_NAME} - EXPORT ${PROJECT_NAME}Targets - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_RUNTIME_DESTINATION} - COMPONENT "${PROJECT_NAME}_Runtime" - NAMELINK_COMPONENT "${PROJECT_NAME}_Development" - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_RUNTIME_DESTINATION} - COMPONENT "${PROJECT_NAME}_Development" - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}/${PROJECT_RUNTIME_DESTINATION} - COMPONENT "${PROJECT_NAME}_Runtime" - BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}/${PROJECT_RUNTIME_DESTINATION} - COMPONENT "${PROJECT_NAME}_Runtime" - PUBLIC_HEADER DESTINATION ${PROJECT_INCLUDE_DESTINATION} COMPONENT "${PROJECT_NAME}_Development" - INCLUDES - DESTINATION "${PROJECT_INCLUDE_DESTINATION}" - ) + foreach(CURRENT_TARGET IN LISTS UNDERLYING_TARGETS) + set_target_properties(${CURRENT_TARGET} PROPERTIES VERSION ${PROJECT_VERSION}) - set("${PROJECT_NAME}_INSTALL_CMAKEDIR" - "${INSTALL_DIR_FOR_CMAKE_CONFIGS}/cmake/${PROJECT_NAME}${PROJECT_VERSION_SUFFIX}" - CACHE PATH "CMake package config location relative to the install prefix" - ) + if(PROJECT_VERSION_MAJOR OR PROJECT_VERSION_MINOR) + if("${PROJECT_COMPATIBILITY}" STREQUAL "AnyNewerVersion") + set(PROJECT_SOVERSION "") + elseif("${PROJECT_COMPATIBILITY}" STREQUAL "SameMajorVersion") + set(PROJECT_SOVERSION "${PROJECT_VERSION_MAJOR}") + elseif("${PROJECT_COMPATIBILITY}" STREQUAL "SameMinorVersion") + set(PROJECT_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}") + elseif("${PROJECT_COMPATIBILITY}" STREQUAL "ExactVersion") + # Note: according to cmake doc ExactVersion still ignores tweak version + set(PROJECT_SOVERSION + "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" + ) + else() + message(FATAL_ERROR "Unknown compatibility mode '${PROJECT_COMPATIBILITY}'") + endif() + + set_target_properties(${CURRENT_TARGET} PROPERTIES SOVERSION "${PROJECT_SOVERSION}") + endif() + + set("${PROJECT_NAME}_INSTALL_CMAKEDIR" + "${INSTALL_DIR_FOR_CMAKE_CONFIGS}/cmake/${PROJECT_NAME}${PROJECT_VERSION_SUFFIX}" + CACHE PATH "CMake package config location relative to the install prefix" + ) + + # Name target files same as the exported targets (without namespace) + get_target_property(EXP_NAME ${CURRENT_TARGET} EXPORT_NAME) + if(NOT EXP_NAME) + set(EXP_NAME "${CURRENT_TARGET}") + endif() + + list(APPEND PACKAGE_TARGET_NAMES "${EXP_NAME}") + list(APPEND PACKAGE_TARGET_FILES "${EXP_NAME}Targets.cmake") + + install( + TARGETS ${CURRENT_TARGET} + EXPORT ${EXP_NAME}Targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_RUNTIME_DESTINATION} + COMPONENT "${PROJECT_NAME}_Runtime" + NAMELINK_COMPONENT "${PROJECT_NAME}_Development" + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_RUNTIME_DESTINATION} + COMPONENT "${PROJECT_NAME}_Development" + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}/${PROJECT_RUNTIME_DESTINATION} + COMPONENT "${PROJECT_NAME}_Runtime" + BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}/${PROJECT_RUNTIME_DESTINATION} + COMPONENT "${PROJECT_NAME}_Runtime" + PUBLIC_HEADER DESTINATION ${PROJECT_INCLUDE_DESTINATION} + COMPONENT "${PROJECT_NAME}_Development" + INCLUDES + DESTINATION "${PROJECT_INCLUDE_DESTINATION}" + ) + + install( + EXPORT ${EXP_NAME}Targets + DESTINATION "${${PROJECT_NAME}_INSTALL_CMAKEDIR}" + NAMESPACE ${PROJECT_NAMESPACE} + COMPONENT "${PROJECT_NAME}_Development" + ) + endforeach() mark_as_advanced("${PROJECT_NAME}_INSTALL_CMAKEDIR") - configure_file( + configure_package_config_file( ${PACKAGE_PROJECT_ROOT_PATH}/Config.cmake.in - "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" @ONLY - ) - - install( - EXPORT ${PROJECT_NAME}Targets - DESTINATION "${${PROJECT_NAME}_INSTALL_CMAKEDIR}" - NAMESPACE ${PROJECT_NAMESPACE} - COMPONENT "${PROJECT_NAME}_Development" + "${PROJECT_BINARY_DIR}/${PROJECT_NAME_LOWER}-config.cmake" + INSTALL_DESTINATION "${${PROJECT_NAME}_INSTALL_CMAKEDIR}" ) install( - FILES "${PROJECT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" - "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + FILES "${PROJECT_BINARY_DIR}/${PROJECT_NAME_LOWER}-config-version.cmake" + "${PROJECT_BINARY_DIR}/${PROJECT_NAME_LOWER}-config.cmake" DESTINATION "${${PROJECT_NAME}_INSTALL_CMAKEDIR}" COMPONENT "${PROJECT_NAME}_Development" ) @@ -196,6 +713,7 @@ function(packageProject) ) if(PROJECT_CPACK) + # TODO: Make use of CPackComponent? if(CPACK_PACKAGE_NAMESPACE) set(CPACK_PACKAGE_NAME ${CPACK_PACKAGE_NAMESPACE}-${PROJECT_NAME}) else() diff --git a/Config.cmake.in b/Config.cmake.in index 15f626a..ff58564 100644 --- a/Config.cmake.in +++ b/Config.cmake.in @@ -1,10 +1,275 @@ +@PACKAGE_INIT@ + include(CMakeFindDependencyMacro) -string(REGEX MATCHALL "[^;]+" SEPARATE_DEPENDENCIES "@PROJECT_DEPENDENCIES@") +# ################################################################################################## +# Data setup +# ################################################################################################## + +# The name of the package this config file is for +set(PACKAGE_NAME "@PROJECT_NAME@") +string(TOLOWER "${PACKAGE_NAME}" PACKAGE_NAME_LOWER) + +# A list of target names contained in this package +set(TARGET_NAMES "@PACKAGE_TARGET_NAMES@") + +# A list of corresponding target files. The order of target files corresponds to the order of target +# names in the TARGET_NAMES list. +set(TARGET_FILES "@PACKAGE_TARGET_FILES@") +list(TRANSFORM TARGET_FILES PREPEND "${CMAKE_CURRENT_LIST_DIR}/") + +# Separator used in the list of dependencies for a single target +set(TARGET_DEPENDENCY_SEPARATOR "@TARGET_DEPENDENCY_SEPARATOR@") + +# List of target dependencies. The order of these entries corresponds to the order of target names +# in the TARGET_NAMES list. Each entry is itself a list where individual elements are separated by +# TARGET_DEPENDENCY_SEPARATOR. +set(TARGET_DEPENDENCIES "@PACKAGE_TARGET_DEPENDENCIES@") + +# The name of the main target (to be used under all circumstances) +set(MAIN_TARGET "@PACKAGE_MAIN_TARGET@") + +# Components to be selected by default (that is, if no explicit COMPONENTS where specified in the +# find_package command +set(DEFAULT_COMPONENTS "@PROJECT_DEFAULT_COMPONENTS@") + +# Consistency check +foreach(CURRENT_LIST IN ITEMS TARGET_NAMES TARGET_FILES TARGET_DEPENDENCIES) + list(LENGTH ${CURRENT_LIST} LIST_SIZE) + + if(NOT DEFINED N_TARGETS) + set(N_TARGETS "${LIST_SIZE}") + else() + if(NOT ("${N_TARGETS}" STREQUAL "${LIST_SIZE}")) + message( + FATAL_ERROR + "Corrupted config file for package '${PACKAGE_NAME}': target-related lists have inconsistent sizes" + ) + endif() + endif() +endforeach() + +# ################################################################################################## +# Handle potential capitalization differences in the project's name +# ################################################################################################## + +string(TOLOWER "${CMAKE_FIND_PACKAGE_NAME}" CMAKE_FIND_PACKAGE_NAME_LOWER) + +if("${PACKAGE_NAME_LOWER}" STREQUAL "${CMAKE_FIND_PACKAGE_NAME_LOWER}") + if(NOT ("${PACKAGE_NAME}" STREQUAL "${CMAKE_FIND_PACKAGE_NAME}")) + # The find_package call used a different capitalization of '@PROJECT_NAME@' This can lead to + # issues like the config file being found on a platform with a case-insensitive file system but + # not on a platform with a case-sensitive filesystem (in case the config file uses the + # NameConfig.cmake naming convention rather than name-config.cmake (all-lowercase)). Additional + # issues can arise if the caller expects variables such as Name_* to be defined where the + # capitalization of Name is important as well. + # + # We try to circumvent such issues by adapting the capitalization used in the find_package call + # but ultimately, the caller should switch to using the proper capitalization. + message( + AUTHOR_WARNING + "Incorrect capitalization in '${CMAKE_FIND_PACKAGE_NAME}'. It should be changed to '@PROJECT_NAME@' to avoid issues." + ) + set(PACKAGE_NAME "${CMAKE_FIND_PACKAGE_NAME}") + endif() +endif() + +# ################################################################################################## +# Helper functions & macros +# ################################################################################################## + +macro(package_error MSG) + set(${PACKAGE_NAME}_FOUND FALSE) + set(${PACKAGE_NAME}_NOT_FOUND_MESSAGE "${MSG}") + + # Since this is a macro, this return will exit this config file + return() +endmacro() + +function(list_contains LIST ELEMENT OUTPUT_VARIABLE) + list(FIND LIST "${ELEMENT}" IDX) + + if("${IDX}" STREQUAL "-1") + set(${OUTPUT_VARIABLE} + FALSE + PARENT_SCOPE + ) + else() + set(${OUTPUT_VARIABLE} + TRUE + PARENT_SCOPE + ) + endif() +endfunction() + +macro(verify_all_components_are_valid) + foreach(CURRENT_COMP IN LISTS ${PACKAGE_NAME}_FIND_COMPONENTS) + # Verify that all specified components are actually known + list_contains("${TARGET_NAMES}" "${CURRENT_COMP}" COMPONENT_IS_KNOWN) + + if(NOT COMPONENT_IS_KNOWN) + package_error("Unknown component '${CURRENT_COMP}' for package '${PACKAGE_NAME}'") + endif() + + if("${CURRENT_COMP}" STREQUAL "${MAIN_TARGET}") + package_error( + "'${CURRENT_COMP}' is not a component for package '${PACKAGE_NAME}' - remove from COMPONENTS list" + ) + endif() + endforeach() +endmacro() + +function(get_dependencies IDX OUTPUT_VARIABLE) + list(GET TARGET_DEPENDENCIES ${IDX} DEPENDENCIES) + + # Convert string to list by turning the custom separator to the cmake list separator (semicolon) + string(REPLACE "${TARGET_DEPENDENCY_SEPARATOR}" ";" DEPENDENCIES "${DEPENDENCIES}") -foreach(dependency ${SEPARATE_DEPENDENCIES}) - string(REPLACE " " ";" args "${dependency}") - find_dependency(${args}) + # Note: this causes DEPENDENCIES to effectively become an empty list, if it contains only empty + # elements (a quirk of how cmake represents a list containing only a single empty string) + list(REMOVE_DUPLICATES DEPENDENCIES) + + set(${OUTPUT_VARIABLE} + "${DEPENDENCIES}" + PARENT_SCOPE + ) +endfunction() + +function(to_internal_target TARGET OUTPUT_VARIABLE) + # Check as-given + list_contains("${TARGET_NAMES}" "${TARGET}" INTERNAL_TARGET) + if(INTERNAL_TARGET) + set(${OUTPUT_VARIABLE} + "${TARGET}" + PARENT_SCOPE + ) + else() + # Not an internal target + unset(${OUTPUT_VARIABLE} PARENT_SCOPE) + endif() +endfunction() + +macro(include_target TARGET) + if(NOT ${PACKAGE_NAME}_${TARGET}_FOUND) + list(FIND TARGET_NAMES "${TARGET}" "${TARGET}_IDX") + + get_dependencies("${${TARGET}_IDX}" DEPENDENCIES) + + # Dependency resolution + foreach(CURRENT_DEP IN LISTS DEPENDENCIES) + # CURRENT_DEP may be a space-separated list where all additional elements represent special + # options to be passed to find_dependency + string(REPLACE " " ";" DEPENDENCY_DETAILS ${CURRENT_DEP}) + list(GET DEPENDENCY_DETAILS 0 CURRENT_DEP) + list(POP_FRONT DEPENDENCY_DETAILS) + + if(NOT ${PACKAGE_NAME}_${CURRENT_DEP}_FOUND) + to_internal_target(${CURRENT_DEP} INTERNAL_TARGET) + + if(INTERNAL_TARGET) + if(DEPENDENCY_DETAILS) + # Could we do something clever with this? + message( + AUTHOR_WARNING + "Discarding extra info for dependency ${INTERNAL_TARGET}: ${DEPENDENCY_DETAILS}" + ) + endif() + # Recurse to ensure that the dependency is included before the dependent target + include_target(${INTERNAL_TARGET}) + else() + # Regular external dependency + find_dependency(${CURRENT_DEP} ${DEPENDENCY_DETAILS}) + + set(${PACKAGE_NAME}_${CURRENT_DEP}_FOUND "${CURRENT_DEP}_FOUND") + endif() + endif() + + if(NOT ${PACKAGE_NAME}_${CURRENT_DEP}_FOUND) + # Dependency is still not met -> error + if(NOT ("${TARGET}" STREQUAL "${MAIN_TARGET}")) + set(FOR_COMPONENT "for component '${TARGET}' ") + endif() + + package_error( + "Unmet dependency '${CURRENT_DEP}' ${FOR_COMPONENT}of package '${PACKAGE_NAME}'" + ) + endif() + endforeach() + + list(GET TARGET_FILES "${${TARGET}_IDX}" FILE_TO_INCLUDE) + + if(NOT EXISTS ${FILE_TO_INCLUDE}) + message( + FATAL_ERROR + "Corrupted config file for package '${PACKAGE_NAME}': Expected file '${FILE_TO_INCLUDE}' to exist, but it didn't" + ) + endif() + + include("${FILE_TO_INCLUDE}") + + if(${PACKAGE_NAME}_NOT_FOUND_MESSAGE) + # There has been an error detected in the included target file -> abort processing + return() + endif() + + set(${PACKAGE_NAME}_${TARGET}_FOUND TRUE) + endif() +endmacro() + +# ################################################################################################## +# Preliminaries +# ################################################################################################## + +if(NOT ${PACKAGE_NAME}_FIND_COMPONENTS AND DEFAULT_COMPONENTS) + set(${PACKAGE_NAME}_FIND_COMPONENTS "${DEFAULT_COMPONENTS}") +endif() + +verify_all_components_are_valid() + +if(NOT ${PACKAGE_NAME}_FIND_COMPONENTS AND NOT MAIN_TARGET) + package_error( + "No components specified for package '${PACKAGE_NAME}' which doesn't have a non-component part" + ) +endif() + +# ################################################################################################## +# Include relevant targets +# ################################################################################################## + +if(MAIN_TARGET) + include_target(${MAIN_TARGET}) +endif() + +foreach(CURRENT_COMPONENT IN LISTS ${PACKAGE_NAME}_FIND_COMPONENTS) + include_target(${CURRENT_COMPONENT}) endforeach() -include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake") +# ################################################################################################## +# Final checks and cleanup +# ################################################################################################## + +check_required_components("${PACKAGE_NAME}") + +# Clear up local variables to not pollute the calling scope with them This should all variables +# globally defined or defined in a macro (that don't use explicit prefixing that make name clashes +# unlikely) +unset(PACKAGE_NAME) +unset(PACKAGE_NAME_LOWER) +unset(TARGET_NAMES) +unset(TARGET_FILES) +unset(TARGET_DEPENDENCY_SEPARATOR) +unset(TARGET_DEPENDENCIES) +unset(MAIN_TARGET) +unset(DEFAULT_COMPONENTS) +unset(N_TARGETS) +unset(LIST_SIZE) +unset(N_TARGETS) +unset(CMAKE_FIND_PACKAGE_NAME_LOWER) +unset(COMPONENT_IS_KNOWN) +unset(CURRENT_COMP) +unset(DEPENDENCIES) +unset(CURRENT_DEP) +unset(DEPENDENCY_DETAILS) +unset(INTERNAL_TARGET) +unset(FOR_COMPONENT) +unset(FILE_TO_INCLUDE) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8fcacf4..eefa111 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -9,11 +9,19 @@ project( ) if(TEST_INSTALLED_VERSION) - find_package(dependency 1.2 REQUIRED) + # Note: Wrong capitalization. The project really is called "dependency". However, PackageProject + # should have installed workarounds to make this work nonetheless (even on case-sensitive + # filesystems) + find_package(Dependency 1.2 REQUIRED) find_package(header_only 1.0 REQUIRED) find_package(namespaced_dependency 4.5.6 REQUIRED) find_package(transitive_dependency 7.8.9 REQUIRED) find_package(runtime_destination_dependency 1.5 REQUIRED) + find_package( + components 1.0 + COMPONENTS Pow + REQUIRED + ) else() if(TEST_CPACK) set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Foo Bar ") @@ -23,13 +31,19 @@ else() add_subdirectory(namespaced_dependency) add_subdirectory(transitive_dependency) add_subdirectory(runtime_destination_dependency) + add_subdirectory(components) endif() add_executable(main main.cpp) target_link_libraries( - main dependency header_only ns::namespaced_dependency - transitive_dependency::transitive_dependency runtime_destination_dependency + main + dependency + header_only + ns::namespaced_dependency + transitive_dependency::transitive_dependency + runtime_destination_dependency + Comps::Pow ) enable_testing() diff --git a/test/components/CMakeLists.txt b/test/components/CMakeLists.txt new file mode 100644 index 0000000..8644d68 --- /dev/null +++ b/test/components/CMakeLists.txt @@ -0,0 +1,78 @@ +cmake_minimum_required(VERSION 3.14...3.22) + +project( + components + VERSION 1.0 + LANGUAGES CXX + DESCRIPTION "A multi-component library setup for testing PackageProject.cmake" +) + +add_library(components_core INTERFACE) +target_include_directories( + components_core INTERFACE $ + $ +) + +add_library(components_add STATIC source/add.cpp) +target_link_libraries(components_add PUBLIC components_core) + +add_library(components_mult STATIC source/mult.cpp) +target_link_libraries(components_mult PUBLIC components_core) + +add_library(components_pow STATIC source/pow.cpp) +# Note: we are explicitly sloppy about Pow's dependency to Core in that we don't specify the +# dependency explicitly. Rather we rely on transitive dependency handling on cmake's side +target_link_libraries(components_pow PUBLIC components_mult) + +# Only add a subset of aliases here to test packageProject's capability to define alias targets +add_library(Comps::Core ALIAS components_core) + +foreach(CURRENT IN ITEMS components_add components_mult components_pow) + target_include_directories( + ${CURRENT} PUBLIC $ + $ + ) +endforeach() + +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../.. PackageProject) + +packageProject( + NAME ${PROJECT_NAME} + COMPONENTS + # These are the different possibilities of specifying components (with aliases) + Comps::Core + components_add + components_mult + AS + Comps::Mult + components_pow + AS + Pow + VERSION ${PROJECT_VERSION} + NAMESPACE Comps + BINARY_DIR ${PROJECT_BINARY_DIR} + INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include + INCLUDE_DESTINATION + include/${PROJECT_NAME}-${PROJECT_VERSION} + DEFAULT_COMPONENTS + Core + Add + COMPONENT_DEPENDENCIES + components_add + ON + Core + Comps::Mult + ON + Comps::Core + # Again, we are implicit about Pow's dependence on Core + Pow + ON + components_mult + CPACK "${TEST_CPACK}" +) + +foreach(ALIAS_NAME IN ITEMS Core Mult Pow) + if(NOT TARGET Comps::${ALIAS_NAME}) + message(FATAL_ERROR "Comps::${ALIAS_NAME} was expected to be defined, but wasn't") + endif() +endforeach() diff --git a/test/components/include/components/add.h b/test/components/include/components/add.h new file mode 100644 index 0000000..250929c --- /dev/null +++ b/test/components/include/components/add.h @@ -0,0 +1,9 @@ +#pragma once + +#include "components/number.h" + +namespace components { + + Number add(const Number &lhs, const Number &rhs); + +} diff --git a/test/components/include/components/mult.h b/test/components/include/components/mult.h new file mode 100644 index 0000000..2f6e7c8 --- /dev/null +++ b/test/components/include/components/mult.h @@ -0,0 +1,9 @@ +#pragma once + +#include "components/number.h" + +namespace components { + + Number mult(const Number &lhs, const Number &rhs); + +} diff --git a/test/components/include/components/number.h b/test/components/include/components/number.h new file mode 100644 index 0000000..ce97db7 --- /dev/null +++ b/test/components/include/components/number.h @@ -0,0 +1,25 @@ +#pragma once + +namespace components { + + class Number { + public: + explicit Number(int val) : m_val(val) {} + + int value() const { return m_val; } + + void setValue(int val) { m_val = val; } + + private: + int m_val; + }; + + inline bool operator==(const Number &lhs, const Number &rhs) { + return lhs.value() == rhs.value(); + } + + inline bool operator==(const Number &lhs, int rhs) { + return lhs.value() == rhs; + } + +} diff --git a/test/components/include/components/pow.h b/test/components/include/components/pow.h new file mode 100644 index 0000000..12d4ecc --- /dev/null +++ b/test/components/include/components/pow.h @@ -0,0 +1,9 @@ +#pragma once + +#include "components/number.h" + +namespace components { + + Number pow(const Number &base, const Number &exponent); + +} diff --git a/test/components/source/add.cpp b/test/components/source/add.cpp new file mode 100644 index 0000000..979eeb8 --- /dev/null +++ b/test/components/source/add.cpp @@ -0,0 +1,10 @@ +#include "components/number.h" +#include "components/add.h" + +namespace components { + + Number add(const Number &lhs, const Number &rhs) { + return Number(lhs.value() + rhs.value()); + } + +} diff --git a/test/components/source/mult.cpp b/test/components/source/mult.cpp new file mode 100644 index 0000000..44d0387 --- /dev/null +++ b/test/components/source/mult.cpp @@ -0,0 +1,10 @@ +#include "components/number.h" +#include "components/mult.h" + +namespace components { + + Number mult(const Number &lhs, const Number &rhs) { + return Number(lhs.value() * rhs.value()); + } + +} diff --git a/test/components/source/pow.cpp b/test/components/source/pow.cpp new file mode 100644 index 0000000..4b3cfef --- /dev/null +++ b/test/components/source/pow.cpp @@ -0,0 +1,17 @@ +#include "components/number.h" +#include "components/mult.h" +#include "components/pow.h" + +namespace components { + + Number pow(const Number &base, const Number &exponent) { + Number result(1); + + for (int i = 0; i < exponent.value(); ++i) { + result = mult(result, base); + } + + return result; + } + +} diff --git a/test/main.cpp b/test/main.cpp index 10fafb9..34172ca 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -43,5 +44,6 @@ int main() { result &= RUNTIME_DESTINATION_DEPENDENCY_VERSION_MINOR == 5; result &= RUNTIME_DESTINATION_DEPENDENCY_VERSION_PATCH == 0; result &= RUNTIME_DESTINATION_DEPENDENCY_VERSION_TWEAK == 0; + result &= (components::pow(components::Number(2), components::Number(3)) == 8); return result ? 0 : 1; }