-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcpp-library-install.cmake
More file actions
572 lines (506 loc) · 28.6 KB
/
cpp-library-install.cmake
File metadata and controls
572 lines (506 loc) · 28.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
# SPDX-License-Identifier: BSL-1.0
#
# cpp-library-install.cmake - Installation support for cpp-library projects
#
# This module provides minimal but complete CMake installation support for libraries
# built with cpp-library. It handles:
# - Header-only libraries (INTERFACE targets)
# - Static libraries
# - Shared libraries (when BUILD_SHARED_LIBS is ON)
# - CMake package config generation for find_package() support
#
# Note: GNUInstallDirs and CMakePackageConfigHelpers are included inside
# _cpp_library_setup_install() to avoid requiring project() to be called
# when this module is loaded.
# System packages that don't require version constraints in find_dependency()
# These are commonly available system libraries where version requirements are typically not specified.
# To extend this list in your project, use cpp_library_map_dependency() to explicitly map additional packages.
set(_CPP_LIBRARY_SYSTEM_PACKAGES "Threads" "OpenMP" "ZLIB" "CURL" "OpenSSL")
# Registers a custom dependency mapping for find_dependency() generation
# - Precondition: TARGET is a namespaced target (e.g., "Qt6::Core", "stlab::enum-ops") or non-namespaced (e.g., "opencv_core")
# - Postcondition: FIND_DEPENDENCY_CALL stored for TARGET, used in package config generation
# - FIND_DEPENDENCY_CALL should be the complete arguments to find_dependency(), including version if needed
# - Multiple components of the same package (same name+version+args) are automatically merged into one call
# - Examples:
# - cpp_library_map_dependency("Qt6::Core" "Qt6 6.5.0 COMPONENTS Core")
# - cpp_library_map_dependency("Qt6::Widgets" "Qt6 6.5.0 COMPONENTS Widgets")
# → Generates: find_dependency(Qt6 6.5.0 COMPONENTS Core Widgets)
# - cpp_library_map_dependency("stlab::enum-ops" "stlab-enum-ops 1.0.0")
# - cpp_library_map_dependency("opencv_core" "OpenCV 4.5.0")
# - Note: Most namespaced dependencies work automatically; only use when automatic detection fails or special syntax needed
function(cpp_library_map_dependency TARGET FIND_DEPENDENCY_CALL)
set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEPENDENCY_MAP_${TARGET} "${FIND_DEPENDENCY_CALL}")
# Track all mapped targets for cleanup in tests
set_property(GLOBAL APPEND PROPERTY _CPP_LIBRARY_ALL_MAPPED_TARGETS "${TARGET}")
endfunction()
# Generates find_dependency() calls for target's INTERFACE link libraries
# - Precondition: TARGET_NAME specifies existing target with INTERFACE_LINK_LIBRARIES, dependency provider installed
# - Postcondition: OUTPUT_VAR contains newline-separated find_dependency() calls for public dependencies
# - Uses dependency tracking data from cpp_library_dependency_provider to generate accurate calls
# - Automatically includes version constraints from tracked find_package() calls
# - Common system packages (Threads, OpenMP, etc.) are handled automatically
# - Merges multiple components of the same package into a single find_dependency() call with COMPONENTS
# - cpp_library_map_dependency() can override tracked dependencies for non-namespaced targets or special cases
# - cpp-library dependencies: namespace::namespace → find_dependency(namespace VERSION), namespace::component → find_dependency(namespace-component VERSION)
# - External dependencies: name::name → find_dependency(name VERSION), name::component → find_dependency(name VERSION)
function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE)
get_target_property(LINK_LIBS ${TARGET_NAME} INTERFACE_LINK_LIBRARIES)
if(NOT LINK_LIBS)
set(${OUTPUT_VAR} "" PARENT_SCOPE)
return()
endif()
# Process each linked library
foreach(LIB IN LISTS LINK_LIBS)
# Handle BUILD_INTERFACE generator expressions
# When re-exporting dependencies from external packages, they must be wrapped in BUILD_INTERFACE
# to avoid CMake export errors, but we still want to track them for find_dependency()
if(LIB MATCHES "^\\$<BUILD_INTERFACE:([^>]+)>$")
set(EXTRACTED_TARGET "${CMAKE_MATCH_1}")
# Only process if it's a namespaced target (external dependency)
# Non-namespaced targets in BUILD_INTERFACE are local build targets
if(EXTRACTED_TARGET MATCHES "::")
set(LIB "${EXTRACTED_TARGET}")
message(DEBUG "cpp-library: Extracted ${LIB} from BUILD_INTERFACE generator expression")
else()
# Skip non-namespaced BUILD_INTERFACE targets (local build targets)
message(DEBUG "cpp-library: Skipping non-namespaced BUILD_INTERFACE target: ${EXTRACTED_TARGET}")
continue()
endif()
elseif(LIB MATCHES "^\\$<")
# Skip other generator expressions (INSTALL_INTERFACE, etc.)
continue()
endif()
set(FIND_DEP_CALL "")
# Check for custom mapping first (allows overrides for non-namespaced targets)
get_property(CUSTOM_MAPPING GLOBAL PROPERTY _CPP_LIBRARY_DEPENDENCY_MAP_${LIB})
if(CUSTOM_MAPPING)
# Use explicit custom mapping
set(FIND_DEP_CALL "${CUSTOM_MAPPING}")
message(DEBUG "cpp-library: Using custom mapping for ${LIB}: ${CUSTOM_MAPPING}")
else()
# Use tracked dependency data from provider
_cpp_library_resolve_dependency("${LIB}" "${NAMESPACE}" FIND_DEP_CALL)
endif()
# Add the dependency to the merged list
if(FIND_DEP_CALL)
_cpp_library_add_dependency("${FIND_DEP_CALL}")
endif()
endforeach()
# Generate merged find_dependency() calls
_cpp_library_get_merged_dependencies(DEPENDENCY_LINES)
set(${OUTPUT_VAR} "${DEPENDENCY_LINES}" PARENT_SCOPE)
endfunction()
# Resolve dependency using tracked provider data
# - Precondition: LIB is a target name, NAMESPACE is the project namespace
# - Postcondition: OUTPUT_VAR contains find_dependency() call syntax or error is raised
function(_cpp_library_resolve_dependency LIB NAMESPACE OUTPUT_VAR)
# Parse the target name to extract package name
if(LIB MATCHES "^([^:]+)::(.+)$")
set(PKG_NAME "${CMAKE_MATCH_1}")
set(COMPONENT "${CMAKE_MATCH_2}")
# Determine the package name for lookup
if(PKG_NAME STREQUAL NAMESPACE)
# Internal cpp-library dependency
if(PKG_NAME STREQUAL COMPONENT)
set(FIND_PACKAGE_NAME "${PKG_NAME}")
else()
set(FIND_PACKAGE_NAME "${PKG_NAME}-${COMPONENT}")
endif()
else()
# External dependency - use package name
set(FIND_PACKAGE_NAME "${PKG_NAME}")
endif()
# Look up tracked dependency data
get_property(TRACKED_CALL GLOBAL PROPERTY "_CPP_LIBRARY_TRACKED_DEP_${FIND_PACKAGE_NAME}")
if(TRACKED_CALL)
# Found tracked data - use it directly
set(${OUTPUT_VAR} "${TRACKED_CALL}" PARENT_SCOPE)
message(DEBUG "cpp-library: Using tracked dependency for ${LIB}: ${TRACKED_CALL}")
else()
# Not tracked - check if it's a system package
if(FIND_PACKAGE_NAME IN_LIST _CPP_LIBRARY_SYSTEM_PACKAGES)
set(${OUTPUT_VAR} "${FIND_PACKAGE_NAME}" PARENT_SCOPE)
message(DEBUG "cpp-library: System package ${FIND_PACKAGE_NAME} (no tracking needed)")
else()
# Not tracked and not a system package - check if provider is installed
get_property(PROVIDER_INSTALLED GLOBAL PROPERTY _CPP_LIBRARY_PROVIDER_INSTALLED)
if(NOT PROVIDER_INSTALLED)
_cpp_library_example_usage(EXAMPLE)
message(FATAL_ERROR
"cpp-library: Dependency provider not installed.\n"
"You must call cpp_library_enable_dependency_tracking() before project().\n"
"\n"
"Example:\n"
"${EXAMPLE}\n"
)
else()
# Provider is installed but dependency wasn't tracked
# Check if we're in strict install validation mode
get_property(IN_INSTALL_MODE GLOBAL PROPERTY _CPP_LIBRARY_IN_INSTALL_MODE)
if(IN_INSTALL_MODE)
# Strict mode during install: error out
message(FATAL_ERROR
"cpp-library: Cannot install - Dependency ${LIB} (package: ${FIND_PACKAGE_NAME}) was not tracked.\n"
"\n"
"The dependency provider is installed, but this dependency was not captured.\n"
"Common causes:\n"
" - find_package() or CPMAddPackage() was called in a subdirectory\n"
" - Dependency was added before project() (must be after)\n"
"\n"
"Solution: Ensure dependencies are declared after project() in the top-level CMakeLists.txt.\n"
"\n"
"Correct order:\n"
" cpp_library_enable_dependency_tracking()\n"
" project(my-library)\n"
" cpp_library_setup(...)\n"
" find_package(SomePackage) # or CPMAddPackage(...)\n"
" target_link_libraries(...)\n"
)
else()
# Lenient mode during configure: notify and use fallback
# Print header message before first untracked dependency
get_property(HEADER_PRINTED GLOBAL PROPERTY _CPP_LIBRARY_UNTRACKED_HEADER_PRINTED)
if(NOT HEADER_PRINTED)
message(STATUS "cpp-library: Untracked dependencies (see: https://github.com/stlab/cpp-library#untracked-dependencies)")
set_property(GLOBAL PROPERTY _CPP_LIBRARY_UNTRACKED_HEADER_PRINTED TRUE)
endif()
# Print concise message about this specific dependency
message(STATUS "cpp-library: Dependency ${LIB} (package: ${FIND_PACKAGE_NAME}) was not tracked.")
# Track this as an unverified dependency for install-time validation
set_property(GLOBAL APPEND PROPERTY _CPP_LIBRARY_UNVERIFIED_DEPS
"${LIB}|${FIND_PACKAGE_NAME}")
# Use a reasonable fallback for development builds
set(${OUTPUT_VAR} "${FIND_PACKAGE_NAME}" PARENT_SCOPE)
return()
endif()
endif()
endif()
endif()
else()
# Non-namespaced target - requires explicit mapping
message(FATAL_ERROR
"cpp-library: Non-namespaced dependency '${LIB}' cannot be automatically resolved.\n"
"\n"
"Non-namespaced targets (like 'opencv_core') don't indicate which package they came from.\n"
"You must use cpp_library_map_dependency() to map the target to its package:\n"
"\n"
" cpp_library_map_dependency(\"${LIB}\" \"<PACKAGE_NAME> <VERSION>\")\n"
"\n"
"For example, if ${LIB} comes from OpenCV:\n"
" find_package(OpenCV 4.5.0 REQUIRED)\n"
" cpp_library_map_dependency(\"${LIB}\" \"OpenCV 4.5.0\")\n"
"\n"
"Add this mapping after find_package() or CPMAddPackage() in your CMakeLists.txt.\n"
)
endif()
endfunction()
# Helper function to parse and store a dependency for later merging
# - Parses find_dependency() arguments to extract package, version, and components
# - Stores in global properties for merging by _cpp_library_get_merged_dependencies()
function(_cpp_library_add_dependency FIND_DEP_ARGS)
# Parse: PackageName [Version] [COMPONENTS component1 component2 ...] [other args]
string(REGEX MATCH "^([^ ]+)" PKG_NAME "${FIND_DEP_ARGS}")
# Remove package name from args - use string(REPLACE) for literal match
string(LENGTH "${PKG_NAME}" PKG_NAME_LEN)
string(LENGTH "${FIND_DEP_ARGS}" TOTAL_LEN)
if(TOTAL_LEN GREATER PKG_NAME_LEN)
math(EXPR START_POS "${PKG_NAME_LEN}")
string(SUBSTRING "${FIND_DEP_ARGS}" ${START_POS} -1 REMAINING_ARGS)
string(STRIP "${REMAINING_ARGS}" REMAINING_ARGS)
else()
set(REMAINING_ARGS "")
endif()
# Extract version (first token that looks like a semantic version number: major.minor[.patch]...)
set(VERSION "")
if(REMAINING_ARGS MATCHES "^([0-9]+\\.[0-9]+(\\.[0-9]+)*)")
set(VERSION "${CMAKE_MATCH_1}")
# Remove version from args - use substring to avoid regex issues with dots
string(LENGTH "${VERSION}" VERSION_LEN)
string(LENGTH "${REMAINING_ARGS}" TOTAL_LEN)
if(TOTAL_LEN GREATER VERSION_LEN)
math(EXPR START_POS "${VERSION_LEN}")
string(SUBSTRING "${REMAINING_ARGS}" ${START_POS} -1 REMAINING_ARGS)
string(STRIP "${REMAINING_ARGS}" REMAINING_ARGS)
else()
set(REMAINING_ARGS "")
endif()
endif()
# Extract COMPONENTS if present
set(COMPONENTS "")
set(BASE_ARGS "${REMAINING_ARGS}")
if(REMAINING_ARGS MATCHES "COMPONENTS +(.+)")
set(COMPONENTS_PART "${CMAKE_MATCH_1}")
# Extract just the component names (until next keyword or end)
string(REGEX REPLACE " +(REQUIRED|OPTIONAL_COMPONENTS|CONFIG|NO_MODULE).*$" "" COMPONENTS "${COMPONENTS_PART}")
# Remove COMPONENTS and component names from base args
# Escape all regex special characters in COMPONENTS for safe regex use
# Must escape: \ first (to avoid double-escaping), then all other special chars
string(REPLACE "\\" "\\\\" COMPONENTS_ESCAPED "${COMPONENTS}")
string(REPLACE "." "\\." COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "*" "\\*" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "+" "\\+" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "?" "\\?" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "^" "\\^" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "$" "\\$" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "|" "\\|" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "(" "\\(" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE ")" "\\)" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "[" "\\[" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "]" "\\]" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "{" "\\{" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REPLACE "}" "\\}" COMPONENTS_ESCAPED "${COMPONENTS_ESCAPED}")
string(REGEX REPLACE "COMPONENTS +${COMPONENTS_ESCAPED}" "" BASE_ARGS "${REMAINING_ARGS}")
string(STRIP "${COMPONENTS}" COMPONENTS)
endif()
string(STRIP "${BASE_ARGS}" BASE_ARGS)
# Create a key for this package (package_name + version + base_args)
# Use <|> as delimiter (unlikely to appear in package arguments)
set(PKG_KEY "${PKG_NAME}<|>${VERSION}<|>${BASE_ARGS}")
# Get or initialize the global list of package keys
get_property(PKG_KEYS GLOBAL PROPERTY _CPP_LIBRARY_PKG_KEYS)
if(NOT PKG_KEY IN_LIST PKG_KEYS)
set_property(GLOBAL APPEND PROPERTY _CPP_LIBRARY_PKG_KEYS "${PKG_KEY}")
endif()
# Append components to this package key
if(COMPONENTS)
get_property(EXISTING_COMPONENTS GLOBAL PROPERTY "_CPP_LIBRARY_PKG_COMPONENTS_${PKG_KEY}")
if(EXISTING_COMPONENTS)
set_property(GLOBAL PROPERTY "_CPP_LIBRARY_PKG_COMPONENTS_${PKG_KEY}" "${EXISTING_COMPONENTS} ${COMPONENTS}")
else()
set_property(GLOBAL PROPERTY "_CPP_LIBRARY_PKG_COMPONENTS_${PKG_KEY}" "${COMPONENTS}")
endif()
endif()
endfunction()
# Helper function to generate merged find_dependency() calls
# - Reads stored dependency info and merges components for the same package
# - Returns newline-separated find_dependency() calls
function(_cpp_library_get_merged_dependencies OUTPUT_VAR)
get_property(PKG_KEYS GLOBAL PROPERTY _CPP_LIBRARY_PKG_KEYS)
set(RESULT "")
foreach(PKG_KEY IN LISTS PKG_KEYS)
# Parse the key: package_name<|>version<|>base_args
# Use <|> as delimiter (unlikely to appear in package arguments)
string(REPLACE "<|>" ";" KEY_PARTS "${PKG_KEY}")
list(LENGTH KEY_PARTS PARTS_COUNT)
if(PARTS_COUNT GREATER_EQUAL 3)
list(GET KEY_PARTS 0 PKG_NAME)
list(GET KEY_PARTS 1 VERSION)
# Get remaining parts in case BASE_ARGS was split (shouldn't happen with <|> delimiter)
list(SUBLIST KEY_PARTS 2 -1 BASE_ARGS_PARTS)
list(JOIN BASE_ARGS_PARTS "<|>" BASE_ARGS)
else()
message(WARNING "Invalid package key format: ${PKG_KEY}")
continue()
endif()
# Build the find_dependency() call
set(FIND_CALL "${PKG_NAME}")
if(VERSION)
string(APPEND FIND_CALL " ${VERSION}")
endif()
# Add components if any
get_property(COMPONENTS GLOBAL PROPERTY "_CPP_LIBRARY_PKG_COMPONENTS_${PKG_KEY}")
if(COMPONENTS)
# Remove duplicates from components list
string(REPLACE " " ";" COMP_LIST "${COMPONENTS}")
list(REMOVE_DUPLICATES COMP_LIST)
list(JOIN COMP_LIST " " UNIQUE_COMPONENTS)
string(APPEND FIND_CALL " COMPONENTS ${UNIQUE_COMPONENTS}")
endif()
if(BASE_ARGS)
string(APPEND FIND_CALL " ${BASE_ARGS}")
endif()
list(APPEND RESULT "find_dependency(${FIND_CALL})")
# Clean up this key's component list
set_property(GLOBAL PROPERTY "_CPP_LIBRARY_PKG_COMPONENTS_${PKG_KEY}")
endforeach()
# Clean up the keys list
set_property(GLOBAL PROPERTY _CPP_LIBRARY_PKG_KEYS "")
if(RESULT)
list(JOIN RESULT "\n" RESULT_STR)
else()
set(RESULT_STR "")
endif()
set(${OUTPUT_VAR} "${RESULT_STR}" PARENT_SCOPE)
endfunction()
# Deferred function to generate Config.cmake after all target_link_libraries() calls
# This runs at the end of CMakeLists.txt processing via cmake_language(DEFER)
function(_cpp_library_deferred_generate_config)
# Include required modules
include(CMakePackageConfigHelpers)
# Retrieve stored arguments from global properties
get_property(ARG_NAME GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_NAME)
get_property(ARG_PACKAGE_NAME GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_PACKAGE_NAME)
get_property(ARG_VERSION GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_VERSION)
get_property(ARG_NAMESPACE GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_NAMESPACE)
get_property(CPP_LIBRARY_ROOT GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_ROOT)
get_property(BINARY_DIR GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_BINARY_DIR)
# Now generate find_dependency() calls with complete link information
_cpp_library_generate_dependencies(PACKAGE_DEPENDENCIES ${ARG_NAME} ${ARG_NAMESPACE})
# Generate package version file
write_basic_package_version_file(
"${BINARY_DIR}/${ARG_PACKAGE_NAME}ConfigVersion.cmake"
VERSION ${ARG_VERSION}
COMPATIBILITY SameMajorVersion
)
# Generate package config file from template
configure_file(
"${CPP_LIBRARY_ROOT}/templates/Config.cmake.in"
"${BINARY_DIR}/${ARG_PACKAGE_NAME}Config.cmake"
@ONLY
)
# Save unverified dependencies to a file for install-time validation
get_property(UNVERIFIED_DEPS GLOBAL PROPERTY _CPP_LIBRARY_UNVERIFIED_DEPS)
if(UNVERIFIED_DEPS)
set(UNVERIFIED_FILE "${BINARY_DIR}/${ARG_PACKAGE_NAME}_unverified_deps.cmake")
file(WRITE "${UNVERIFIED_FILE}" "# Unverified dependencies for ${ARG_PACKAGE_NAME}\n")
file(APPEND "${UNVERIFIED_FILE}" "set(_UNVERIFIED_DEPS_LIST [[${UNVERIFIED_DEPS}]])\n")
set_property(GLOBAL PROPERTY _CPP_LIBRARY_HAS_UNVERIFIED_DEPS TRUE)
else()
set_property(GLOBAL PROPERTY _CPP_LIBRARY_HAS_UNVERIFIED_DEPS FALSE)
endif()
message(STATUS "cpp-library: Generated ${ARG_PACKAGE_NAME}Config.cmake with dependencies")
endfunction()
# Configures CMake install rules for library target and package config files.
# - Precondition: NAME, PACKAGE_NAME, VERSION, and NAMESPACE specified; target NAME exists
# - Postcondition: install rules created for target, config files, and export with NAMESPACE:: prefix
# - Supports header-only (INTERFACE) and compiled libraries, uses SameMajorVersion compatibility
# - Installation can be controlled via ${NAMESPACE}_INSTALL option (defaults to PROJECT_IS_TOP_LEVEL)
function(_cpp_library_setup_install)
set(oneValueArgs
NAME # Target name (e.g., "stlab-enum-ops")
PACKAGE_NAME # Package name for find_package() (e.g., "stlab-enum-ops")
VERSION # Version string (e.g., "1.2.3")
NAMESPACE # Namespace for alias (e.g., "stlab")
)
set(multiValueArgs
HEADERS # List of header file paths (for FILE_SET support check)
)
cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
# Validate required arguments
if(NOT ARG_NAME)
message(FATAL_ERROR "_cpp_library_setup_install: NAME is required")
endif()
if(NOT ARG_PACKAGE_NAME)
message(FATAL_ERROR "_cpp_library_setup_install: PACKAGE_NAME is required")
endif()
if(NOT ARG_VERSION)
message(FATAL_ERROR "_cpp_library_setup_install: VERSION is required")
endif()
if(NOT ARG_NAMESPACE)
message(FATAL_ERROR "_cpp_library_setup_install: NAMESPACE is required")
endif()
# Define installation option with PROJECT_IS_TOP_LEVEL as default
# This allows explicit control: -D${NAMESPACE}_INSTALL=ON/OFF
# Upper-case the namespace for the option name
string(TOUPPER "${ARG_NAMESPACE}" NAMESPACE_UPPER)
option(${NAMESPACE_UPPER}_INSTALL "Enable installation of ${ARG_PACKAGE_NAME}" ${PROJECT_IS_TOP_LEVEL})
# Check if installation is enabled
if(NOT ${NAMESPACE_UPPER}_INSTALL)
message(STATUS "cpp-library: Installation disabled for ${ARG_PACKAGE_NAME} (${NAMESPACE_UPPER}_INSTALL=OFF)")
return()
endif()
# Include required CMake modules (deferred from top-level to avoid requiring project() before include)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
# Install the library target
# For header-only libraries (INTERFACE), this installs the target metadata
# For compiled libraries, this installs the library files and headers
if(ARG_HEADERS)
# Install with FILE_SET for modern header installation
install(TARGETS ${ARG_NAME}
EXPORT ${ARG_NAME}Targets
FILE_SET headers DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
else()
# Install without FILE_SET (fallback for edge cases)
install(TARGETS ${ARG_NAME}
EXPORT ${ARG_NAME}Targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
endif()
# Defer Config.cmake generation until end of CMakeLists.txt processing
# This ensures all target_link_libraries() calls have been made first
# Store arguments in global properties for the deferred function
set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_NAME "${ARG_NAME}")
set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_PACKAGE_NAME "${ARG_PACKAGE_NAME}")
set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_VERSION "${ARG_VERSION}")
set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_NAMESPACE "${ARG_NAMESPACE}")
set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_ROOT "${CPP_LIBRARY_ROOT}")
set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}")
# Defer install validation and file installation setup until after config generation
# This ensures:
# 1. The unverified deps file is created first
# 2. Validation install code is registered before export/config file installation
# 3. At install time, validation runs before any config files are written
# Note: DEFER uses LIFO ordering, so register validation first (runs last)
cmake_language(DEFER DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
CALL _cpp_library_setup_install_validation)
# Register config generation second so it runs first (LIFO) and sets properties
cmake_language(DEFER DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
CALL _cpp_library_deferred_generate_config)
endfunction()
# Deferred function to setup install validation after config generation
# This runs after _cpp_library_deferred_generate_config() has created the unverified deps file
# Registers validation BEFORE export/config file installation to prevent broken configs from being written
function(_cpp_library_setup_install_validation)
# Retrieve stored arguments from global properties (set by _cpp_library_setup_install)
get_property(NAME GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_NAME)
get_property(PACKAGE_NAME GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_PACKAGE_NAME)
get_property(NAMESPACE GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_NAMESPACE)
get_property(BINARY_DIR GLOBAL PROPERTY _CPP_LIBRARY_DEFERRED_INSTALL_BINARY_DIR)
# Check if there are unverified dependencies
get_property(HAS_UNVERIFIED GLOBAL PROPERTY _CPP_LIBRARY_HAS_UNVERIFIED_DEPS)
if(HAS_UNVERIFIED)
set(UNVERIFIED_FILE "${BINARY_DIR}/${PACKAGE_NAME}_unverified_deps.cmake")
# Add install-time validation to ensure all dependencies are properly tracked
# This runs before config files are installed and will fail if untracked dependencies exist
install(CODE "
message(STATUS \"cpp-library: Validating tracked dependencies for ${PACKAGE_NAME}...\")
# Load the list of unverified dependencies
include(\"${UNVERIFIED_FILE}\")
if(_UNVERIFIED_DEPS_LIST)
# Parse the unverified dependencies list
string(REPLACE \";\" \"\\n - \" FORMATTED_DEPS \"\${_UNVERIFIED_DEPS_LIST}\")
string(REGEX REPLACE \"\\\\|[a-zA-Z0-9_:.\\\\- ]+\" \"\" FORMATTED_DEPS \"\${FORMATTED_DEPS}\")
message(FATAL_ERROR
\"cpp-library: Cannot install ${PACKAGE_NAME} - untracked dependencies detected:\\n\"
\" - \${FORMATTED_DEPS}\\n\"
\"\\n\"
\"These dependencies were not captured by the dependency provider.\\n\"
\"Common causes:\\n\"
\" - find_package() or CPMAddPackage() was called in a subdirectory\\n\"
\" - Dependency was added before project() (must be after)\\n\"
\"\\n\"
\"Solution: Ensure dependencies are declared after project() in the top-level CMakeLists.txt.\\n\"
\"Or use cpp_library_map_dependency() to manually register each dependency.\\n\"
)
endif()
message(STATUS \"cpp-library: Dependency validation passed for ${PACKAGE_NAME}\")
")
else()
install(CODE "
message(STATUS \"cpp-library: All dependencies properly tracked for ${PACKAGE_NAME}\")
")
endif()
# Now register the export and config file installations AFTER validation
# This ensures validation runs first at install time, preventing broken configs from being written
# Install export targets with namespace
# This allows downstream projects to use find_package(package-name)
# and link against namespace::target
install(EXPORT ${NAME}Targets
FILE ${PACKAGE_NAME}Targets.cmake
NAMESPACE ${NAMESPACE}::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME}
)
# Install package config and version files
install(FILES
"${BINARY_DIR}/${PACKAGE_NAME}Config.cmake"
"${BINARY_DIR}/${PACKAGE_NAME}ConfigVersion.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME}
)
endfunction()