Skip to content

Commit 4b0a8b2

Browse files
committed
Support libraries consisting of various components
Fixes #22 Fixes #43 (if using multiple targets as components)
1 parent 99aa2e4 commit 4b0a8b2

12 files changed

Lines changed: 963 additions & 67 deletions

File tree

CMakeLists.txt

Lines changed: 541 additions & 51 deletions
Large diffs are not rendered by default.

Config.cmake.in

Lines changed: 240 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,61 @@
22

33
include(CMakeFindDependencyMacro)
44

5+
# ##################################################################################################
6+
# Data setup
7+
# ##################################################################################################
8+
9+
# The name of the package this config file is for
10+
set(PACKAGE_NAME "@PROJECT_NAME@")
11+
string(TOLOWER "${PACKAGE_NAME}" PACKAGE_NAME_LOWER)
12+
13+
# A list of target names contained in this package
14+
set(TARGET_NAMES "@PACKAGE_TARGET_NAMES@")
15+
16+
# A list of corresponding target files. The order of target files corresponds to the order of target
17+
# names in the TARGET_NAMES list.
18+
set(TARGET_FILES "@PACKAGE_TARGET_FILES@")
19+
list(TRANSFORM TARGET_FILES PREPEND "${CMAKE_CURRENT_LIST_DIR}/")
20+
21+
# Separator used in the list of dependencies for a single target
22+
set(TARGET_DEPENDENCY_SEPARATOR "@TARGET_DEPENDENCY_SEPARATOR@")
23+
24+
# List of target dependencies. The order of these entries corresponds to the order of target names
25+
# in the TARGET_NAMES list. Each entry is itself a list where individual elements are separated by
26+
# TARGET_DEPENDENCY_SEPARATOR.
27+
set(TARGET_DEPENDENCIES "@PACKAGE_TARGET_DEPENDENCIES@")
28+
29+
# The name of the main target (to be used under all circumstances)
30+
set(MAIN_TARGET "@PACKAGE_MAIN_TARGET@")
31+
32+
# Components to be selected by default (that is, if no explicit COMPONENTS where specified in the
33+
# find_package command
34+
set(DEFAULT_COMPONENTS "@PROJECT_DEFAULT_COMPONENTS@")
35+
36+
# Consistency check
37+
foreach(CURRENT_LIST IN ITEMS TARGET_NAMES TARGET_FILES TARGET_DEPENDENCIES)
38+
list(LENGTH ${CURRENT_LIST} LIST_SIZE)
39+
40+
if(NOT DEFINED N_TARGETS)
41+
set(N_TARGETS "${LIST_SIZE}")
42+
else()
43+
if(NOT ("${N_TARGETS}" STREQUAL "${LIST_SIZE}"))
44+
message(
45+
FATAL_ERROR
46+
"Corrupted config file for package '${PACKAGE_NAME}': target-related lists have inconsistent sizes"
47+
)
48+
endif()
49+
endif()
50+
endforeach()
51+
552
# ##################################################################################################
653
# Handle potential capitalization differences in the project's name
754
# ##################################################################################################
855

9-
set(PROJECT_NAME "@PROJECT_NAME@")
10-
string(TOLOWER "${PROJECT_NAME}" PROJECT_NAME_LOWER)
1156
string(TOLOWER "${CMAKE_FIND_PACKAGE_NAME}" CMAKE_FIND_PACKAGE_NAME_LOWER)
1257

13-
if("${PROJECT_NAME_LOWER}" STREQUAL "${CMAKE_FIND_PACKAGE_NAME_LOWER}")
14-
if(NOT ("${PROJECT_NAME}" STREQUAL "${CMAKE_FIND_PACKAGE_NAME}"))
58+
if("${PACKAGE_NAME_LOWER}" STREQUAL "${CMAKE_FIND_PACKAGE_NAME_LOWER}")
59+
if(NOT ("${PACKAGE_NAME}" STREQUAL "${CMAKE_FIND_PACKAGE_NAME}"))
1560
# The find_package call used a different capitalization of '@PROJECT_NAME@' This can lead to
1661
# issues like the config file being found on a platform with a case-insensitive file system but
1762
# not on a platform with a case-sensitive filesystem (in case the config file uses the
@@ -25,25 +70,206 @@ if("${PROJECT_NAME_LOWER}" STREQUAL "${CMAKE_FIND_PACKAGE_NAME_LOWER}")
2570
AUTHOR_WARNING
2671
"Incorrect capitalization in '${CMAKE_FIND_PACKAGE_NAME}'. It should be changed to '@PROJECT_NAME@' to avoid issues."
2772
)
28-
set(PROJECT_NAME "${CMAKE_FIND_PACKAGE_NAME}")
73+
set(PACKAGE_NAME "${CMAKE_FIND_PACKAGE_NAME}")
2974
endif()
3075
endif()
3176

