diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57403d2..b5731dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,12 @@ jobs: - name: Run provider merging tests run: cmake -P tests/install/test_provider_merge.cmake + - name: Run nested installation round-trip tests + run: cmake -P tests/install/test_nested_install.cmake + + - name: Run target type tests + run: cmake -P tests/setup/test_target_type.cmake + - name: Run setup.cmake version resolution test run: cmake -P tests/setup/test_setup_version_resolution.cmake diff --git a/README.md b/README.md index 1687a00..c4d3178 100644 --- a/README.md +++ b/README.md @@ -177,21 +177,43 @@ cmake --install build/install --prefix /opt/mylib The `install` preset enables `CPM_USE_LOCAL_PACKAGES`, which verifies your generated Config.cmake works correctly. See the [CPM.cmake documentation](https://github.com/cpm-cmake/CPM.cmake#cpm_use_local_packages) for more about using installed packages. -**Controlling installation**: The `${NAMESPACE}_INSTALL` option controls whether installation is enabled (defaults to `PROJECT_IS_TOP_LEVEL`). Use `-D${NAMESPACE}_INSTALL=ON/OFF` to override: +**Controlling installation**: The uppercase `${NAMESPACE}_INSTALL` option controls whether installation is enabled (defaults to `PROJECT_IS_TOP_LEVEL`). Use `-D${NAMESPACE}_INSTALL=ON/OFF` to override: ```bash cmake -DSTLAB_INSTALL=OFF -B build # Disable install for top-level project cmake -DSTLAB_INSTALL=ON -B build # Enable install for non-top-level (e.g., via CPM) ``` -**Re-exporting CPM dependencies:** When re-exporting dependencies from `CPMAddPackage`, wrap them in `BUILD_INTERFACE` to avoid export errors (CPM creates non-IMPORTED targets that can't be exported): +For independent packages sharing a namespace, specify an option name with `INSTALL_OPTION`: + +```cmake +cpp_library_setup( + DESCRIPTION "Execution primitives" + NAMESPACE stlab + HEADERS execution.hpp + INSTALL_OPTION STLAB_EXECUTION_INSTALL +) +``` + +This package then uses `STLAB_EXECUTION_INSTALL`, independently of `STLAB_INSTALL` or other +packages' custom options. Each custom option defaults to that project's `PROJECT_IS_TOP_LEVEL`. +Omitting `INSTALL_OPTION` preserves the shared namespace-level option and its existing behavior. +Specifying `INSTALL_OPTION` without a value is an error. + +**Re-exporting CPM dependencies:** When re-exporting dependencies from `CPMAddPackage`, use separate build and install interfaces. CPM creates non-IMPORTED targets that may not be included in your install export: ```cmake CPMAddPackage("gh:other-org/some-package@1.0.0") -target_link_libraries(my-library INTERFACE $) +target_link_libraries(my-library INTERFACE + $ + $>) ``` -cpp-library automatically extracts these and generates appropriate `find_dependency()` calls. Dependencies from `find_package()` and system libraries don't need `BUILD_INTERFACE`. +cpp-library extracts the build-interface dependency to generate the appropriate `find_dependency()` call. +The install interface preserves transitive linkage; its nested `$<1:...>` expression defers +target resolution to the downstream consumer, allowing the dependency to be installed separately. +Using only `BUILD_INTERFACE` loads the dependency's package but drops its linkage from the +installed target. Dependencies from `find_package()` and system libraries don't need these wrappers. #### Dependency Handling in Installed Packages @@ -334,12 +356,14 @@ cpp_library_setup( # Source specification for non-header-only libraries SOURCES source_list # List of source filenames (e.g., "your_library.cpp", omit for header-only libraries) + [LIBRARY_TYPE STATIC|SHARED] # Explicit compiled target type (requires SOURCES) # Optional features [EXAMPLES example_list] # Example source files to build (e.g., "example.cpp example_fail.cpp") [TESTS test_list] # Test source files to build (e.g., "tests.cpp") [DOCS_EXCLUDE_SYMBOLS symbols] # Symbols to exclude from docs [REQUIRES_CPP_VERSION 17|20|23] # C++ version (default: 17) + [INSTALL_OPTION option_name] # Independent installation control (default: uppercase NAMESPACE_INSTALL) ) ``` @@ -348,7 +372,8 @@ cpp_library_setup( - The project name is automatically taken from `PROJECT_NAME` (set by the `project()` command). You must call `project(your-library)` before `cpp_library_setup()`. - **If you specify `TESTS` or `EXAMPLES`**, call `include(CTest)` after `project()` and before `cpp_library_setup()`. - Version is automatically detected from git tags (see [Version Management](#version-management) for overrides). -- Installation is controlled by the `${NAMESPACE}_INSTALL` option, which defaults to `PROJECT_IS_TOP_LEVEL`. +- Installation is controlled by `INSTALL_OPTION`, or the uppercase `${NAMESPACE}_INSTALL` option + when omitted. Options default to `PROJECT_IS_TOP_LEVEL`. ### Target Naming @@ -405,7 +430,12 @@ All file specifications use filenames only, automatically placed in standard dir ### Library Types - **Header-only**: Specify only `HEADERS`, omit `SOURCES` -- **Compiled**: Specify both `HEADERS` and `SOURCES` (builds as static by default, set `BUILD_SHARED_LIBS=ON` for shared) +- **Compiled**: Specify both `HEADERS` and `SOURCES`. When `LIBRARY_TYPE` is omitted, + the target respects `BUILD_SHARED_LIBS` (static by default; shared when `ON`). + Use `LIBRARY_TYPE STATIC` or `LIBRARY_TYPE SHARED` to select the target type + explicitly without changing the parent project's `BUILD_SHARED_LIBS`. + A `LIBRARY_TYPE` keyword without a value, other types, and explicit types + without `SOURCES` are diagnosed. ## Reference @@ -503,7 +533,9 @@ During configuration, you may see messages like: ```cmake # In your top-level CMakeLists.txt (after project()) CPMAddPackage("gh:stlab/stlab-copy-on-write@1.1.0") - target_link_libraries(my-library INTERFACE $) + target_link_libraries(my-library INTERFACE + $ + $>) ``` 2. **Manually register dependencies**: @@ -512,7 +544,9 @@ During configuration, you may see messages like: # After adding the dependency CPMAddPackage("gh:stlab/stlab-copy-on-write@1.1.0") cpp_library_map_dependency("stlab::copy-on-write" "stlab-copy-on-write 1.1.0") - target_link_libraries(my-library INTERFACE $) + target_link_libraries(my-library INTERFACE + $ + $>) ``` 3. **Use CPM_USE_LOCAL_PACKAGES**: Install dependencies first, then build with local packages: @@ -558,12 +592,22 @@ To use cpp-library from a specific commit: CPMAddPackage("gh:stlab/cpp-library#65dbed9fff9a0331355bd51dc1e8156262390154") ``` -To run cpp-library's unit tests for dependency mapping and installation: +To run cpp-library's script regressions (C++ fixtures require Ninja and a configured compiler; +on Windows, use a Visual Studio developer environment): ```bash cmake -P tests/install/CMakeLists.txt +cmake -P tests/install/test_provider_merge.cmake +cmake -P tests/install/test_nested_install.cmake +cmake -P tests/setup/test_target_type.cmake +cmake -P tests/setup/test_setup_version_resolution.cmake ``` +The nested-install regression configures, builds, installs, and runs a downstream consumer, +checks independent and legacy installation controls, and verifies package-local dependency validation. +The consumer links only the parent target and calls its API, which requires a compiled static +leaf library, so missing installed transitive linkage causes a link failure. + See `tests/install/README.md` for details. ## License diff --git a/cmake/cpp-library-ci.cmake b/cmake/cpp-library-ci.cmake index a7dfec9..348159a 100644 --- a/cmake/cpp-library-ci.cmake +++ b/cmake/cpp-library-ci.cmake @@ -16,8 +16,8 @@ function(_cpp_library_setup_ci PACKAGE_NAME force_init) # @1.13.0 set(CI_ACTION_MSVC_DEV_CMD "ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756") # [DEPENDENCY] https://github.com/ssciwr/doxygen-install/releases - # @2.0.1 - set(CI_ACTION_DOXYGEN_INSTALL "ssciwr/doxygen-install@329d88f5a303066a5bd006db7516b1925b86350e") + # @2.0.3 + set(CI_ACTION_DOXYGEN_INSTALL "ssciwr/doxygen-install@fa3e94d34131d3990b6b8ba939ab982964aa3758") # [DEPENDENCY] https://github.com/actions/configure-pages/releases set(CI_ACTION_CONFIGURE_PAGES "actions/configure-pages@v6") # [DEPENDENCY] https://github.com/actions/upload-pages-artifact/releases diff --git a/cmake/cpp-library-docs.cmake b/cmake/cpp-library-docs.cmake index 371d401..72cc1bc 100644 --- a/cmake/cpp-library-docs.cmake +++ b/cmake/cpp-library-docs.cmake @@ -23,7 +23,7 @@ function(_cpp_library_setup_docs) # https://github.com/jothepro/doxygen-awesome-css CPMAddPackage( # [DEPENDENCY] https://github.com/jothepro/doxygen-awesome-css/releases - URI gh:jothepro/doxygen-awesome-css@2.4.2 + URI gh:jothepro/doxygen-awesome-css@2.5.0 DOWNLOAD_ONLY YES ) diff --git a/cmake/cpp-library-install.cmake b/cmake/cpp-library-install.cmake index 374635a..8558ce6 100644 --- a/cmake/cpp-library-install.cmake +++ b/cmake/cpp-library-install.cmake @@ -39,6 +39,7 @@ 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 +# - Optional fourth argument receives only this target's unverified 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 @@ -47,6 +48,10 @@ endfunction() # - 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) + set(UNVERIFIED_DEPS "") + if(ARGC GREATER 3) + set(${ARGV3} "" PARENT_SCOPE) + endif() get_target_property(LINK_LIBS ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) if(NOT LINK_LIBS) @@ -87,7 +92,10 @@ function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE) 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) + _cpp_library_resolve_dependency("${LIB}" "${NAMESPACE}" FIND_DEP_CALL UNVERIFIED_DEP) + if(UNVERIFIED_DEP) + list(APPEND UNVERIFIED_DEPS "${UNVERIFIED_DEP}") + endif() endif() # Add the dependency to the merged list @@ -100,12 +108,19 @@ function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE) _cpp_library_get_merged_dependencies(DEPENDENCY_LINES) set(${OUTPUT_VAR} "${DEPENDENCY_LINES}" PARENT_SCOPE) + if(ARGC GREATER 3) + set(${ARGV3} "${UNVERIFIED_DEPS}" PARENT_SCOPE) + endif() 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 +# - Optional fourth argument receives the unverified dependency, or an empty string function(_cpp_library_resolve_dependency LIB NAMESPACE OUTPUT_VAR) + if(ARGC GREATER 3) + set(${ARGV3} "" PARENT_SCOPE) + endif() # Parse the target name to extract package name if(LIB MATCHES "^([^:]+)::(.+)$") set(PKG_NAME "${CMAKE_MATCH_1}") @@ -185,8 +200,9 @@ function(_cpp_library_resolve_dependency LIB NAMESPACE OUTPUT_VAR) 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}") + if(ARGC GREATER 3) + set(${ARGV3} "${LIB}|${FIND_PACKAGE_NAME}" PARENT_SCOPE) + endif() # Use a reasonable fallback for development builds set(${OUTPUT_VAR} "${FIND_PACKAGE_NAME}" PARENT_SCOPE) @@ -362,20 +378,20 @@ 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) +function(_cpp_library_deferred_generate_config TARGET_NAME) # 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) + set(ARG_NAME "${TARGET_NAME}") + get_target_property(ARG_PACKAGE_NAME ${TARGET_NAME} _CPP_LIBRARY_INSTALL_PACKAGE_NAME) + get_target_property(ARG_VERSION ${TARGET_NAME} _CPP_LIBRARY_INSTALL_VERSION) + get_target_property(ARG_NAMESPACE ${TARGET_NAME} _CPP_LIBRARY_INSTALL_NAMESPACE) + get_target_property(CPP_LIBRARY_ROOT ${TARGET_NAME} _CPP_LIBRARY_INSTALL_ROOT) + get_target_property(BINARY_DIR ${TARGET_NAME} _CPP_LIBRARY_INSTALL_BINARY_DIR) # Now generate find_dependency() calls with complete link information - _cpp_library_generate_dependencies(PACKAGE_DEPENDENCIES ${ARG_NAME} ${ARG_NAMESPACE}) + _cpp_library_generate_dependencies(PACKAGE_DEPENDENCIES ${ARG_NAME} ${ARG_NAMESPACE} + UNVERIFIED_DEPS) # Generate package version file write_basic_package_version_file( @@ -392,14 +408,12 @@ function(_cpp_library_deferred_generate_config) ) # Save unverified dependencies to a file for install-time validation - get_property(UNVERIFIED_DEPS GLOBAL PROPERTY _CPP_LIBRARY_UNVERIFIED_DEPS) + set_property(TARGET ${TARGET_NAME} PROPERTY _CPP_LIBRARY_INSTALL_UNVERIFIED_DEPS + "${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") @@ -409,19 +423,24 @@ endfunction() # - 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) +# - Installation is controlled via INSTALL_OPTION or uppercase ${NAMESPACE}_INSTALL +# (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") + INSTALL_OPTION # Optional custom installation option name ) set(multiValueArgs HEADERS # List of header file paths (for FILE_SET support check) ) cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if("INSTALL_OPTION" IN_LIST ARG_KEYWORDS_MISSING_VALUES) + message(FATAL_ERROR "_cpp_library_setup_install: INSTALL_OPTION requires a value") + endif() # Validate required arguments if(NOT ARG_NAME) @@ -437,15 +456,17 @@ function(_cpp_library_setup_install) 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}) + if(DEFINED ARG_INSTALL_OPTION) + set(install_option "${ARG_INSTALL_OPTION}") + else() + string(TOUPPER "${ARG_NAMESPACE}" NAMESPACE_UPPER) + set(install_option "${NAMESPACE_UPPER}_INSTALL") + endif() + option(${install_option} "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)") + if(NOT ${install_option}) + message(STATUS "cpp-library: Installation disabled for ${ARG_PACKAGE_NAME} (${install_option}=OFF)") return() endif() @@ -475,45 +496,39 @@ function(_cpp_library_setup_install) ) 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) + set_target_properties(${ARG_NAME} PROPERTIES + _CPP_LIBRARY_INSTALL_PACKAGE_NAME "${ARG_PACKAGE_NAME}" + _CPP_LIBRARY_INSTALL_VERSION "${ARG_VERSION}" + _CPP_LIBRARY_INSTALL_NAMESPACE "${ARG_NAMESPACE}" + _CPP_LIBRARY_INSTALL_ROOT "${CPP_LIBRARY_ROOT}" + _CPP_LIBRARY_INSTALL_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}") + + # Freeze the target name now: deferred arguments are evaluated after function locals expire. + cmake_language(EVAL CODE + "cmake_language(DEFER CALL _cpp_library_finish_install [[${ARG_NAME}]])") endfunction() +# Finalizes one target after its directory has finished declaring link dependencies. +# - Postcondition: configuration generated before validation and export installation are registered +function(_cpp_library_finish_install TARGET_NAME) + _cpp_library_deferred_generate_config("${TARGET_NAME}") + _cpp_library_setup_install_validation("${TARGET_NAME}") +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) +function(_cpp_library_setup_install_validation TARGET_NAME) + set(NAME "${TARGET_NAME}") + get_target_property(PACKAGE_NAME ${TARGET_NAME} _CPP_LIBRARY_INSTALL_PACKAGE_NAME) + get_target_property(NAMESPACE ${TARGET_NAME} _CPP_LIBRARY_INSTALL_NAMESPACE) + get_target_property(BINARY_DIR ${TARGET_NAME} _CPP_LIBRARY_INSTALL_BINARY_DIR) # Check if there are unverified dependencies - get_property(HAS_UNVERIFIED GLOBAL PROPERTY _CPP_LIBRARY_HAS_UNVERIFIED_DEPS) + get_target_property(UNVERIFIED_DEPS ${TARGET_NAME} _CPP_LIBRARY_INSTALL_UNVERIFIED_DEPS) - if(HAS_UNVERIFIED) + if(UNVERIFIED_DEPS) set(UNVERIFIED_FILE "${BINARY_DIR}/${PACKAGE_NAME}_unverified_deps.cmake") # Add install-time validation to ensure all dependencies are properly tracked @@ -527,7 +542,7 @@ function(_cpp_library_setup_install_validation) 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}\") + string(REGEX REPLACE \"\\\\|[a-zA-Z0-9_:. -]+\" \"\" FORMATTED_DEPS \"\${FORMATTED_DEPS}\") message(FATAL_ERROR \"cpp-library: Cannot install ${PACKAGE_NAME} - untracked dependencies detected:\\n\" diff --git a/cmake/cpp-library-setup.cmake b/cmake/cpp-library-setup.cmake index 5a20df5..7112e5e 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -55,7 +55,8 @@ endfunction() # Creates library target (INTERFACE or compiled) with headers and proper configuration. # - Precondition: NAME, NAMESPACE, PACKAGE_NAME, CLEAN_NAME, and REQUIRES_CPP_VERSION specified -# - Postcondition: library target created with alias NAMESPACE::CLEAN_NAME; install rules when ${NAMESPACE}_INSTALL is ON +# - Postcondition: library target created with alias NAMESPACE::CLEAN_NAME; install rules when +# INSTALL_OPTION (or uppercase ${NAMESPACE}_INSTALL) is ON function(_cpp_library_setup_core) set(oneValueArgs NAME @@ -65,6 +66,8 @@ function(_cpp_library_setup_core) PACKAGE_NAME CLEAN_NAME REQUIRES_CPP_VERSION + LIBRARY_TYPE + INSTALL_OPTION ) set(multiValueArgs HEADERS @@ -72,6 +75,13 @@ function(_cpp_library_setup_core) ) cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if("INSTALL_OPTION" IN_LIST ARG_KEYWORDS_MISSING_VALUES) + message(FATAL_ERROR "_cpp_library_setup_core: INSTALL_OPTION requires a value") + endif() + set(install_args) + if(DEFINED ARG_INSTALL_OPTION) + list(APPEND install_args INSTALL_OPTION "${ARG_INSTALL_OPTION}") + endif() # Get version from git tags if not provided if(NOT ARG_VERSION) @@ -79,9 +89,21 @@ function(_cpp_library_setup_core) set(ARG_VERSION "${GIT_VERSION}") endif() + if(DEFINED ARG_LIBRARY_TYPE AND NOT ARG_LIBRARY_TYPE STREQUAL "" + AND NOT ARG_LIBRARY_TYPE MATCHES "^(STATIC|SHARED)$") + message(FATAL_ERROR "cpp_library_setup: LIBRARY_TYPE must be STATIC or SHARED") + endif() + if(ARG_LIBRARY_TYPE AND NOT ARG_SOURCES) + message(FATAL_ERROR "cpp_library_setup: LIBRARY_TYPE requires SOURCES") + endif() + if(ARG_SOURCES) - # Create a library with sources (respects BUILD_SHARED_LIBS variable) - add_library(${ARG_NAME} ${ARG_SOURCES}) + # Use the explicit type, or respect BUILD_SHARED_LIBS when omitted. + if(ARG_LIBRARY_TYPE) + add_library(${ARG_NAME} ${ARG_LIBRARY_TYPE} ${ARG_SOURCES}) + else() + add_library(${ARG_NAME} ${ARG_SOURCES}) + endif() add_library(${ARG_NAMESPACE}::${ARG_CLEAN_NAME} ALIAS ${ARG_NAME}) target_include_directories(${ARG_NAME} PUBLIC $ @@ -115,7 +137,7 @@ function(_cpp_library_setup_core) endif() endif() - # Setup installation (controlled by ${NAMESPACE}_INSTALL option, defaults to PROJECT_IS_TOP_LEVEL) + # Setup installation (defaults to PROJECT_IS_TOP_LEVEL) # The option is defined and checked inside _cpp_library_setup_install() _cpp_library_setup_install( NAME "${ARG_NAME}" @@ -123,6 +145,7 @@ function(_cpp_library_setup_core) VERSION "${ARG_VERSION}" NAMESPACE "${ARG_NAMESPACE}" HEADERS "${ARG_HEADERS}" + ${install_args} ) endfunction() diff --git a/cpp-library.cmake b/cpp-library.cmake index 6f4cbc0..a57e19a 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -156,13 +156,16 @@ endfunction() # - Precondition: PROJECT_NAME defined via project(), at least one HEADERS specified # - Postcondition: library target created, version set from git tags, optional tests/docs/examples configured # - When PROJECT_IS_TOP_LEVEL: also configures templates, testing, and docs -# - Installation is controlled by ${NAMESPACE}_INSTALL (defaults to PROJECT_IS_TOP_LEVEL) +# - Installation is controlled by INSTALL_OPTION or uppercase ${NAMESPACE}_INSTALL +# (defaults to PROJECT_IS_TOP_LEVEL) function(cpp_library_setup) # Parse arguments set(oneValueArgs DESCRIPTION # Description string NAMESPACE # Namespace (e.g., "stlab") REQUIRES_CPP_VERSION # C++ version (default: 17) + LIBRARY_TYPE # Explicit compiled library type (STATIC or SHARED) + INSTALL_OPTION # Custom installation option name ) set(multiValueArgs HEADERS # List of header filenames (e.g., "your_header.hpp") @@ -174,6 +177,17 @@ function(cpp_library_setup) cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + if("LIBRARY_TYPE" IN_LIST ARG_KEYWORDS_MISSING_VALUES) + message(FATAL_ERROR "cpp_library_setup: LIBRARY_TYPE requires a value") + endif() + if("INSTALL_OPTION" IN_LIST ARG_KEYWORDS_MISSING_VALUES) + message(FATAL_ERROR "cpp_library_setup: INSTALL_OPTION requires a value") + endif() + set(install_args) + if(DEFINED ARG_INSTALL_OPTION) + list(APPEND install_args INSTALL_OPTION "${ARG_INSTALL_OPTION}") + endif() + # Validate required arguments if(NOT ARG_DESCRIPTION) message(FATAL_ERROR "cpp_library_setup: DESCRIPTION is required") @@ -271,6 +285,8 @@ function(cpp_library_setup) HEADERS "${GENERATED_HEADERS}" SOURCES "${GENERATED_SOURCES}" REQUIRES_CPP_VERSION "${ARG_REQUIRES_CPP_VERSION}" + LIBRARY_TYPE "${ARG_LIBRARY_TYPE}" + ${install_args} ) # Only setup development infrastructure when building as top-level project diff --git a/tests/install/fixtures/consumer/CMakeLists.txt b/tests/install/fixtures/consumer/CMakeLists.txt new file mode 100644 index 0000000..c0c72d9 --- /dev/null +++ b/tests/install/fixtures/consumer/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.24) +project(consumer LANGUAGES CXX) +find_package(stlab-parent 1.0.0 REQUIRED) +add_executable(consumer main.cpp) +target_link_libraries(consumer PRIVATE stlab::parent) diff --git a/tests/install/fixtures/consumer/main.cpp b/tests/install/fixtures/consumer/main.cpp new file mode 100644 index 0000000..6c8d566 --- /dev/null +++ b/tests/install/fixtures/consumer/main.cpp @@ -0,0 +1,3 @@ +#include + +int main() { return stlab::parent() == 42 ? 0 : 1; } diff --git a/tests/install/fixtures/nested/CMakeLists.txt b/tests/install/fixtures/nested/CMakeLists.txt new file mode 100644 index 0000000..ef901c0 --- /dev/null +++ b/tests/install/fixtures/nested/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.24) +include("${TOOLKIT_SOURCE}/cpp-library.cmake") +cpp_library_enable_dependency_tracking() +project(parent LANGUAGES CXX) +set(CPP_LIBRARY_VERSION 1.0.0) +set(CMAKE_INSTALL_LIBDIR lib) +if(LEGACY_OFF) + set(STLAB_INSTALL OFF CACHE BOOL "Legacy namespace installation") +endif() + +set(install_args INSTALL_OPTION STLAB_PARENT_INSTALL) +if(LEGACY_OPTIONS) + set(install_args) +elseif(MISSING_INSTALL_OPTION) + set(install_args INSTALL_OPTION) +endif() +cpp_library_setup( + DESCRIPTION "Parent package fixture" + NAMESPACE stlab + HEADERS parent.hpp + ${install_args}) +add_subdirectory(leaf) +# Resolve the installed target downstream, even when leaf is not exported in this build. +target_link_libraries(parent INTERFACE + $ + $>) +cpp_library_map_dependency("stlab::leaf" "stlab-leaf 1.0.0") +if(INVALID_PACKAGE STREQUAL "parent") + add_library(UnknownParent::dep INTERFACE IMPORTED) + target_link_libraries(parent INTERFACE $) +endif() diff --git a/tests/install/fixtures/nested/include/stlab/parent.hpp b/tests/install/fixtures/nested/include/stlab/parent.hpp new file mode 100644 index 0000000..f1a108f --- /dev/null +++ b/tests/install/fixtures/nested/include/stlab/parent.hpp @@ -0,0 +1,6 @@ +#pragma once +#include + +namespace stlab { +inline int parent() { return 20 + leaf(); } +} diff --git a/tests/install/fixtures/nested/leaf/CMakeLists.txt b/tests/install/fixtures/nested/leaf/CMakeLists.txt new file mode 100644 index 0000000..8a6ff1d --- /dev/null +++ b/tests/install/fixtures/nested/leaf/CMakeLists.txt @@ -0,0 +1,18 @@ +include("${TOOLKIT_SOURCE}/cpp-library.cmake") +project(leaf LANGUAGES CXX) +set(CPP_LIBRARY_VERSION 1.0.0) +set(install_args INSTALL_OPTION STLAB_LEAF_INSTALL) +if(LEGACY_OPTIONS) + set(install_args) +endif() +cpp_library_setup( + DESCRIPTION "Leaf package fixture" + NAMESPACE stlab + HEADERS leaf.hpp + SOURCES leaf.cpp + LIBRARY_TYPE STATIC + ${install_args}) +if(INVALID_PACKAGE STREQUAL "leaf") + add_library(UnknownLeaf::dep INTERFACE IMPORTED) + target_link_libraries(leaf INTERFACE $) +endif() diff --git a/tests/install/fixtures/nested/leaf/include/stlab/leaf.hpp b/tests/install/fixtures/nested/leaf/include/stlab/leaf.hpp new file mode 100644 index 0000000..9f2d540 --- /dev/null +++ b/tests/install/fixtures/nested/leaf/include/stlab/leaf.hpp @@ -0,0 +1,4 @@ +#pragma once +namespace stlab { +int leaf(); +} diff --git a/tests/install/fixtures/nested/leaf/src/leaf.cpp b/tests/install/fixtures/nested/leaf/src/leaf.cpp new file mode 100644 index 0000000..2230093 --- /dev/null +++ b/tests/install/fixtures/nested/leaf/src/leaf.cpp @@ -0,0 +1,5 @@ +#include + +namespace stlab { +int leaf() { return 22; } +} diff --git a/tests/install/test_nested_install.cmake b/tests/install/test_nested_install.cmake new file mode 100644 index 0000000..4d7c21a --- /dev/null +++ b/tests/install/test_nested_install.cmake @@ -0,0 +1,165 @@ +# SPDX-License-Identifier: BSL-1.0 +# +# Run as: cmake -P tests/install/test_nested_install.cmake +# Requires a C++ compiler and Ninja (on Windows, run in a VS developer environment). +cmake_minimum_required(VERSION 3.24) + +get_filename_component(toolkit_source "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +if(NOT DEFINED test_root) + string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef test_id) + set(test_root "${toolkit_source}/build/nested-install-${test_id}") +endif() +if(WIN32) + set(CMAKE_EXECUTABLE_SUFFIX ".exe") +endif() +set(failed_cases) + +# Executes real build tools; any unexpected failure retains the exact command output. +function(run_command) + execute_process(COMMAND ${ARGN} + RESULT_VARIABLE result OUTPUT_VARIABLE out ERROR_VARIABLE err) + if(NOT result EQUAL 0) + message(FATAL_ERROR "Command failed (${result}): ${ARGN}\n${out}\n${err}") + endif() + if(err) + message(STATUS "Command diagnostics: ${ARGN}\n${err}") + endif() +endfunction() + +# Checks generated/installed package artifacts, including the dependency direction. +function(check_package prefix package present dependencies) + set(config_dir "${prefix}/lib/cmake/stlab-${package}") + foreach(suffix Config.cmake ConfigVersion.cmake Targets.cmake) + set(path "${config_dir}/stlab-${package}${suffix}") + if(present AND NOT EXISTS "${path}") + message(FATAL_ERROR "Missing selected package artifact: ${path}") + elseif(NOT present AND EXISTS "${path}") + message(FATAL_ERROR "Unselected package artifact installed: ${path}") + endif() + endforeach() + if(present) + file(READ "${config_dir}/stlab-${package}Config.cmake" config) + string(REGEX MATCHALL "find_dependency\\([^\n]*\\)" actual "${config}") + if(NOT "${actual}" STREQUAL "${dependencies}") + message(FATAL_ERROR "stlab-${package} dependencies: expected '${dependencies}', got '${actual}'") + endif() + endif() +endfunction() + +# Each case has private source copies: toolkit template initialization must never mutate fixtures. +function(run_case name parent_installed leaf_installed) + set(case_root "${test_root}/${name}") + set(source "${case_root}/source") + set(binary "${case_root}/build") + set(prefix "${case_root}/prefix") + file(MAKE_DIRECTORY "${source}") + file(COPY "${CMAKE_CURRENT_LIST_DIR}/fixtures/nested/" DESTINATION "${source}") + run_command("${CMAKE_COMMAND}" -S "${source}" -B "${binary}" -G Ninja + "-DTOOLKIT_SOURCE=${toolkit_source}" ${ARGN}) + run_command("${CMAKE_COMMAND}" --build "${binary}") + run_command("${CMAKE_COMMAND}" --install "${binary}" --prefix "${prefix}") + check_package("${prefix}" parent "${parent_installed}" "find_dependency(stlab-leaf 1.0.0)") + check_package("${prefix}" leaf "${leaf_installed}" "") + if(parent_installed AND leaf_installed) + set(consumer "${case_root}/consumer") + file(MAKE_DIRECTORY "${consumer}") + file(COPY "${CMAKE_CURRENT_LIST_DIR}/fixtures/consumer/" DESTINATION "${consumer}") + run_command("${CMAKE_COMMAND}" -S "${consumer}" -B "${consumer}/build" -G Ninja + "-DCMAKE_PREFIX_PATH=${prefix}") + run_command("${CMAKE_COMMAND}" --build "${consumer}/build") + run_command("${consumer}/build/consumer${CMAKE_EXECUTABLE_SUFFIX}") + endif() + message(STATUS "PASS ${name}: parent=${parent_installed}, leaf=${leaf_installed}") +endfunction() + +# Installs directory-local rules to prove a broken package does not poison its sibling. +function(run_invalid_case invalid clean target) + set(case_root "${test_root}/invalid-${invalid}") + set(source "${case_root}/source") + set(binary "${case_root}/build") + set(prefix "${case_root}/prefix") + file(MAKE_DIRECTORY "${source}") + file(COPY "${CMAKE_CURRENT_LIST_DIR}/fixtures/nested/" DESTINATION "${source}") + run_command("${CMAKE_COMMAND}" -S "${source}" -B "${binary}" -G Ninja + "-DTOOLKIT_SOURCE=${toolkit_source}" + -DSTLAB_PARENT_INSTALL=ON -DSTLAB_LEAF_INSTALL=ON "-DINVALID_PACKAGE=${invalid}") + run_command("${CMAKE_COMMAND}" --build "${binary}") + set(parent_binary "${binary}") + set(leaf_binary "${binary}/leaf") + run_command("${CMAKE_COMMAND}" "-DCMAKE_INSTALL_PREFIX=${prefix}" + -DCMAKE_INSTALL_LOCAL_ONLY=TRUE -P "${${clean}_binary}/cmake_install.cmake") + if(clean STREQUAL "parent") + check_package("${prefix}" parent TRUE "find_dependency(stlab-leaf 1.0.0)") + else() + check_package("${prefix}" leaf TRUE "") + endif() + execute_process(COMMAND "${CMAKE_COMMAND}" "-DCMAKE_INSTALL_PREFIX=${prefix}" + -DCMAKE_INSTALL_LOCAL_ONLY=TRUE -P "${${invalid}_binary}/cmake_install.cmake" + RESULT_VARIABLE result OUTPUT_VARIABLE out ERROR_VARIABLE err) + set(output "${out}\n${err}") + set(diagnostic "cpp-library: Cannot install stlab-${invalid} - untracked dependencies detected:") + string(FIND "${output}" "${diagnostic}" found) + string(FIND "${output}" " - ${target}" dependency_found) + string(FIND "${output}" "Cannot install stlab-${clean}" contaminated) + if(result EQUAL 0 OR found LESS 0 OR dependency_found LESS 0 OR NOT contaminated EQUAL -1) + message(FATAL_ERROR "Expected only ${invalid} diagnostic '${diagnostic}' and '${target}':\n${output}") + endif() + check_package("${prefix}" "${invalid}" FALSE "") + message(STATUS "PASS invalid-${invalid}: isolated '${diagnostic}' / '${target}'") +endfunction() + +# Separate child script invocations let all expected RED failures be inspected in one run. +if(DEFINED CASE) + if(CASE STREQUAL "both") + run_case(both TRUE TRUE -DSTLAB_PARENT_INSTALL=ON -DSTLAB_LEAF_INSTALL=ON -DLEGACY_OFF=ON) + elseif(CASE STREQUAL "parent-only") + run_case(parent-only TRUE FALSE -DSTLAB_PARENT_INSTALL=ON -DSTLAB_LEAF_INSTALL=OFF) + elseif(CASE STREQUAL "leaf-only") + run_case(leaf-only FALSE TRUE -DSTLAB_PARENT_INSTALL=OFF -DSTLAB_LEAF_INSTALL=ON) + elseif(CASE STREQUAL "custom-defaults") + run_case(custom-defaults TRUE FALSE) + elseif(CASE STREQUAL "legacy-on") + run_case(legacy-on TRUE TRUE -DLEGACY_OPTIONS=ON -DSTLAB_INSTALL=ON) + elseif(CASE STREQUAL "legacy-off") + run_case(legacy-off FALSE FALSE -DLEGACY_OPTIONS=ON -DSTLAB_INSTALL=OFF) + elseif(CASE STREQUAL "legacy-defaults") + run_case(legacy-defaults TRUE TRUE -DLEGACY_OPTIONS=ON) + elseif(CASE STREQUAL "invalid-parent") + run_invalid_case(parent leaf UnknownParent::dep) + elseif(CASE STREQUAL "invalid-leaf") + run_invalid_case(leaf parent UnknownLeaf::dep) + elseif(CASE STREQUAL "missing-option") + set(source "${test_root}/missing-option/source") + file(MAKE_DIRECTORY "${source}") + file(COPY "${CMAKE_CURRENT_LIST_DIR}/fixtures/nested/" DESTINATION "${source}") + execute_process(COMMAND "${CMAKE_COMMAND}" -S "${source}" -B "${test_root}/missing-option/build" + -G Ninja "-DTOOLKIT_SOURCE=${toolkit_source}" -DMISSING_INSTALL_OPTION=ON + RESULT_VARIABLE result OUTPUT_VARIABLE out ERROR_VARIABLE err) + set(diagnostic "cpp_library_setup: INSTALL_OPTION requires a value") + string(FIND "${out}\n${err}" "${diagnostic}" found) + if(result EQUAL 0 OR found LESS 0) + message(FATAL_ERROR "Expected diagnostic '${diagnostic}':\n${out}\n${err}") + endif() + message(STATUS "PASS missing-option: ${diagnostic}") + else() + message(FATAL_ERROR "Unknown test case: ${CASE}") + endif() + return() +endif() + +foreach(case both parent-only leaf-only custom-defaults legacy-on legacy-off legacy-defaults + invalid-parent invalid-leaf missing-option) + execute_process(COMMAND "${CMAKE_COMMAND}" "-DCASE=${case}" "-Dtest_root=${test_root}" + -P "${CMAKE_CURRENT_LIST_FILE}" + RESULT_VARIABLE result OUTPUT_VARIABLE out ERROR_VARIABLE err) + message(STATUS "${case}:\n${out}") + if(NOT result EQUAL 0) + message(STATUS "FAIL ${case}:\n${err}") + list(APPEND failed_cases "${case}") + endif() +endforeach() +file(REMOVE_RECURSE "${test_root}") +if(failed_cases) + message(FATAL_ERROR "Nested install cases failed: ${failed_cases}") +endif() +message(STATUS "All 10 nested install cases passed!") diff --git a/tests/setup/fixtures/target_type/CMakeLists.txt b/tests/setup/fixtures/target_type/CMakeLists.txt new file mode 100644 index 0000000..1cd7b78 --- /dev/null +++ b/tests/setup/fixtures/target_type/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.24) +include("${TOOLKIT_SOURCE}/cpp-library.cmake") +cpp_library_enable_dependency_tracking() +project(sample LANGUAGES CXX) +set(CPP_LIBRARY_VERSION 1.0.0) +set(FIXTURE_INSTALL OFF CACHE BOOL "" FORCE) +set(type_args) +if(MISSING_LIBRARY_TYPE) + list(APPEND type_args LIBRARY_TYPE) +elseif(DEFINED REQUESTED_TYPE) + list(APPEND type_args LIBRARY_TYPE "${REQUESTED_TYPE}") +endif() +set(source_args SOURCES sample.cpp) +if(OMIT_SOURCES) + set(source_args) +endif() +cpp_library_setup( + DESCRIPTION "Explicit compiled target fixture" + NAMESPACE fixture + HEADERS sample.hpp + ${source_args} + ${type_args}) +get_target_property(actual sample TYPE) +if(NOT actual STREQUAL EXPECTED_TYPE) + message(FATAL_ERROR "Expected ${EXPECTED_TYPE}, got ${actual}") +endif() +if(NOT BUILD_SHARED_LIBS STREQUAL EXPECTED_BUILD_SHARED_LIBS) + message(FATAL_ERROR "Setup changed parent BUILD_SHARED_LIBS") +endif() diff --git a/tests/setup/fixtures/target_type/include/fixture/sample.hpp b/tests/setup/fixtures/target_type/include/fixture/sample.hpp new file mode 100644 index 0000000..f96c2d4 --- /dev/null +++ b/tests/setup/fixtures/target_type/include/fixture/sample.hpp @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BSL-1.0 +#pragma once + +namespace fixture { +/// Returns 42. +int sample(); +} diff --git a/tests/setup/fixtures/target_type/src/sample.cpp b/tests/setup/fixtures/target_type/src/sample.cpp new file mode 100644 index 0000000..fa2b235 --- /dev/null +++ b/tests/setup/fixtures/target_type/src/sample.cpp @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BSL-1.0 +#include + +namespace fixture { +int sample() { return 42; } +} diff --git a/tests/setup/test_target_type.cmake b/tests/setup/test_target_type.cmake new file mode 100644 index 0000000..ecc5ae4 --- /dev/null +++ b/tests/setup/test_target_type.cmake @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: BSL-1.0 +# +# Run as: cmake -P tests/setup/test_target_type.cmake + +cmake_minimum_required(VERSION 3.24) + +get_filename_component(toolkit_source "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) +string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef test_id) +set(test_root "${toolkit_source}/build/target-type-${test_id}") +set(failed_cases) + +# Configures a private fixture copy and checks target type or the required diagnostic. +function(run_case name shared expected_type expected_diagnostic) + set(case_source "${test_root}/${name}/source") + set(case_binary "${test_root}/${name}/build") + file(MAKE_DIRECTORY "${case_source}") + file(COPY "${CMAKE_CURRENT_LIST_DIR}/fixtures/target_type/" + DESTINATION "${case_source}") + + execute_process( + COMMAND "${CMAKE_COMMAND}" -S "${case_source}" -B "${case_binary}" -G Ninja + "-DTOOLKIT_SOURCE=${toolkit_source}" + "-DBUILD_SHARED_LIBS=${shared}" + "-DEXPECTED_BUILD_SHARED_LIBS=${shared}" + "-DEXPECTED_TYPE=${expected_type}" + ${ARGN} + RESULT_VARIABLE result OUTPUT_VARIABLE out ERROR_VARIABLE err) + if(expected_diagnostic) + if(result EQUAL 0 OR NOT "${out}\n${err}" MATCHES "${expected_diagnostic}") + message(STATUS "FAIL ${name}: Expected diagnostic ${expected_diagnostic}:\n${out}\n${err}") + list(APPEND failed_cases "${name}") + else() + message(STATUS "PASS ${name}: ${expected_diagnostic}") + endif() + elseif(NOT result EQUAL 0) + message(STATUS "FAIL ${name}: Target type case failed:\n${out}\n${err}") + list(APPEND failed_cases "${name}") + else() + if(err) + message(STATUS "${name} configure diagnostics:\n${err}") + endif() + message(STATUS "PASS ${name}: ${expected_type}; BUILD_SHARED_LIBS=${shared}") + endif() + set(failed_cases "${failed_cases}" PARENT_SCOPE) +endfunction() + +run_case(default_static OFF STATIC_LIBRARY "") +run_case(default_shared ON SHARED_LIBRARY "") +run_case(explicit_static ON STATIC_LIBRARY "" -DREQUESTED_TYPE=STATIC) +run_case(explicit_shared OFF SHARED_LIBRARY "" -DREQUESTED_TYPE=SHARED) +run_case(header_only_static_default OFF INTERFACE_LIBRARY "" -DOMIT_SOURCES=ON) +run_case(header_only_shared_default ON INTERFACE_LIBRARY "" -DOMIT_SOURCES=ON) +run_case(invalid_type OFF "" "cpp_library_setup: LIBRARY_TYPE must be STATIC or SHARED" + -DREQUESTED_TYPE=MODULE) +run_case(invalid_false_type OFF "" "cpp_library_setup: LIBRARY_TYPE must be STATIC or SHARED" + -DREQUESTED_TYPE=OFF) +run_case(missing_library_type OFF STATIC_LIBRARY "cpp_library_setup: LIBRARY_TYPE requires a value" + -DMISSING_LIBRARY_TYPE=ON) +run_case(static_without_sources ON "" "cpp_library_setup: LIBRARY_TYPE requires SOURCES" + -DREQUESTED_TYPE=STATIC -DOMIT_SOURCES=ON) +run_case(shared_without_sources OFF "" "cpp_library_setup: LIBRARY_TYPE requires SOURCES" + -DREQUESTED_TYPE=SHARED -DOMIT_SOURCES=ON) + +file(REMOVE_RECURSE "${test_root}") +if(failed_cases) + message(FATAL_ERROR "Target type cases failed: ${failed_cases}") +endif() +message(STATUS "All target type cases passed!")