Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
676 changes: 597 additions & 79 deletions CMakeLists.txt

Large diffs are not rendered by default.

275 changes: 270 additions & 5 deletions Config.cmake.in
Original file line number Diff line number Diff line change
@@ -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}/@[email protected]")
# ##################################################################################################
# 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)
20 changes: 17 additions & 3 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>")
Expand All @@ -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()
Expand Down
Loading