3277
# ##################################################################################################
33-
# Look up all required dependencies
78+
# Helper functions & macros
3479
# ##################################################################################################
3580

36-
string(REGEX MATCHALL "[^;]+" SEPARATE_DEPENDENCIES "@PROJECT_DEPENDENCIES@")
81+
macro(package_error MSG)
82+
set(${PACKAGE_NAME}_FOUND FALSE)
83+
set(${PACKAGE_NAME}_NOT_FOUND_MESSAGE "${MSG}")
3784

38-
foreach(dependency ${SEPARATE_DEPENDENCIES})
39-
string(REPLACE " " ";" args "${dependency}")
40-
find_dependency(${args})
41-
endforeach()
85+
# Since this is a macro, this return will exit this config file
86+
return()
87+
endmacro()
88+
89+
function(list_contains LIST ELEMENT OUTPUT_VARIABLE)
90+
list(FIND LIST "${ELEMENT}" IDX)
91+
92+
if("${IDX}" STREQUAL "-1")
93+
set(${OUTPUT_VARIABLE}
94+
FALSE
95+
PARENT_SCOPE
96+
)
97+
else()
98+
set(${OUTPUT_VARIABLE}
99+
TRUE
100+
PARENT_SCOPE
101+
)
102+
endif()
103+
endfunction()
104+
105+
macro(verify_all_components_are_valid)
106+
foreach(CURRENT_COMP IN LISTS ${PACKAGE_NAME}_FIND_COMPONENTS)
107+
# Verify that all specified components are actually known
108+
list_contains("${TARGET_NAMES}" "${CURRENT_COMP}" COMPONENT_IS_KNOWN)
109+
110+
if(NOT COMPONENT_IS_KNOWN)
111+
package_error("Unknown component '${CURRENT_COMP}' for package '${PACKAGE_NAME}'")
112+
endif()
113+
114+
if("${CURRENT_COMP}" STREQUAL "${MAIN_TARGET}")
115+
package_error(
116+
"'${CURRENT_COMP}' is not a component for package '${PACKAGE_NAME}' - remove from COMPONENTS list"
117+
)
118+
endif()
119+
endforeach()
120+
endmacro()
121+
122+
function(get_dependencies IDX OUTPUT_VARIABLE)
123+
list(GET TARGET_DEPENDENCIES ${IDX} DEPENDENCIES)
124+
125+
# Convert string to list by turning the custom separator to the cmake list separator (semicolon)
126+
string(REPLACE "${TARGET_DEPENDENCY_SEPARATOR}" ";" DEPENDENCIES "${DEPENDENCIES}")
127+
128+
# Note: this causes DEPENDENCIES to effectively become an empty list, if it contains only empty
129+
# elements (a quirk of how cmake represents a list containing only a single empty string)
130+
list(REMOVE_DUPLICATES DEPENDENCIES)
131+
132+
set(${OUTPUT_VARIABLE}
133+
"${DEPENDENCIES}"
134+
PARENT_SCOPE
135+
)
136+
endfunction()
137+
138+
function(to_internal_target TARGET OUTPUT_VARIABLE)
139+
# Check as-given
140+
list_contains("${TARGET_NAMES}" "${TARGET}" INTERNAL_TARGET)
141+
if(INTERNAL_TARGET)
142+
set(${OUTPUT_VARIABLE}
143+
"${TARGET}"
144+
PARENT_SCOPE
145+
)
146+
else()
147+
# Not an internal target
148+
unset(${OUTPUT_VARIABLE} PARENT_SCOPE)
149+
endif()
150+
endfunction()
42151

