diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57403d2..c0eedee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,30 @@ 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 + windows-packages: + name: Windows static and DLL consumers + runs-on: windows-latest + + steps: + - uses: actions/checkout@v7 + # [DEPENDENCY] https://github.com/ilammy/msvc-dev-cmd/releases + - uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 + + - name: Run target type tests + run: cmake -P tests/setup/test_target_type.cmake + + - name: Run nested static and shared installation round trips + run: cmake -P tests/install/test_nested_install.cmake + integration-tests: name: Integration Tests runs-on: ubuntu-latest diff --git a/README.md b/README.md index 1687a00..c354a42 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 @@ -340,6 +362,7 @@ cpp_library_setup( [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 +371,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 +429,75 @@ 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`. The target respects + `BUILD_SHARED_LIBS` (static by default; shared when `ON`), leaving the choice + to the library consumer. + +### Windows DLLs and ABI boundaries + +Static linking is the default. When consumers require a Windows DLL, follow the +[stlab-execution](https://github.com/stlab/stlab-execution) pattern: put the +compiled runtime behind a small, explicitly versioned C ABI and list its exports +in a checked-in module-definition (`.def`) file. Keep the C++ convenience layer in +headers rather than exporting C++ implementation symbols. Execution's +`src/execution.def` is the model for this export list. + +For example, after `cpp_library_setup(...)` creates `my-library`: + +```cmake +get_target_property(library_type my-library TYPE) +if(WIN32 AND library_type STREQUAL "SHARED_LIBRARY") + target_sources(my-library PRIVATE src/my-library.def) + target_compile_definitions(my-library INTERFACE MY_LIBRARY_USING_DLL) +endif() +``` + +The `.def` file names only the intended ABI entry points: + +```text +EXPORTS + my_library_v1_initialize + my_library_v1_shutdown +``` + +Public declarations use `extern "C"` and `__declspec(dllimport)` when consuming the +DLL. In this example, the target's `MY_LIBRARY_USING_DLL` interface definition +selects imports for shared consumers, including installed consumers, but is absent +when compiling the library itself or consuming a static build: + +```cpp +#if defined(_WIN32) && defined(MY_LIBRARY_USING_DLL) +#define MY_LIBRARY_API __declspec(dllimport) +#else +#define MY_LIBRARY_API +#endif + +extern "C" { +MY_LIBRARY_API int my_library_v1_initialize(void); +MY_LIBRARY_API void my_library_v1_shutdown(void); +} +``` + +The `.def` file controls exports when building the DLL; `dllexport` is not needed +for these entry points. Do not enable `WINDOWS_EXPORT_ALL_SYMBOLS` or broadly mark +implementation classes `dllexport`. A `.def` file and C linkage do not by themselves +guarantee ABI stability: preserve calling conventions, argument layouts, ownership +and lifetime rules, and version incompatible entry points. Do not expose standard +library types or let C++ exceptions cross this boundary. Allocate and release +owned resources on the same side of the boundary. + +cpp-library installs DLLs in `CMAKE_INSTALL_BINDIR` and import/static libraries in +`CMAKE_INSTALL_LIBDIR`. Consumers should link the exported CMake target so that +include paths, import definitions, and transitive dependencies are preserved; +`BUILD_SHARED_LIBS` does not change the type of an already installed library. + +For the tests and examples created through `TESTS` and `EXAMPLES`, cpp-library +automatically copies runtime DLL dependencies beside each executable after linking +on Windows, including DLLs from installed packages. Dependencies must provide +proper CMake shared-library targets with their DLL locations. Static builds with +no runtime DLL dependencies need no copies. Deploying downstream applications and +their DLLs remains the application's responsibility; this convenience does not +add application installation rules or deploy the compiler runtime. ## Reference @@ -503,7 +595,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 +606,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 +654,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-copy-runtime-dlls.cmake b/cmake/cpp-library-copy-runtime-dlls.cmake new file mode 100644 index 0000000..a1079b5 --- /dev/null +++ b/cmake/cpp-library-copy-runtime-dlls.cmake @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: BSL-1.0 + +cmake_minimum_required(VERSION 3.24) + +if(NOT DEFINED CPP_LIBRARY_RUNTIME_DLLS + OR NOT IS_DIRECTORY "${CPP_LIBRARY_RUNTIME_DESTINATION}") + message(FATAL_ERROR "cpp-library: Runtime DLL deployment requires a DLL list and an existing destination directory") +endif() + +file(REAL_PATH "${CPP_LIBRARY_RUNTIME_DESTINATION}" destination) +foreach(dll IN LISTS CPP_LIBRARY_RUNTIME_DLLS) + if(NOT EXISTS "${dll}") + message(FATAL_ERROR "cpp-library: Runtime DLL does not exist: ${dll}") + endif() + file(REAL_PATH "${dll}" source) + get_filename_component(filename "${source}" NAME) + # A source-built DLL may already share the executable's output directory. + if(NOT source STREQUAL "${destination}/${filename}") + file(COPY_FILE "${source}" "${destination}/${filename}" ONLY_IF_DIFFERENT) + endif() +endforeach() 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..e2583a7 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,7 @@ function(_cpp_library_setup_core) PACKAGE_NAME CLEAN_NAME REQUIRES_CPP_VERSION + INSTALL_OPTION ) set(multiValueArgs HEADERS @@ -72,6 +74,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) @@ -80,7 +89,7 @@ function(_cpp_library_setup_core) endif() if(ARG_SOURCES) - # Create a library with sources (respects BUILD_SHARED_LIBS variable) + # Respect the consumer's BUILD_SHARED_LIBS setting. add_library(${ARG_NAME} ${ARG_SOURCES}) add_library(${ARG_NAMESPACE}::${ARG_CLEAN_NAME} ALIAS ${ARG_NAME}) target_include_directories(${ARG_NAME} PUBLIC @@ -115,7 +124,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 +132,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..ef397ec 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -134,6 +134,16 @@ function(_cpp_library_setup_executables) add_executable(${executable_base} "${source_dir}/${executable}") target_link_libraries(${executable_base} PRIVATE ${ARG_NAMESPACE}::${CLEAN_NAME} doctest::doctest) + if(WIN32) + # A script handles empty DLL lists without requiring newer copy commands. + add_custom_command(TARGET ${executable_base} POST_BUILD + COMMAND "${CMAKE_COMMAND}" + "-DCPP_LIBRARY_RUNTIME_DLLS=$" + "-DCPP_LIBRARY_RUNTIME_DESTINATION=$" + -P "${CPP_LIBRARY_ROOT}/cmake/cpp-library-copy-runtime-dlls.cmake" + VERBATIM) + endif() + # Register as CTest test add_test(NAME ${executable_base} COMMAND ${executable_base}) @@ -156,13 +166,15 @@ 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) + INSTALL_OPTION # Custom installation option name ) set(multiValueArgs HEADERS # List of header filenames (e.g., "your_header.hpp") @@ -174,6 +186,14 @@ function(cpp_library_setup) cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + 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 +291,7 @@ function(cpp_library_setup) HEADERS "${GENERATED_HEADERS}" SOURCES "${GENERATED_SOURCES}" REQUIRES_CPP_VERSION "${ARG_REQUIRES_CPP_VERSION}" + ${install_args} ) # Only setup development infrastructure when building as top-level project diff --git a/tests/install/README.md b/tests/install/README.md index da2d47e..9e85c69 100644 --- a/tests/install/README.md +++ b/tests/install/README.md @@ -30,6 +30,26 @@ The test suite covers: 8. **Generator Expressions**: BUILD_INTERFACE dependencies skipped 9. **Edge Cases**: Empty libraries, different versions, override behavior +## Installation Round Trips + +The separate integration script requires Ninja and a C++ compiler: + +```bash +cmake -P tests/install/test_nested_install.cmake +``` + +On Windows, run it from an x64 Visual Studio developer environment. It builds and +installs nested parent/leaf packages, then builds and runs separate installed +consumers. The leaf is tested as both static and shared, with explicit versioned +C ABI exports from a `.def` file for the Windows DLL. + +The consumers exercise cpp-library's test and example executable setup in +separate output directories containing spaces. Windows checks verify transitive +installed DLL deployment, an empty DLL list for static builds, and the case where +a DLL is already in the destination directory. The remaining cases cover +independent install options, legacy namespace options, and isolated dependency +validation failures. These round trips run on both Ubuntu and Windows in CI. + ## Test Output Successful run: @@ -80,4 +100,3 @@ verify_output("${RESULT}" "find_dependency(package-name 1.0.0)" "Test N") ## CI Integration These tests run automatically on every push/PR via GitHub Actions. See `.github/workflows/ci.yml` for the workflow configuration. - diff --git a/tests/install/fixtures/consumer/CMakeLists.txt b/tests/install/fixtures/consumer/CMakeLists.txt new file mode 100644 index 0000000..1962076 --- /dev/null +++ b/tests/install/fixtures/consumer/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.24) +project(consumer LANGUAGES CXX) +include(CTest) +include("${TOOLKIT_SOURCE}/cpp-library.cmake") +find_package(stlab-parent 1.0.0 REQUIRED) +get_target_property(imported stlab::leaf IMPORTED) +get_target_property(leaf_type stlab::leaf TYPE) +if(NOT imported OR NOT leaf_type STREQUAL EXPECTED_LEAF_TYPE) + message(FATAL_ERROR "Expected installed ${EXPECTED_LEAF_TYPE}, got ${leaf_type}") +endif() + +# These standalone main functions need no doctest headers or runtime. +add_library(doctest::doctest INTERFACE IMPORTED) +_cpp_library_setup_executables( + NAME parent NAMESPACE stlab TYPE tests EXECUTABLES consumer.cpp) +_cpp_library_setup_executables( + NAME parent NAMESPACE stlab TYPE examples EXECUTABLES example.cpp) +set_target_properties(consumer PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/client apps/tests") +set_target_properties(example PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/client apps/examples") + +if(WIN32) + file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/runtime-dlls.cmake" + CONTENT "set(runtime_dlls \"$\")\n") +endif() diff --git a/tests/install/fixtures/consumer/examples/example.cpp b/tests/install/fixtures/consumer/examples/example.cpp new file mode 100644 index 0000000..6c8d566 --- /dev/null +++ b/tests/install/fixtures/consumer/examples/example.cpp @@ -0,0 +1,3 @@ +#include + +int main() { return stlab::parent() == 42 ? 0 : 1; } diff --git a/tests/install/fixtures/consumer/tests/consumer.cpp b/tests/install/fixtures/consumer/tests/consumer.cpp new file mode 100644 index 0000000..6c8d566 --- /dev/null +++ b/tests/install/fixtures/consumer/tests/consumer.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..c37dd13 --- /dev/null +++ b/tests/install/fixtures/nested/leaf/CMakeLists.txt @@ -0,0 +1,34 @@ +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 + ${install_args}) +get_target_property(leaf_type leaf TYPE) +set(LEAF_SHARED 0) +if(leaf_type STREQUAL "SHARED_LIBRARY") + set(LEAF_SHARED 1) + if(WIN32) + target_sources(leaf PRIVATE src/leaf.def) + endif() +endif() +configure_file(include/stlab/leaf-config.hpp.in + "${CMAKE_CURRENT_BINARY_DIR}/include/stlab/leaf-config.hpp" @ONLY) +target_sources(leaf PUBLIC + FILE_SET headers TYPE HEADERS + BASE_DIRS "${CMAKE_CURRENT_BINARY_DIR}/include" + FILES "${CMAKE_CURRENT_BINARY_DIR}/include/stlab/leaf-config.hpp") +target_include_directories(leaf PUBLIC + "$") +target_compile_definitions(leaf PRIVATE LEAF_BUILD) +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-config.hpp.in b/tests/install/fixtures/nested/leaf/include/stlab/leaf-config.hpp.in new file mode 100644 index 0000000..6fa56de --- /dev/null +++ b/tests/install/fixtures/nested/leaf/include/stlab/leaf-config.hpp.in @@ -0,0 +1,2 @@ +#pragma once +#define LEAF_SHARED @LEAF_SHARED@ 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..77d439e --- /dev/null +++ b/tests/install/fixtures/nested/leaf/include/stlab/leaf.hpp @@ -0,0 +1,14 @@ +#pragma once +#include + +#if defined(_WIN32) && LEAF_SHARED && !defined(LEAF_BUILD) +#define LEAF_API __declspec(dllimport) +#else +#define LEAF_API +#endif + +extern "C" LEAF_API int stlab_fixture_leaf_v1(); + +namespace stlab { +inline int leaf() { return stlab_fixture_leaf_v1(); } +} 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..1854b66 --- /dev/null +++ b/tests/install/fixtures/nested/leaf/src/leaf.cpp @@ -0,0 +1,3 @@ +#include + +extern "C" int stlab_fixture_leaf_v1() { return 22; } diff --git a/tests/install/fixtures/nested/leaf/src/leaf.def b/tests/install/fixtures/nested/leaf/src/leaf.def new file mode 100644 index 0000000..49030eb --- /dev/null +++ b/tests/install/fixtures/nested/leaf/src/leaf.def @@ -0,0 +1,2 @@ +EXPORTS + stlab_fixture_leaf_v1 diff --git a/tests/install/test_nested_install.cmake b/tests/install/test_nested_install.cmake new file mode 100644 index 0000000..a85eba3 --- /dev/null +++ b/tests/install/test_nested_install.cmake @@ -0,0 +1,193 @@ +# 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() +set(failed_cases) +find_program(ctest NAMES ctest REQUIRED) + +# 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 shared) + 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}" "-DBUILD_SHARED_LIBS=${shared}" ${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}") + if(shared) + set(expected_leaf_type SHARED_LIBRARY) + else() + set(expected_leaf_type STATIC_LIBRARY) + endif() + run_command("${CMAKE_COMMAND}" -S "${consumer}" -B "${consumer}/build" -G Ninja + "-DCMAKE_PREFIX_PATH=${prefix}" "-DTOOLKIT_SOURCE=${toolkit_source}" + "-DEXPECTED_LEAF_TYPE=${expected_leaf_type}") + run_command("${CMAKE_COMMAND}" --build "${consumer}/build") + if(WIN32) + include("${consumer}/build/runtime-dlls.cmake") + if(shared AND NOT runtime_dlls) + message(FATAL_ERROR "The installed shared leaf must provide a runtime DLL") + elseif(NOT shared AND runtime_dlls) + message(FATAL_ERROR "A static leaf must not require a runtime DLL") + endif() + foreach(dll IN LISTS runtime_dlls) + cmake_path(IS_PREFIX prefix "${dll}" NORMALIZE installed) + get_filename_component(filename "${dll}" NAME) + foreach(directory tests examples) + if(NOT installed OR NOT EXISTS "${consumer}/build/client apps/${directory}/${filename}") + message(FATAL_ERROR "Installed DLL was not deployed beside ${directory}: ${dll}") + endif() + endforeach() + endforeach() + if(shared) + run_command("${CMAKE_COMMAND}" "-DCPP_LIBRARY_RUNTIME_DLLS=${runtime_dlls}" + "-DCPP_LIBRARY_RUNTIME_DESTINATION=${prefix}/bin" + -P "${toolkit_source}/cmake/cpp-library-copy-runtime-dlls.cmake") + endif() + endif() + run_command("${ctest}" --test-dir "${consumer}/build" --output-on-failure) + 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 OFF -DSTLAB_PARENT_INSTALL=ON -DSTLAB_LEAF_INSTALL=ON -DLEGACY_OFF=ON) + elseif(CASE STREQUAL "both-shared") + run_case(both-shared TRUE TRUE ON -DSTLAB_PARENT_INSTALL=ON -DSTLAB_LEAF_INSTALL=ON) + elseif(CASE STREQUAL "parent-only") + run_case(parent-only TRUE FALSE OFF -DSTLAB_PARENT_INSTALL=ON -DSTLAB_LEAF_INSTALL=OFF) + elseif(CASE STREQUAL "leaf-only") + run_case(leaf-only FALSE TRUE OFF -DSTLAB_PARENT_INSTALL=OFF -DSTLAB_LEAF_INSTALL=ON) + elseif(CASE STREQUAL "custom-defaults") + run_case(custom-defaults TRUE FALSE OFF) + elseif(CASE STREQUAL "legacy-on") + run_case(legacy-on TRUE TRUE OFF -DLEGACY_OPTIONS=ON -DSTLAB_INSTALL=ON) + elseif(CASE STREQUAL "legacy-off") + run_case(legacy-off FALSE FALSE OFF -DLEGACY_OPTIONS=ON -DSTLAB_INSTALL=OFF) + elseif(CASE STREQUAL "legacy-defaults") + run_case(legacy-defaults TRUE TRUE OFF -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 both-shared 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 11 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..e80ed66 --- /dev/null +++ b/tests/setup/fixtures/target_type/CMakeLists.txt @@ -0,0 +1,22 @@ +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(source_args SOURCES sample.cpp) +if(OMIT_SOURCES) + set(source_args) +endif() +cpp_library_setup( + DESCRIPTION "Consumer-selected target type fixture" + NAMESPACE fixture + HEADERS sample.hpp + ${source_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..65bc750 --- /dev/null +++ b/tests/setup/test_target_type.cmake @@ -0,0 +1,49 @@ +# 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 the consumer-selected target type. +function(run_case name shared expected_type) + 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(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(header_only_static_default OFF INTERFACE_LIBRARY -DOMIT_SOURCES=ON) +run_case(header_only_shared_default ON INTERFACE_LIBRARY -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!")