43-
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
152+
macro(include_target TARGET)
153+
if(NOT ${PACKAGE_NAME}_${TARGET}_FOUND)
154+
list(FIND TARGET_NAMES "${TARGET}" "${TARGET}_IDX")
155+
156+
get_dependencies("${${TARGET}_IDX}" DEPENDENCIES)
157+
158+
# Dependency resolution
159+
foreach(CURRENT_DEP IN LISTS DEPENDENCIES)
160+
# CURRENT_DEP may be a space-separated list where all additional elements represent special
161+
# options to be passed to find_dependency
162+
string(REPLACE " " ";" DEPENDENCY_DETAILS ${CURRENT_DEP})
163+
list(GET DEPENDENCY_DETAILS 0 CURRENT_DEP)
164+
list(POP_FRONT DEPENDENCY_DETAILS)
165+
166+
if(NOT ${PACKAGE_NAME}_${CURRENT_DEP}_FOUND)
167+
to_internal_target(${CURRENT_DEP} INTERNAL_TARGET)
168+
169+
if(INTERNAL_TARGET)
170+
if(DEPENDENCY_DETAILS)
171+
# Could we do something clever with this?
172+
message(
173+
AUTHOR_WARNING
174+
"Discarding extra info for dependency ${INTERNAL_TARGET}: ${DEPENDENCY_DETAILS}"
175+
)
176+
endif()
177+
# Recurse to ensure that the dependency is included before the dependent target
178+
include_target(${INTERNAL_TARGET})
179+
else()
180+
# Regular external dependency
181+
find_dependency(${CURRENT_DEP} ${DEPENDENCY_DETAILS})
182+
183+
set(${PACKAGE_NAME}_${CURRENT_DEP}_FOUND "${CURRENT_DEP}_FOUND")
184+
endif()
185+
endif()
186+
187+
if(NOT ${PACKAGE_NAME}_${CURRENT_DEP}_FOUND)
188+
# Dependency is still not met -> error
189+
if(NOT ("${TARGET}" STREQUAL "${MAIN_TARGET}"))
190+
set(FOR_COMPONENT "for component '${TARGET}' ")
191+
endif()
192+
193+
package_error(
194+
"Unmet dependency '${CURRENT_DEP}' ${FOR_COMPONENT}of package '${PACKAGE_NAME}'"
195+
)
196+
endif()
197+
endforeach()
198+
199+
list(GET TARGET_FILES "${${TARGET}_IDX}" FILE_TO_INCLUDE)
200+
201+
if(NOT EXISTS ${FILE_TO_INCLUDE})
202+
message(
203+
FATAL_ERROR
204+
"Corrupted config file for package '${PACKAGE_NAME}': Expected file '${FILE_TO_INCLUDE}' to exist, but it didn't"
205+
)
206+
endif()
207+
208+
include("${FILE_TO_INCLUDE}")
209+
210+
if(${PACKAGE_NAME}_NOT_FOUND_MESSAGE)
211+
# There has been an error detected in the included target file -> abort processing
212+
return()
213+
endif()
214+
215+
set(${PACKAGE_NAME}_${TARGET}_FOUND TRUE)
216+
endif()
217+
endmacro()
44218

45219
# ##################################################################################################
46-
# Final checks
220+
# Preliminaries
47221
# ##################################################################################################
48222

49-
check_required_components("${PROJECT_NAME}")
223+
if(NOT ${PACKAGE_NAME}_FIND_COMPONENTS AND DEFAULT_COMPONENTS)
224+
set(${PACKAGE_NAME}_FIND_COMPONENTS "${DEFAULT_COMPONENTS}")
225+
endif()
226+
227+
verify_all_components_are_valid()
228+
229+
if(NOT ${PACKAGE_NAME}_FIND_COMPONENTS AND NOT MAIN_TARGET)
230+
package_error(
231+
"No components specified for package '${PACKAGE_NAME}' which doesn't have a non-component part"
232+
)
233+
endif()
234+
235+
# ##################################################################################################
236+
# Include relevant targets
237+
# ##################################################################################################
238+
239+
if(MAIN_TARGET)
240+
include_target(${MAIN_TARGET})
241+
endif()
242+
243+
foreach(CURRENT_COMPONENT IN LISTS ${PACKAGE_NAME}_FIND_COMPONENTS)
244+
include_target(${CURRENT_COMPONENT})
245+
endforeach()
246+
247+
# ##################################################################################################
248+
# Final checks and cleanup
249+
# ##################################################################################################
250+
251+
check_required_components("${PACKAGE_NAME}")
252+
253+
# Clear up local variables to not pollute the calling scope with them This should all variables
254+
# globally defined or defined in a macro (that don't use explicit prefixing that make name clashes
255+
# unlikely)
256+
unset(PACKAGE_NAME)
257+
unset(PACKAGE_NAME_LOWER)
258+
unset(TARGET_NAMES)
259+
unset(TARGET_FILES)
260+
unset(TARGET_DEPENDENCY_SEPARATOR)
261+
unset(TARGET_DEPENDENCIES)
262+
unset(MAIN_TARGET)
263+
unset(DEFAULT_COMPONENTS)
264+
unset(N_TARGETS)
265+
unset(LIST_SIZE)
266+
unset(N_TARGETS)
267+
unset(CMAKE_FIND_PACKAGE_NAME_LOWER)
268+
unset(COMPONENT_IS_KNOWN)
269+
unset(CURRENT_COMP)
270+
unset(DEPENDENCIES)
271+
unset(CURRENT_DEP)
272+
unset(DEPENDENCY_DETAILS)
273+
unset(INTERNAL_TARGET)
274+
unset(FOR_COMPONENT)
275+
unset(FILE_TO_INCLUDE)

test/CMakeLists.txt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ if(TEST_INSTALLED_VERSION)
1717
find_package(namespaced_dependency 4.5.6 REQUIRED)
1818
find_package(transitive_dependency 7.8.9 REQUIRED)
1919
find_package(runtime_destination_dependency 1.5 REQUIRED)
20+
find_package(
21+
components 1.0
22+
COMPONENTS Pow
23+
REQUIRED
24+
)
2025
else()
2126
if(TEST_CPACK)
2227
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Foo Bar <foo@bar.local>")
@@ -26,13 +31,19 @@ else()
2631
add_subdirectory(namespaced_dependency)
2732
add_subdirectory(transitive_dependency)
2833
add_subdirectory(runtime_destination_dependency)
34+
add_subdirectory(components)
2935
endif()
3036

3137
add_executable(main main.cpp)
3238

3339
target_link_libraries(
34-
main dependency header_only ns::namespaced_dependency
35-
transitive_dependency::transitive_dependency runtime_destination_dependency
40+
main
41+
dependency
42+
header_only
43+
ns::namespaced_dependency
44+
transitive_dependency::transitive_dependency
45+
runtime_destination_dependency
46+
Comps::Pow
3647
)
3748

3849
enable_testing()

test/components/CMakeLists.txt

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
cmake_minimum_required(VERSION 3.14...3.22)
2+
3+
project(
4+
components
5+
VERSION 1.0
6+
LANGUAGES CXX
7+
DESCRIPTION "A multi-component library setup for testing PackageProject.cmake"
8+
)
9+
10+
add_library(components_core INTERFACE)
11+
target_include_directories(
12+
components_core INTERFACE $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
13+
$<INSTALL_INTERFACE:include/${PROJECT_NAME}-${PROJECT_VERSION}>
14+
)
15+
16+
add_library(components_add STATIC source/add.cpp)
17+
target_link_libraries(components_add PUBLIC components_core)
18+
19+
add_library(components_mult STATIC source/mult.cpp)
20+
target_link_libraries(components_mult PUBLIC components_core)
21+
22+
add_library(components_pow STATIC source/pow.cpp)
23+
# Note: we are explicitly sloppy about Pow's dependency to Core in that we don't specify the
24+
# dependency explicitly. Rather we rely on transitive dependency handling on cmake's side
25+
target_link_libraries(components_pow PUBLIC components_mult)
26+
27+
# Only add a subset of aliases here to test packageProject's capability to define alias targets
28+
add_library(Comps::Core ALIAS components_core)
29+
30+
foreach(CURRENT IN ITEMS components_add components_mult components_pow)
31+
target_include_directories(
32+
${CURRENT} PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
33+
$<INSTALL_INTERFACE:include/${PROJECT_NAME}-${PROJECT_VERSION}>
34+
)
35+
endforeach()
36+
37+
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../.. PackageProject)
38+
39+
packageProject(
40+
NAME ${PROJECT_NAME}
41+
COMPONENTS
42+
# These are the different possibilities of specifying components (with aliases)
43+
Comps::Core
44+
components_add
45+
components_mult
46+
AS
47+
Comps::Mult
48+
components_pow
49+
AS
50+
Pow
51+
VERSION ${PROJECT_VERSION}
52+
NAMESPACE Comps
53+
BINARY_DIR ${PROJECT_BINARY_DIR}
54+
INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include
55+
INCLUDE_DESTINATION
56+
include/${PROJECT_NAME}-${PROJECT_VERSION}
57+
DEFAULT_COMPONENTS
58+
Core
59+
Add
60+
COMPONENT_DEPENDENCIES
61+
components_add
62+
ON
63+
Core
64+
Comps::Mult
65+
ON
66+
Comps::Core
67+
# Again, we are implicit about Pow's dependence on Core
68+
Pow
69+
ON
70+
components_mult
71+
CPACK "${TEST_CPACK}"
72+
)
73+
74+
foreach(ALIAS_NAME IN ITEMS Core Mult Pow)
75+
if(NOT TARGET Comps::${ALIAS_NAME})
76+
message(FATAL_ERROR "Comps::${ALIAS_NAME} was expected to be defined, but wasn't")
77+
endif()
78+
endforeach()

0 commit comments

Comments
 (0)