From 3ab13f2be3b835fd1e6feaab7845088c31642eba Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 14:54:03 -0800 Subject: [PATCH 01/23] Adding installation support (again) Added cleaned up installation support, along with an overhauled readme. --- .vscode/settings.json | 9 + README.md | 325 ++++++++++++++--------------- cmake/cpp-library-install.cmake | 93 +++++++++ cmake/cpp-library-setup.cmake | 16 +- cpp-library.cmake | 1 + templates/.github/workflows/ci.yml | 78 +++++++ templates/.vscode/extensions.json | 1 - templates/CMakePresets.json | 17 +- 8 files changed, 368 insertions(+), 172 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 cmake/cpp-library-install.cmake diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e6164d4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "cSpell.words": [ + "clangd", + "ctest", + "doctest", + "MSVC", + "mylib" + ] +} diff --git a/README.md b/README.md index a0835b5..3ffa4a6 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,17 @@ Modern CMake template for C++ libraries with comprehensive infrastructure. `cpp-library` provides a standardized CMake infrastructure template for C++ libraries. It eliminates boilerplate and provides consistent patterns for: - **Project Declaration**: Uses existing `project()` declaration with automatic git tag-based versioning -- **Testing**: Integrated doctest with CTest and compile-fail test support -- **Documentation**: Doxygen with doxygen-awesome-css theme -- **Development Tools**: clangd integration, CMakePresets.json, clang-tidy support -- **CI/CD**: GitHub Actions workflows with multi-platform testing -- **Dependency Management**: CPM.cmake integration +- **Library Setup**: INTERFACE targets for header-only libraries, static/shared libraries for compiled libraries +- **Installation**: CMake package config generation with proper header and library installation +- **Testing**: Integrated [doctest](https://github.com/doctest/doctest) with CTest and compile-fail test support +- **Documentation**: [Doxygen](https://www.doxygen.nl/) with [doxygen-awesome-css](https://github.com/jothepro/doxygen-awesome-css) theme +- **Development Tools**: [clangd](https://clangd.llvm.org/) integration, CMakePresets.json, [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) support +- **CI/CD**: [GitHub Actions](https://docs.github.com/en/actions) workflows with multi-platform testing and installation verification +- **Dependency Management**: [CPM.cmake](https://github.com/cpm-cmake/CPM.cmake) integration ## Usage -Use CPMAddPackage to fetch cpp-library directly in your CMakeLists.txt: +Use `CPMAddPackage` to fetch cpp-library directly in your `CMakeLists.txt`: ```cmake cmake_minimum_required(VERSION 3.20) @@ -28,7 +30,7 @@ cmake_minimum_required(VERSION 3.20) # Project declaration - cpp_library_setup will use this name and detect version from git tags project(your-library) -# Setup cpp-library infrastructure +# Only set CPM cache when building as top-level project if(PROJECT_IS_TOP_LEVEL) set(CPM_SOURCE_CACHE ${CMAKE_SOURCE_DIR}/.cache/cpm CACHE PATH "CPM cache") endif() @@ -50,11 +52,122 @@ cpp_library_setup( ) ``` -### Prerequisites +### Getting Started -- **CPM.cmake**: Must be included before using cpp-library -- **CMake 3.20+**: Required for modern CMake features -- **C++17+**: Default requirement (configurable) +Before using cpp-library, you'll need: + +- **CMake 3.20+** - [Download here](https://cmake.org/download/) +- **A C++17+ compiler** - GCC 7+, Clang 5+, MSVC 2017+, or Apple Clang 9+ + +#### Step 1: Install CPM.cmake + +[CPM.cmake](https://github.com/cpm-cmake/CPM.cmake) is required for dependency management. [Add it to your project](https://github.com/cpm-cmake/CPM.cmake?tab=readme-ov-file#adding-cpm): + +```bash +mkdir -p cmake +wget -O cmake/CPM.cmake https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake +``` + +Create the standard directory structure: + +```bash +mkdir -p include/your_namespace examples tests +``` + +#### Step 2: Create your CMakeLists.txt + +Create a `CMakeLists.txt` file following the example shown at the [beginning of the Usage section](#usage). + +#### Step 3: Build and test + +```bash +cmake --preset=test +cmake --build --preset=test +ctest --preset=test +``` + +### Consuming Libraries Built with cpp-library + +#### Using CPMAddPackage (recommended) + +The preferred way to consume a library built with cpp-library is via [CPM.cmake](https://github.com/cpm-cmake/CPM.cmake): + +```cmake +cmake_minimum_required(VERSION 3.20) +project(my-app) + +include(cmake/CPM.cmake) + +# Fetch the library directly from GitHub +CPMAddPackage("gh:your-org/your-library@1.0.0") + +add_executable(my-app main.cpp) +target_link_libraries(my-app PRIVATE your_namespace::your-library) +``` + +The library will be automatically fetched and built as part of your project. + +#### Installation (optional) + +Installation is optional and typically not required when using CPM. If you need to install your library (e.g., for system-wide deployment or use with a package manager) use: + +```bash +# Build and install to default system location +cmake --preset=default +cmake --build --preset=default +cmake --install build/default + +# Install to custom prefix +cmake --install build/default --prefix /opt/mylib +``` + +For information about using installed packages with `find_package()`, see the [CPM.cmake documentation](https://github.com/cpm-cmake/CPM.cmake) about [controlling how dependencies are found](https://github.com/cpm-cmake/CPM.cmake#cpm_use_local_packages). + +### Updating cpp-library + +To update to the latest version of cpp-library in your project: + +#### Step 1: Update the version in CMakeLists.txt + +Change the version tag in your `CPMAddPackage` call: + +```cmake +CPMAddPackage("gh:stlab/cpp-library@4.1.0") # Update version here +``` + +#### Step 2: Regenerate template files + +Use the `init` preset to regenerate `CMakePresets.json` and CI workflows with the latest templates: + +```bash +cmake --preset=init +cmake --build --preset=init +``` + +This ensures your project uses the latest presets and CI configurations from the updated cpp-library version. + +### Setting Up GitHub Repository + +#### Version Tagging + +cpp-library automatically detects your library version from git tags. To version your library: + +```bash +git tag v1.0.0 +git push origin v1.0.0 +``` + +Tags should follow [semantic versioning](https://semver.org/) (e.g., `v1.0.0`, `v2.1.3`). + +#### GitHub Pages Deployment + +To enable automatic documentation deployment to GitHub Pages: + +1. Go to your repository **Settings** → **Pages** +2. Under **Source**, select **GitHub Actions** +3. Push a commit to trigger the CI workflow + +Your documentation will be automatically built and deployed to `https://your-org.github.io/your-library/` on every push to the main branch. ## API Reference @@ -78,29 +191,11 @@ cpp_library_setup( ) ``` -**Note**: The project name is automatically taken from `PROJECT_NAME` (set by the `project()` -command). You must call `project(your-library)` before `cpp_library_setup()`. Version is -automatically detected from git tags. - -**NOTE**: Examples using doctest should have `test` in the name if you want them to be visible in -the TestMate test explorer. - -### Template Regeneration - -To force regeneration of template files (CMakePresets.json, CI workflows, etc.), you can use the `init` preset: - -```bash -cmake --preset=init -cmake --build --preset=init -``` - -Alternatively, you can set the CMake variable `CPP_LIBRARY_FORCE_INIT` to `ON`: - -```bash -cmake -DCPP_LIBRARY_FORCE_INIT=ON -B build/init -``` +**Notes:** -This will regenerate all template files, overwriting any existing ones. +- The project name is automatically taken from `PROJECT_NAME` (set by the `project()` command). You must call `project(your-library)` before `cpp_library_setup()`. +- Version is automatically detected from git tags (see [Version Tagging](#version-tagging)). +- Examples using doctest should include `test` in the filename to be visible in the [C++ TestMate](https://marketplace.visualstudio.com/items?itemName=matepek.vscode-catch2-test-adapter) extension for VS Code test explorer. ### Path Conventions @@ -115,8 +210,6 @@ The template uses consistent path conventions for all file specifications: - **TESTS**: Source files with `.cpp` extension, located in `tests/` directory - Examples: `tests.cpp`, `unit_tests.cpp` -The template automatically generates the full paths based on these conventions. HEADERS are placed in `include//` and SOURCES are placed in `src/`. - ### Library Types **Header-only libraries**: Specify only `HEADERS`, omit `SOURCES` @@ -141,159 +234,55 @@ cpp_library_setup( ) ``` -## Features - -### Non-Header-Only Library Support - -- **Non-header-only library support**: For libraries with source files, specify them explicitly with the `SOURCES` argument as filenames (e.g., `"your_library.cpp"`). - Both header-only and compiled libraries are supported seamlessly. - -### Automated Infrastructure +Libraries with sources build as static libraries by default. Set `BUILD_SHARED_LIBS=ON` to build shared libraries instead. -- **CMakePresets.json**: Generates standard presets (default, test, docs, clang-tidy, init) -- **Testing**: doctest integration with CTest and compile-fail test support -- **Documentation**: Doxygen with doxygen-awesome-css theme -- **Development**: clangd compile_commands.json symlink -- **CI/CD**: GitHub Actions workflows with multi-platform testing and documentation deployment +## Reference -### Smart Defaults +### CMake Presets -- **C++17** standard requirement (configurable) -- **Ninja** generator in presets -- **Debug** builds for testing, **Release** for default -- **Build isolation** with separate build directories -- **Two-mode operation**: Full infrastructure when top-level, lightweight when consumed -- **Automatic version detection**: Version is automatically extracted from git tags (e.g., `v1.2.3` becomes `1.2.3`) -- **Always-enabled features**: CI/CD, and CMakePresets.json, are always generated +cpp-library generates a `CMakePresets.json` file with the following configurations: -### Testing Features +- **`default`**: Release build for production use +- **`test`**: Debug build with testing enabled +- **`docs`**: Documentation generation with Doxygen +- **`clang-tidy`**: Static analysis build +- **`install`**: Local installation test (installs to `build/install/prefix`) +- **`init`**: Template regeneration (regenerates CMakePresets.json, CI workflows, etc.) -- **doctest@2.4.12** for unit testing -- **Compile-fail tests**: Automatic detection for examples with `_fail` suffix -- **CTest integration**: Proper test registration and labeling -- **Multi-directory support**: Checks both `tests/` directories - -### Documentation Features - -- **Doxygen integration** with modern configuration -- **doxygen-awesome-css@2.4.1** theme for beautiful output -- **Symbol exclusion** support for implementation details -- **GitHub Pages deployment** via CI -- **Custom Doxyfile support** (falls back to template) +### Version Management -### Development Tools +Version is automatically detected from git tags: -- **clang-tidy integration** via CMakePresets.json -- **clangd support** with compile_commands.json symlink -- **CMakePresets.json** with multiple configurations: - - `default`: Release build - - `test`: Debug build with testing - - `docs`: Documentation generation - - `clang-tidy`: Static analysis - - `init`: Template regeneration (forces regeneration of CMakePresets.json, CI workflows, etc.) +- Supports `v1.2.3` and `1.2.3` tag formats +- Falls back to `0.0.0` if no tag is found (with warning) +- Version used in CMake package config files -### CI/CD Features +### Testing -- **Multi-platform testing**: Ubuntu, macOS, Windows -- **Multi-compiler support**: GCC, Clang, MSVC -- **Static analysis**: clang-tidy integration -- **Documentation deployment**: Automatic GitHub Pages deployment -- **Template generation**: CI workflow generation +- **Test framework**: [doctest](https://github.com/doctest/doctest) +- **Compile-fail tests**: Automatically detected via `_fail` suffix in filenames +- **Test discovery**: Scans `tests/` and `examples/` directories +- **CTest integration**: All tests registered with CTest for IDE integration -### Dependency Management +## Template Files Generated -- **CPM.cmake** integration for seamless fetching -- **Automatic caching** via CPM's built-in mechanisms -- **Version pinning** for reliable builds -- **Git tag versioning** for reliable updates +cpp-library automatically generates infrastructure files on first configuration and when using the `init` preset: -### Version Management +- **CMakePresets.json**: Build configurations (default, test, docs, clang-tidy, install, init) +- **.github/workflows/ci.yml**: Multi-platform CI/CD pipeline with testing and documentation deployment +- **.gitignore**: Standard C++ project ignores +- **.vscode/extensions.json**: Recommended VS Code extensions +- **Package config files**: `Config.cmake` for CMake integration (when building as top-level project) -- **Automatic git tag detection**: Version is automatically extracted from the latest git tag -- **Fallback versioning**: Uses `0.0.0` if no git tag is found (with warning) -- **Tag format support**: Supports both `v1.2.3` and `1.2.3` tag formats +These files are generated automatically. To regenerate with the latest templates, use `cmake --preset=init`. ## Example Projects -This template is used by: +See these projects using cpp-library: - [stlab/enum-ops](https://github.com/stlab/enum-ops) - Type-safe operators for enums - [stlab/copy-on-write](https://github.com/stlab/copy-on-write) - Copy-on-write wrapper -### Real Usage Example (enum-ops) - -```cmake -cmake_minimum_required(VERSION 3.20) -project(enum-ops) - -# Setup cpp-library infrastructure -if(PROJECT_IS_TOP_LEVEL) - set(CPM_SOURCE_CACHE ${CMAKE_SOURCE_DIR}/.cache/cpm CACHE PATH "CPM cache") -endif() -include(cmake/CPM.cmake) - -# Fetch cpp-library via CPM -CPMAddPackage("gh:stlab/cpp-library@4.0.3") -include(${cpp-library_SOURCE_DIR}/cpp-library.cmake) - -# Configure library (handles both lightweight and full modes automatically) -cpp_library_setup( - DESCRIPTION "Type-safe operators for enums" - NAMESPACE stlab - HEADERS enum_ops.hpp - EXAMPLES enum_ops_example_test.cpp enum_ops_example_fail.cpp - TESTS enum_ops_tests.cpp - DOCS_EXCLUDE_SYMBOLS "stlab::implementation" -) -``` - -## Quick Start - -1. **Initialize a new project**: - - ```bash - # Clone or create your project - mkdir my-library && cd my-library - - # Create basic structure - mkdir -p include/your_namespace src examples tests cmake - - # Add CPM.cmake - curl -L https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake -o cmake/CPM.cmake - ``` - -2. **Create CMakeLists.txt** with the usage example above - -3. **Add your headers** to `include/your_namespace/` - -4. **Add examples** to `examples/` (use `_fail` suffix for compile-fail tests, e.g., `example.cpp`, `example_fail.cpp`) - -5. **Add tests** to `tests/` (use `_fail` suffix for compile-fail tests, e.g., `tests.cpp`, `tests_fail.cpp`) - -6. **Build and test**: - - ```bash - cmake --preset=test - cmake --build --preset=test - ctest --preset=test - ``` - -7. **Regenerate templates** (if needed): - ```bash - cmake --preset=init - cmake --build --preset=init - ``` - -## Template Files Generated - -The template automatically generates: - -- **CMakePresets.json**: Build configurations for different purposes -- **.github/workflows/ci.yml**: Multi-platform CI/CD pipeline -- **.gitignore**: Standard ignores for C++ projects -- **src/**: Source directory for non-header-only libraries (auto-detected) -- **Package config files**: For proper CMake integration - ## License Distributed under the Boost Software License, Version 1.0. See `LICENSE`. diff --git a/cmake/cpp-library-install.cmake b/cmake/cpp-library-install.cmake new file mode 100644 index 0000000..631b20b --- /dev/null +++ b/cmake/cpp-library-install.cmake @@ -0,0 +1,93 @@ +# SPDX-License-Identifier: BSL-1.0 +# +# cpp-library-install.cmake - Installation support for cpp-library projects +# +# This module provides minimal but complete CMake installation support for libraries +# built with cpp-library. It handles: +# - Header-only libraries (INTERFACE targets) +# - Static libraries +# - Shared libraries (when BUILD_SHARED_LIBS is ON) +# - CMake package config generation for find_package() support + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +# Function to setup installation for a library target +# This should be called from _cpp_library_setup_core when PROJECT_IS_TOP_LEVEL +function(_cpp_library_setup_install) + set(oneValueArgs + NAME # Target name (e.g., "stlab-enum-ops") + VERSION # Version string (e.g., "1.2.3") + NAMESPACE # Namespace for alias (e.g., "stlab") + ) + set(multiValueArgs + HEADERS # List of header file paths (for FILE_SET support check) + ) + + cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # Validate required arguments + if(NOT ARG_NAME) + message(FATAL_ERROR "_cpp_library_setup_install: NAME is required") + endif() + if(NOT ARG_VERSION) + message(FATAL_ERROR "_cpp_library_setup_install: VERSION is required") + endif() + if(NOT ARG_NAMESPACE) + message(FATAL_ERROR "_cpp_library_setup_install: NAMESPACE is required") + endif() + + # Install the library target + # For header-only libraries (INTERFACE), this installs the target metadata + # For compiled libraries, this installs the library files and headers + if(ARG_HEADERS) + # Install with FILE_SET for modern header installation + install(TARGETS ${ARG_NAME} + EXPORT ${ARG_NAME}Targets + FILE_SET headers DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ) + else() + # Install without FILE_SET (fallback for edge cases) + install(TARGETS ${ARG_NAME} + EXPORT ${ARG_NAME}Targets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ) + endif() + + # Generate package version file + # Uses SameMajorVersion compatibility (e.g., 2.1.0 is compatible with 2.0.0) + write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}ConfigVersion.cmake" + VERSION ${ARG_VERSION} + COMPATIBILITY SameMajorVersion + ) + + # Generate package config file from template + configure_file( + "${CPP_LIBRARY_ROOT}/templates/Config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}Config.cmake" + @ONLY + ) + + # Install export targets with namespace + # This allows downstream projects to use find_package(package-name) + # and link against namespace::target + install(EXPORT ${ARG_NAME}Targets + FILE ${ARG_NAME}Targets.cmake + NAMESPACE ${ARG_NAMESPACE}:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${ARG_NAME} + ) + + # Install package config and version files + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}ConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${ARG_NAME} + ) + +endfunction() diff --git a/cmake/cpp-library-setup.cmake b/cmake/cpp-library-setup.cmake index 131bf23..c615182 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -53,11 +53,12 @@ function(_cpp_library_setup_core) string(REPLACE "${ARG_NAMESPACE}-" "" CLEAN_NAME "${ARG_NAME}") if(ARG_SOURCES) - # Create a regular library if sources are present - add_library(${ARG_NAME} STATIC ${ARG_SOURCES}) + # Create a library with sources (respects BUILD_SHARED_LIBS variable) + add_library(${ARG_NAME} ${ARG_SOURCES}) add_library(${ARG_NAMESPACE}::${CLEAN_NAME} ALIAS ${ARG_NAME}) target_include_directories(${ARG_NAME} PUBLIC $ + $ ) target_compile_features(${ARG_NAME} PUBLIC cxx_std_${ARG_REQUIRES_CPP_VERSION}) if(ARG_HEADERS) @@ -74,6 +75,7 @@ function(_cpp_library_setup_core) add_library(${ARG_NAMESPACE}::${CLEAN_NAME} ALIAS ${ARG_NAME}) target_include_directories(${ARG_NAME} INTERFACE $ + $ ) target_compile_features(${ARG_NAME} INTERFACE cxx_std_${ARG_REQUIRES_CPP_VERSION}) if(ARG_HEADERS) @@ -85,6 +87,16 @@ function(_cpp_library_setup_core) ) endif() endif() + + # Setup installation when building as top-level project + if(ARG_TOP_LEVEL) + _cpp_library_setup_install( + NAME "${ARG_NAME}" + VERSION "${ARG_VERSION}" + NAMESPACE "${ARG_NAMESPACE}" + HEADERS "${ARG_HEADERS}" + ) + endif() endfunction() diff --git a/cpp-library.cmake b/cpp-library.cmake index 4014741..fcc452e 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -15,6 +15,7 @@ include(CTest) include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-setup.cmake") include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-testing.cmake") include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-docs.cmake") +include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-install.cmake") # Shared function to handle examples and tests consistently function(_cpp_library_setup_executables) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index d08a155..bfed9d7 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -43,6 +43,84 @@ jobs: - name: Test run: ctest --preset=test + install-test: + name: Test Installation (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: test + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v5 + + - name: Build and Install + run: | + cmake --preset=default + cmake --build --preset=default + cmake --install build/default --prefix ${{ runner.temp }}/install + + - name: Test find_package + shell: bash + run: | + # Create a minimal test to verify the installation works with find_package + mkdir -p ${{ runner.temp }}/test-find-package + cd ${{ runner.temp }}/test-find-package + + # Get project name from CMakeLists.txt + PROJECT_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\(.*\))/\1/' | awk '{print $1}') + + # Create test CMakeLists.txt + cat > CMakeLists.txt << EOF + cmake_minimum_required(VERSION 3.20) + project(test-find-package CXX) + + set(CMAKE_PREFIX_PATH "${{ runner.temp }}/install") + find_package(\${PROJECT_NAME} REQUIRED) + + message(STATUS "Successfully found \${PROJECT_NAME}") + EOF + + # Test find_package + cmake -B build -S . + + - name: Test CPMFindPackage + shell: bash + run: | + # Create test to verify CPMFindPackage works (tries find_package first, then CPM) + mkdir -p ${{ runner.temp }}/test-cpm + cd ${{ runner.temp }}/test-cpm + + # Download CPM.cmake + mkdir cmake + curl -L https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake -o cmake/CPM.cmake + + # Get project name from CMakeLists.txt + PROJECT_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\(.*\))/\1/' | awk '{print $1}') + + # Create test CMakeLists.txt that uses CPMFindPackage + cat > CMakeLists.txt << EOF + cmake_minimum_required(VERSION 3.20) + project(test-cpm CXX) + + set(CMAKE_PREFIX_PATH "${{ runner.temp }}/install") + set(CPM_SOURCE_CACHE \${CMAKE_SOURCE_DIR}/.cache/cpm CACHE PATH "CPM cache") + include(cmake/CPM.cmake) + + # CPMFindPackage tries find_package first, then falls back to CPMAddPackage + CPMFindPackage( + NAME \${PROJECT_NAME} + GITHUB_REPOSITORY ${{ github.repository }} + GIT_TAG ${{ github.sha }} + ) + + message(STATUS "Successfully acquired \${PROJECT_NAME} via CPMFindPackage") + EOF + + # Test CPMFindPackage (should find the installed version first) + cmake -B build -S . + clang-tidy: runs-on: ubuntu-latest diff --git a/templates/.vscode/extensions.json b/templates/.vscode/extensions.json index 63a4875..6ffa416 100644 --- a/templates/.vscode/extensions.json +++ b/templates/.vscode/extensions.json @@ -1,5 +1,4 @@ { - "_comment": "Auto-generated from cpp-library (https://github.com/stlab/cpp-library) - Do not edit this file directly", "recommendations": [ "matepek.vscode-catch2-test-adapter", "llvm-vs-code-extensions.vscode-clangd", diff --git a/templates/CMakePresets.json b/templates/CMakePresets.json index 1e805dd..c61386b 100644 --- a/templates/CMakePresets.json +++ b/templates/CMakePresets.json @@ -68,6 +68,20 @@ "CMAKE_CXX_EXTENSIONS": "OFF", "CPP_LIBRARY_FORCE_INIT": "ON" } + }, + { + "name": "install", + "displayName": "Local Install Test", + "description": "Configuration for testing installation locally (installs to build/install/prefix)", + "binaryDir": "${sourceDir}/build/install", + "generator": "Ninja", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "BUILD_TESTING": "OFF", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "CMAKE_CXX_EXTENSIONS": "OFF", + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install/prefix" + } } ], "buildPresets": [ @@ -75,7 +89,8 @@ { "name": "test", "displayName": "Build Tests", "configurePreset": "test" }, { "name": "docs", "displayName": "Build Docs", "configurePreset": "docs", "targets": "docs" }, { "name": "clang-tidy", "displayName": "Build with Clang-Tidy", "configurePreset": "clang-tidy" }, - { "name": "init", "displayName": "Initialize Templates", "configurePreset": "init" } + { "name": "init", "displayName": "Initialize Templates", "configurePreset": "init" }, + { "name": "install", "displayName": "Build for Local Install", "configurePreset": "install" } ], "testPresets": [ { "name": "test", "displayName": "Run All Tests", "configurePreset": "test", "output": { "outputOnFailure": true } }, From b69d45f2bbfa7cac1530a0af4cecb50583d3d5a4 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 15:02:08 -0800 Subject: [PATCH 02/23] Update README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 3ffa4a6..295b375 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,28 @@ cpp_library_setup( - Version is automatically detected from git tags (see [Version Tagging](#version-tagging)). - Examples using doctest should include `test` in the filename to be visible in the [C++ TestMate](https://marketplace.visualstudio.com/items?itemName=matepek.vscode-catch2-test-adapter) extension for VS Code test explorer. +### Target Naming + +For `project(your-library)`, `cpp_library_setup` will create a target called `your-library`. + +The utility will additionally create an alias target based on the `NAMESPACE` option: `your_namespace::your-library`. + +If your project name starts with the namespace followed by a dash, the namespace in the project name is stripped from the alias target: + +```cmake +cmake_minimum_required(VERSION 3.20) +project(namespace-library) + +# ... CPM setup ... + +cpp_library_setup( + NAMESPACE namespace + # ... +) +``` + +Results in `namespace-library` and `namespace::library` targets. + ### Path Conventions The template uses consistent path conventions for all file specifications: From e4d9f545785f18cb7d0504bb9fcc10a39f3f0bd0 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 15:31:34 -0800 Subject: [PATCH 03/23] Fixing quoting issue. --- templates/.github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index bfed9d7..39c4096 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -77,9 +77,9 @@ jobs: project(test-find-package CXX) set(CMAKE_PREFIX_PATH "${{ runner.temp }}/install") - find_package(\${PROJECT_NAME} REQUIRED) + find_package(${PROJECT_NAME} REQUIRED) - message(STATUS "Successfully found \${PROJECT_NAME}") + message(STATUS "Successfully found ${PROJECT_NAME}") EOF # Test find_package @@ -110,12 +110,12 @@ jobs: # CPMFindPackage tries find_package first, then falls back to CPMAddPackage CPMFindPackage( - NAME \${PROJECT_NAME} + NAME ${PROJECT_NAME} GITHUB_REPOSITORY ${{ github.repository }} GIT_TAG ${{ github.sha }} ) - message(STATUS "Successfully acquired \${PROJECT_NAME} via CPMFindPackage") + message(STATUS "Successfully acquired ${PROJECT_NAME} via CPMFindPackage") EOF # Test CPMFindPackage (should find the installed version first) From bcb610fa6eec636af724c66f3c80d4441ea7433d Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 16:02:35 -0800 Subject: [PATCH 04/23] Update ci.yml --- templates/.github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index 39c4096..4c37213 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: cd ${{ runner.temp }}/test-find-package # Get project name from CMakeLists.txt - PROJECT_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\(.*\))/\1/' | awk '{print $1}') + PROJECT_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') # Create test CMakeLists.txt cat > CMakeLists.txt << EOF @@ -97,7 +97,7 @@ jobs: curl -L https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake -o cmake/CPM.cmake # Get project name from CMakeLists.txt - PROJECT_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\(.*\))/\1/' | awk '{print $1}') + PROJECT_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') # Create test CMakeLists.txt that uses CPMFindPackage cat > CMakeLists.txt << EOF From 79bc0a0f8680185273ae33477f9fbd7b6d137ccb Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 16:04:58 -0800 Subject: [PATCH 05/23] Update ci.yml --- templates/.github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index 4c37213..b9c1b70 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: cd ${{ runner.temp }}/test-find-package # Get project name from CMakeLists.txt - PROJECT_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') + PACKAGE_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') # Create test CMakeLists.txt cat > CMakeLists.txt << EOF @@ -77,9 +77,9 @@ jobs: project(test-find-package CXX) set(CMAKE_PREFIX_PATH "${{ runner.temp }}/install") - find_package(${PROJECT_NAME} REQUIRED) + find_package(${PACKAGE_NAME} REQUIRED) - message(STATUS "Successfully found ${PROJECT_NAME}") + message(STATUS "Successfully found ${PACKAGE_NAME}") EOF # Test find_package @@ -97,7 +97,7 @@ jobs: curl -L https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake -o cmake/CPM.cmake # Get project name from CMakeLists.txt - PROJECT_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') + PACKAGE_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') # Create test CMakeLists.txt that uses CPMFindPackage cat > CMakeLists.txt << EOF @@ -110,12 +110,12 @@ jobs: # CPMFindPackage tries find_package first, then falls back to CPMAddPackage CPMFindPackage( - NAME ${PROJECT_NAME} + NAME ${PACKAGE_NAME} GITHUB_REPOSITORY ${{ github.repository }} GIT_TAG ${{ github.sha }} ) - message(STATUS "Successfully acquired ${PROJECT_NAME} via CPMFindPackage") + message(STATUS "Successfully acquired ${PACKAGE_NAME} via CPMFindPackage") EOF # Test CPMFindPackage (should find the installed version first) From 7d2ce0bb48837e89b00a4dc1eb780acc04ba89ca Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 16:16:59 -0800 Subject: [PATCH 06/23] Fixing quotes for Windows bash. --- templates/.github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index b9c1b70..574fd28 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: cd ${{ runner.temp }}/test-find-package # Get project name from CMakeLists.txt - PACKAGE_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') + PACKAGE_NAME=$(grep -m1 "project(" "${{ github.workspace }}/CMakeLists.txt" | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') # Create test CMakeLists.txt cat > CMakeLists.txt << EOF @@ -97,7 +97,7 @@ jobs: curl -L https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake -o cmake/CPM.cmake # Get project name from CMakeLists.txt - PACKAGE_NAME=$(grep -m1 "project(" ${{ github.workspace }}/CMakeLists.txt | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') + PACKAGE_NAME=$(grep -m1 "project(" "${{ github.workspace }}/CMakeLists.txt" | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') # Create test CMakeLists.txt that uses CPMFindPackage cat > CMakeLists.txt << EOF From eb92bfda330b7752725f895fb90397ee52aea9e1 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 16:23:59 -0800 Subject: [PATCH 07/23] Trying again to fix quoting... --- templates/.github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index 574fd28..e0dba04 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -71,12 +71,15 @@ jobs: # Get project name from CMakeLists.txt PACKAGE_NAME=$(grep -m1 "project(" "${{ github.workspace }}/CMakeLists.txt" | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') + # Convert paths to forward slashes for CMake (works on all platforms) + INSTALL_PREFIX=$(echo "${{ runner.temp }}/install" | sed 's|\\|/|g') + # Create test CMakeLists.txt cat > CMakeLists.txt << EOF cmake_minimum_required(VERSION 3.20) project(test-find-package CXX) - set(CMAKE_PREFIX_PATH "${{ runner.temp }}/install") + set(CMAKE_PREFIX_PATH "${INSTALL_PREFIX}") find_package(${PACKAGE_NAME} REQUIRED) message(STATUS "Successfully found ${PACKAGE_NAME}") @@ -99,12 +102,15 @@ jobs: # Get project name from CMakeLists.txt PACKAGE_NAME=$(grep -m1 "project(" "${{ github.workspace }}/CMakeLists.txt" | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') + # Convert paths to forward slashes for CMake (works on all platforms) + INSTALL_PREFIX=$(echo "${{ runner.temp }}/install" | sed 's|\\|/|g') + # Create test CMakeLists.txt that uses CPMFindPackage cat > CMakeLists.txt << EOF cmake_minimum_required(VERSION 3.20) project(test-cpm CXX) - set(CMAKE_PREFIX_PATH "${{ runner.temp }}/install") + set(CMAKE_PREFIX_PATH "${INSTALL_PREFIX}") set(CPM_SOURCE_CACHE \${CMAKE_SOURCE_DIR}/.cache/cpm CACHE PATH "CPM cache") include(cmake/CPM.cmake) From b2d8e3ca8fe830d3c6ff6098f78dd8933f7b2437 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 16:31:49 -0800 Subject: [PATCH 08/23] Removing CPMFindPackage CI test This CI test added complexity without adding value. Testing find_package() is sufficient to ensure the package installs correctly. --- templates/.github/workflows/ci.yml | 39 ------------------------------ 1 file changed, 39 deletions(-) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index e0dba04..8ba32b2 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -88,45 +88,6 @@ jobs: # Test find_package cmake -B build -S . - - name: Test CPMFindPackage - shell: bash - run: | - # Create test to verify CPMFindPackage works (tries find_package first, then CPM) - mkdir -p ${{ runner.temp }}/test-cpm - cd ${{ runner.temp }}/test-cpm - - # Download CPM.cmake - mkdir cmake - curl -L https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake -o cmake/CPM.cmake - - # Get project name from CMakeLists.txt - PACKAGE_NAME=$(grep -m1 "project(" "${{ github.workspace }}/CMakeLists.txt" | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') - - # Convert paths to forward slashes for CMake (works on all platforms) - INSTALL_PREFIX=$(echo "${{ runner.temp }}/install" | sed 's|\\|/|g') - - # Create test CMakeLists.txt that uses CPMFindPackage - cat > CMakeLists.txt << EOF - cmake_minimum_required(VERSION 3.20) - project(test-cpm CXX) - - set(CMAKE_PREFIX_PATH "${INSTALL_PREFIX}") - set(CPM_SOURCE_CACHE \${CMAKE_SOURCE_DIR}/.cache/cpm CACHE PATH "CPM cache") - include(cmake/CPM.cmake) - - # CPMFindPackage tries find_package first, then falls back to CPMAddPackage - CPMFindPackage( - NAME ${PACKAGE_NAME} - GITHUB_REPOSITORY ${{ github.repository }} - GIT_TAG ${{ github.sha }} - ) - - message(STATUS "Successfully acquired ${PACKAGE_NAME} via CPMFindPackage") - EOF - - # Test CPMFindPackage (should find the installed version first) - cmake -B build -S . - clang-tidy: runs-on: ubuntu-latest From e927f5b2e6d710f63f49331845d86d3c565400c6 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 16:49:28 -0800 Subject: [PATCH 09/23] Update ci.yml --- templates/.github/workflows/ci.yml | 35 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index 8ba32b2..69556f3 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -13,21 +13,23 @@ on: jobs: test: + name: Test (${{ matrix.name }}) strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - compiler: [gcc, clang, msvc] - exclude: - - os: ubuntu-latest - compiler: msvc - - os: macos-latest - compiler: msvc - - os: macos-latest - compiler: gcc - - os: windows-latest - compiler: gcc - - os: windows-latest - compiler: clang + include: + - name: Ubuntu GCC + os: ubuntu-latest + cc: gcc + cxx: g++ + - name: Ubuntu Clang + os: ubuntu-latest + cc: clang + cxx: clang++ + - name: macOS + os: macos-latest + - name: Windows + os: windows-latest runs-on: ${{ matrix.os }} @@ -36,6 +38,9 @@ jobs: - name: Configure CMake run: cmake --preset=test + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} - name: Build run: cmake --build --preset=test @@ -68,8 +73,8 @@ jobs: mkdir -p ${{ runner.temp }}/test-find-package cd ${{ runner.temp }}/test-find-package - # Get project name from CMakeLists.txt - PACKAGE_NAME=$(grep -m1 "project(" "${{ github.workspace }}/CMakeLists.txt" | sed 's/project(\([^)]*\)).*/\1/' | awk '{print $1}') + # Get project name from CMakeLists.txt (handles multi-line project() declarations) + PACKAGE_NAME=$(sed -n '/project(/,/)/p' "${{ github.workspace }}/CMakeLists.txt" | tr '\n' ' ' | sed 's/.*project([ \t]*\([^ \t)]*\).*/\1/') # Convert paths to forward slashes for CMake (works on all platforms) INSTALL_PREFIX=$(echo "${{ runner.temp }}/install" | sed 's|\\|/|g') From 832313cc8005f67a14960787701426d8ada26f08 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 17:19:47 -0800 Subject: [PATCH 10/23] making the ci.yml file into a cmake template To simplify the ci logic, I'm changing the ci.yml file to a template. --- cmake/cpp-library-ci.cmake | 23 +++++++++++++++++++ cmake/cpp-library-setup.cmake | 23 ++++++++----------- cpp-library.cmake | 1 + .../.github/workflows/{ci.yml => ci.yml.in} | 7 ++---- 4 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 cmake/cpp-library-ci.cmake rename templates/.github/workflows/{ci.yml => ci.yml.in} (90%) diff --git a/cmake/cpp-library-ci.cmake b/cmake/cpp-library-ci.cmake new file mode 100644 index 0000000..c6e9fb6 --- /dev/null +++ b/cmake/cpp-library-ci.cmake @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: BSL-1.0 +# +# cpp-library-ci.cmake - CI/CD configuration for cpp-library projects +# +# This module handles GitHub Actions workflow generation with PROJECT_NAME substitution + +# Function to configure CI workflow template +function(_cpp_library_setup_ci) + set(options FORCE_INIT) + cmake_parse_arguments(ARG "${options}" "" "" ${ARGN}) + + set(ci_template "${CPP_LIBRARY_ROOT}/templates/.github/workflows/ci.yml.in") + set(ci_dest "${CMAKE_CURRENT_SOURCE_DIR}/.github/workflows/ci.yml") + + if(EXISTS "${ci_template}" AND (NOT EXISTS "${ci_dest}" OR ARG_FORCE_INIT)) + get_filename_component(ci_dir "${ci_dest}" DIRECTORY) + file(MAKE_DIRECTORY "${ci_dir}") + configure_file("${ci_template}" "${ci_dest}" @ONLY) + message(STATUS "Configured template file: .github/workflows/ci.yml") + elseif(NOT EXISTS "${ci_template}") + message(WARNING "CI template file not found: ${ci_template}") + endif() +endfunction() diff --git a/cmake/cpp-library-setup.cmake b/cmake/cpp-library-setup.cmake index c615182..ee80a67 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -113,27 +113,22 @@ function(_cpp_library_copy_templates) ".vscode/extensions.json" "docs/index.html" "CMakePresets.json" - ".github/workflows/ci.yml" ) foreach(template_file IN LISTS TEMPLATE_FILES) set(source_file "${CPP_LIBRARY_ROOT}/templates/${template_file}") set(dest_file "${CMAKE_CURRENT_SOURCE_DIR}/${template_file}") - # Check if template file exists - if(EXISTS "${source_file}") - # Copy if file doesn't exist or FORCE_INIT is enabled - if(NOT EXISTS "${dest_file}" OR ARG_FORCE_INIT) - # Create directory if needed - get_filename_component(dest_dir "${dest_file}" DIRECTORY) - file(MAKE_DIRECTORY "${dest_dir}") - - # Copy the file - file(COPY "${source_file}" DESTINATION "${dest_dir}") - message(STATUS "Copied template file: ${template_file}") - endif() - else() + if(EXISTS "${source_file}" AND (NOT EXISTS "${dest_file}" OR ARG_FORCE_INIT)) + get_filename_component(dest_dir "${dest_file}" DIRECTORY) + file(MAKE_DIRECTORY "${dest_dir}") + file(COPY "${source_file}" DESTINATION "${dest_dir}") + message(STATUS "Copied template file: ${template_file}") + elseif(NOT EXISTS "${source_file}") message(WARNING "Template file not found: ${source_file}") endif() endforeach() + + # Setup CI workflow with PROJECT_NAME substitution + _cpp_library_setup_ci(${ARG_FORCE_INIT}) endfunction() diff --git a/cpp-library.cmake b/cpp-library.cmake index fcc452e..410ae6d 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -16,6 +16,7 @@ include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-setup.cmake") include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-testing.cmake") include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-docs.cmake") include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-install.cmake") +include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-ci.cmake") # Shared function to handle examples and tests consistently function(_cpp_library_setup_executables) diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml.in similarity index 90% rename from templates/.github/workflows/ci.yml rename to templates/.github/workflows/ci.yml.in index 69556f3..467e313 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml.in @@ -73,9 +73,6 @@ jobs: mkdir -p ${{ runner.temp }}/test-find-package cd ${{ runner.temp }}/test-find-package - # Get project name from CMakeLists.txt (handles multi-line project() declarations) - PACKAGE_NAME=$(sed -n '/project(/,/)/p' "${{ github.workspace }}/CMakeLists.txt" | tr '\n' ' ' | sed 's/.*project([ \t]*\([^ \t)]*\).*/\1/') - # Convert paths to forward slashes for CMake (works on all platforms) INSTALL_PREFIX=$(echo "${{ runner.temp }}/install" | sed 's|\\|/|g') @@ -85,9 +82,9 @@ jobs: project(test-find-package CXX) set(CMAKE_PREFIX_PATH "${INSTALL_PREFIX}") - find_package(${PACKAGE_NAME} REQUIRED) + find_package(@PROJECT_NAME@ REQUIRED) - message(STATUS "Successfully found ${PACKAGE_NAME}") + message(STATUS "Successfully found @PROJECT_NAME@") EOF # Test find_package From 77c30bc9d7b0c35359a1f0ef951e63e60e24edf7 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 17:36:17 -0800 Subject: [PATCH 11/23] Fixing force init for CI and simplifying CI template. --- cmake/cpp-library-ci.cmake | 8 +++----- templates/.github/workflows/ci.yml.in | 11 +++++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/cmake/cpp-library-ci.cmake b/cmake/cpp-library-ci.cmake index c6e9fb6..87e729b 100644 --- a/cmake/cpp-library-ci.cmake +++ b/cmake/cpp-library-ci.cmake @@ -5,14 +5,12 @@ # This module handles GitHub Actions workflow generation with PROJECT_NAME substitution # Function to configure CI workflow template -function(_cpp_library_setup_ci) - set(options FORCE_INIT) - cmake_parse_arguments(ARG "${options}" "" "" ${ARGN}) - +# Arguments: force_init (boolean) - whether to overwrite existing file +function(_cpp_library_setup_ci force_init) set(ci_template "${CPP_LIBRARY_ROOT}/templates/.github/workflows/ci.yml.in") set(ci_dest "${CMAKE_CURRENT_SOURCE_DIR}/.github/workflows/ci.yml") - if(EXISTS "${ci_template}" AND (NOT EXISTS "${ci_dest}" OR ARG_FORCE_INIT)) + if(EXISTS "${ci_template}" AND (NOT EXISTS "${ci_dest}" OR force_init)) get_filename_component(ci_dir "${ci_dest}" DIRECTORY) file(MAKE_DIRECTORY "${ci_dir}") configure_file("${ci_template}" "${ci_dest}" @ONLY) diff --git a/templates/.github/workflows/ci.yml.in b/templates/.github/workflows/ci.yml.in index 467e313..3e2619a 100644 --- a/templates/.github/workflows/ci.yml.in +++ b/templates/.github/workflows/ci.yml.in @@ -73,22 +73,21 @@ jobs: mkdir -p ${{ runner.temp }}/test-find-package cd ${{ runner.temp }}/test-find-package - # Convert paths to forward slashes for CMake (works on all platforms) - INSTALL_PREFIX=$(echo "${{ runner.temp }}/install" | sed 's|\\|/|g') - # Create test CMakeLists.txt cat > CMakeLists.txt << EOF cmake_minimum_required(VERSION 3.20) project(test-find-package CXX) - set(CMAKE_PREFIX_PATH "${INSTALL_PREFIX}") find_package(@PROJECT_NAME@ REQUIRED) message(STATUS "Successfully found @PROJECT_NAME@") EOF - # Test find_package - cmake -B build -S . + # Convert paths to forward slashes for CMake (works on all platforms) + INSTALL_PREFIX=$(echo "${{ runner.temp }}/install" | sed 's|\\|/|g') + + # Test find_package with CMAKE_PREFIX_PATH + cmake -B build -S . -DCMAKE_PREFIX_PATH="${INSTALL_PREFIX}" clang-tidy: runs-on: ubuntu-latest From 13cf43f82bdad1a0b3b7d91cbfc50a8a66a4c77b Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 11 Nov 2025 22:38:06 -0800 Subject: [PATCH 12/23] Adding contracts for CMake functions. --- cmake/cpp-library-ci.cmake | 5 +++-- cmake/cpp-library-docs.cmake | 3 +++ cmake/cpp-library-install.cmake | 6 ++++-- cmake/cpp-library-setup.cmake | 10 ++++++++-- cmake/cpp-library-testing.cmake | 3 ++- cpp-library.cmake | 10 ++++++++-- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/cmake/cpp-library-ci.cmake b/cmake/cpp-library-ci.cmake index 87e729b..8dee755 100644 --- a/cmake/cpp-library-ci.cmake +++ b/cmake/cpp-library-ci.cmake @@ -4,8 +4,9 @@ # # This module handles GitHub Actions workflow generation with PROJECT_NAME substitution -# Function to configure CI workflow template -# Arguments: force_init (boolean) - whether to overwrite existing file +# Generates GitHub Actions CI workflow from template with PROJECT_NAME substitution. +# - Postcondition: .github/workflows/ci.yml created from template if not present +# - With force_init: overwrites existing workflow file function(_cpp_library_setup_ci force_init) set(ci_template "${CPP_LIBRARY_ROOT}/templates/.github/workflows/ci.yml.in") set(ci_dest "${CMAKE_CURRENT_SOURCE_DIR}/.github/workflows/ci.yml") diff --git a/cmake/cpp-library-docs.cmake b/cmake/cpp-library-docs.cmake index 792f086..d8e42b8 100644 --- a/cmake/cpp-library-docs.cmake +++ b/cmake/cpp-library-docs.cmake @@ -2,6 +2,9 @@ # # cpp-library-docs.cmake - Documentation setup with Doxygen +# Creates 'docs' target for generating API documentation with Doxygen and doxygen-awesome-css theme. +# - Precondition: NAME, VERSION, and DESCRIPTION specified; Doxygen available +# - Postcondition: 'docs' custom target created, Doxyfile configured, theme downloaded via CPM function(_cpp_library_setup_docs) set(oneValueArgs NAME diff --git a/cmake/cpp-library-install.cmake b/cmake/cpp-library-install.cmake index 631b20b..605df55 100644 --- a/cmake/cpp-library-install.cmake +++ b/cmake/cpp-library-install.cmake @@ -12,8 +12,10 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) -# Function to setup installation for a library target -# This should be called from _cpp_library_setup_core when PROJECT_IS_TOP_LEVEL +# Configures CMake install rules for library target and package config files. +# - Precondition: 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 function(_cpp_library_setup_install) set(oneValueArgs NAME # Target name (e.g., "stlab-enum-ops") diff --git a/cmake/cpp-library-setup.cmake b/cmake/cpp-library-setup.cmake index ee80a67..afcadd6 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -2,7 +2,8 @@ # # cpp-library-setup.cmake - Core library setup functionality -# Function to get version from git tags +# Returns version string from latest git tag, falling back to "0.0.0". +# - Postcondition: OUTPUT_VAR set to version string with 'v' prefix removed function(_cpp_library_get_git_version OUTPUT_VAR) # Try to get version from git tags execute_process( @@ -24,6 +25,9 @@ function(_cpp_library_get_git_version OUTPUT_VAR) endif() endfunction() +# Creates library target (INTERFACE or compiled) with headers and proper configuration. +# - Precondition: NAME, NAMESPACE, and REQUIRES_CPP_VERSION specified +# - Postcondition: library target created with alias NAMESPACE::CLEAN_NAME, install configured if TOP_LEVEL function(_cpp_library_setup_core) set(oneValueArgs NAME @@ -100,7 +104,9 @@ function(_cpp_library_setup_core) endfunction() -# Function to copy static template files +# Copies template files (.clang-format, .gitignore, etc.) to project root if not present. +# - Postcondition: missing template files copied to project, CI workflow configured with PROJECT_NAME substitution +# - With FORCE_INIT: overwrites existing files function(_cpp_library_copy_templates) set(options FORCE_INIT) cmake_parse_arguments(ARG "${options}" "" "" ${ARGN}) diff --git a/cmake/cpp-library-testing.cmake b/cmake/cpp-library-testing.cmake index 22fd18a..8072871 100644 --- a/cmake/cpp-library-testing.cmake +++ b/cmake/cpp-library-testing.cmake @@ -6,7 +6,8 @@ # This file is kept for backward compatibility but the actual implementation # is now in the _cpp_library_setup_executables function. -# Legacy function - now delegates to the consolidated implementation +# Delegates to _cpp_library_setup_executables for backward compatibility. +# - Postcondition: test executables configured via _cpp_library_setup_executables function(_cpp_library_setup_testing) set(oneValueArgs NAME diff --git a/cpp-library.cmake b/cpp-library.cmake index 410ae6d..b4c36e4 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -18,7 +18,10 @@ include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-docs.cmake") include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-install.cmake") include("${CPP_LIBRARY_ROOT}/cmake/cpp-library-ci.cmake") -# Shared function to handle examples and tests consistently +# Creates test or example executables and registers them with CTest. +# - Precondition: doctest target available via CPM, source files exist in TYPE directory +# - Postcondition: executables created and added as tests (unless in clang-tidy mode) +# - Executables with "_fail" suffix are added as negative compilation tests function(_cpp_library_setup_executables) set(oneValueArgs NAME @@ -99,7 +102,10 @@ function(_cpp_library_setup_executables) endfunction() -# Main entry point function - users call this to set up their library +# Sets up a C++ header-only or compiled library with testing, docs, and install support. +# - 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, docs, and installation function(cpp_library_setup) # Parse arguments set(oneValueArgs From 562ad63de6e88cfa2bd43328e77cada0d6b8dbf4 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Wed, 12 Nov 2025 10:03:48 -0800 Subject: [PATCH 13/23] Update CI workflow to conditionally run CMake steps Adds conditional execution for environment setup and CMake configuration steps based on the presence of matrix.cc. --- templates/.github/workflows/ci.yml.in | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/templates/.github/workflows/ci.yml.in b/templates/.github/workflows/ci.yml.in index 3e2619a..ac88633 100644 --- a/templates/.github/workflows/ci.yml.in +++ b/templates/.github/workflows/ci.yml.in @@ -41,6 +41,11 @@ jobs: env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} + if: ${{ matrix.cc }} + + - name: Configure CMake + run: cmake --preset=test + if: ${{ !matrix.cc }} - name: Build run: cmake --build --preset=test From 49eaf58892f3053458ee4f681dfdb4a50e0f1ee4 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Fri, 14 Nov 2025 10:35:29 -0800 Subject: [PATCH 14/23] Add PACKAGE_NAME to install setup and improve version detection The _cpp_library_setup_install function now requires a PACKAGE_NAME argument, ensuring correct package naming in generated CMake config files and install locations. Version detection in _cpp_library_get_git_version now prefers PROJECT_VERSION if set, improving compatibility with package managers and source archives. --- cmake/cpp-library-install.cmake | 26 +++++++++++++++----------- cmake/cpp-library-setup.cmake | 12 ++++++++++-- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/cmake/cpp-library-install.cmake b/cmake/cpp-library-install.cmake index 605df55..268edde 100644 --- a/cmake/cpp-library-install.cmake +++ b/cmake/cpp-library-install.cmake @@ -13,14 +13,15 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) # Configures CMake install rules for library target and package config files. -# - Precondition: NAME, VERSION, and NAMESPACE specified; target NAME exists +# - 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 function(_cpp_library_setup_install) set(oneValueArgs - NAME # Target name (e.g., "stlab-enum-ops") - VERSION # Version string (e.g., "1.2.3") - NAMESPACE # Namespace for alias (e.g., "stlab") + NAME # Target name (e.g., "stlab-enum-ops") + PACKAGE_NAME # Package name for find_package() (e.g., "enum-ops") + VERSION # Version string (e.g., "1.2.3") + NAMESPACE # Namespace for alias (e.g., "stlab") ) set(multiValueArgs HEADERS # List of header file paths (for FILE_SET support check) @@ -32,6 +33,9 @@ function(_cpp_library_setup_install) if(NOT ARG_NAME) message(FATAL_ERROR "_cpp_library_setup_install: NAME is required") endif() + if(NOT ARG_PACKAGE_NAME) + message(FATAL_ERROR "_cpp_library_setup_install: PACKAGE_NAME is required") + endif() if(NOT ARG_VERSION) message(FATAL_ERROR "_cpp_library_setup_install: VERSION is required") endif() @@ -64,7 +68,7 @@ function(_cpp_library_setup_install) # Generate package version file # Uses SameMajorVersion compatibility (e.g., 2.1.0 is compatible with 2.0.0) write_basic_package_version_file( - "${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}ConfigVersion.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_PACKAGE_NAME}ConfigVersion.cmake" VERSION ${ARG_VERSION} COMPATIBILITY SameMajorVersion ) @@ -72,7 +76,7 @@ function(_cpp_library_setup_install) # Generate package config file from template configure_file( "${CPP_LIBRARY_ROOT}/templates/Config.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_PACKAGE_NAME}Config.cmake" @ONLY ) @@ -80,16 +84,16 @@ function(_cpp_library_setup_install) # This allows downstream projects to use find_package(package-name) # and link against namespace::target install(EXPORT ${ARG_NAME}Targets - FILE ${ARG_NAME}Targets.cmake + FILE ${ARG_PACKAGE_NAME}Targets.cmake NAMESPACE ${ARG_NAMESPACE}:: - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${ARG_NAME} + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${ARG_PACKAGE_NAME} ) # Install package config and version files install(FILES - "${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}Config.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/${ARG_NAME}ConfigVersion.cmake" - DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${ARG_NAME} + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_PACKAGE_NAME}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_PACKAGE_NAME}ConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${ARG_PACKAGE_NAME} ) endfunction() diff --git a/cmake/cpp-library-setup.cmake b/cmake/cpp-library-setup.cmake index afcadd6..06e9633 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -2,9 +2,16 @@ # # cpp-library-setup.cmake - Core library setup functionality -# Returns version string from latest git tag, falling back to "0.0.0". -# - Postcondition: OUTPUT_VAR set to version string with 'v' prefix removed +# Returns version string from PROJECT_VERSION (if set), git tag (with 'v' prefix removed), or +# "0.0.0" fallback function(_cpp_library_get_git_version OUTPUT_VAR) + # If PROJECT_VERSION is already set (e.g., by vcpkg or other package manager), + # use it instead of trying to query git (which may not be available in source archives) + if(DEFINED PROJECT_VERSION AND NOT PROJECT_VERSION STREQUAL "") + set(${OUTPUT_VAR} "${PROJECT_VERSION}" PARENT_SCOPE) + return() + endif() + # Try to get version from git tags execute_process( COMMAND git describe --tags --abbrev=0 @@ -96,6 +103,7 @@ function(_cpp_library_setup_core) if(ARG_TOP_LEVEL) _cpp_library_setup_install( NAME "${ARG_NAME}" + PACKAGE_NAME "${CLEAN_NAME}" VERSION "${ARG_VERSION}" NAMESPACE "${ARG_NAMESPACE}" HEADERS "${ARG_HEADERS}" From fc7d4e47a27010ddc5e874299659ef8b7106eae3 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Fri, 14 Nov 2025 10:44:26 -0800 Subject: [PATCH 15/23] Automate find_dependency generation for installed packages Added logic to introspect INTERFACE_LINK_LIBRARIES and generate appropriate find_dependency() calls in installed CMake package config files. Updated documentation to explain dependency handling, added a helper function in cpp-library-install.cmake, and modified the config template to include generated dependencies. This ensures downstream users automatically find and link required dependencies. --- README.md | 40 +++++++++++++++++++++++ cmake/cpp-library-install.cmake | 56 +++++++++++++++++++++++++++++++++ templates/Config.cmake.in | 5 ++- 3 files changed, 100 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 295b375..d43b8b3 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,46 @@ cmake --install build/default --prefix /opt/mylib For information about using installed packages with `find_package()`, see the [CPM.cmake documentation](https://github.com/cpm-cmake/CPM.cmake) about [controlling how dependencies are found](https://github.com/cpm-cmake/CPM.cmake#cpm_use_local_packages). +#### Dependency Handling in Installed Packages + +cpp-library automatically generates correct `find_dependency()` calls in the installed CMake package configuration files by introspecting your target's `INTERFACE_LINK_LIBRARIES`. This ensures downstream users can find and link all required dependencies. + +**How it works:** + +When you link dependencies to your target using `target_link_libraries()`, cpp-library analyzes these links during installation and generates appropriate `find_dependency()` calls. For example: + +```cmake +# In your library's CMakeLists.txt +add_library(my-lib INTERFACE) + +# Link dependencies - these will be automatically handled during installation +target_link_libraries(my-lib INTERFACE + stlab::copy-on-write # Internal CPM dependency + stlab::enum-ops # Internal CPM dependency + Threads::Threads # System dependency +) +``` + +When installed, the generated `my-libConfig.cmake` will include: + +```cmake +include(CMakeFindDependencyMacro) + +# Find dependencies required by this package +find_dependency(copy-on-write) +find_dependency(enum-ops) +find_dependency(Threads) + +include("${CMAKE_CURRENT_LIST_DIR}/my-libTargets.cmake") +``` + +**Supported dependency patterns:** + +- **CPM/Internal dependencies** (`namespace::target`): Automatically mapped to their package names (e.g., `stlab::copy-on-write` → `find_dependency(copy-on-write)`) +- **Threading** (`Threads::Threads`): Generates `find_dependency(Threads)` +- **Qt libraries** (`Qt5::Core`, `Qt6::Widgets`): Generates `find_dependency(Qt5 COMPONENTS Core)` with proper components +- **Generic packages** (`PackageName::Target`): Generates `find_dependency(PackageName)` + ### Updating cpp-library To update to the latest version of cpp-library in your project: diff --git a/cmake/cpp-library-install.cmake b/cmake/cpp-library-install.cmake index 268edde..17bf847 100644 --- a/cmake/cpp-library-install.cmake +++ b/cmake/cpp-library-install.cmake @@ -12,6 +12,58 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) +# Generates find_dependency() calls for target's INTERFACE link libraries +# - Precondition: TARGET_NAME specifies existing target with INTERFACE_LINK_LIBRARIES +# - Postcondition: OUTPUT_VAR contains newline-separated find_dependency() calls for public dependencies +# - Handles common patterns: namespace::target from CPM, Qt5/Qt6::Component, Threads::Threads, etc. +function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE) + get_target_property(LINK_LIBS ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) + + if(NOT LINK_LIBS) + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + + set(DEPENDENCY_LIST "") + + foreach(LIB IN LISTS LINK_LIBS) + # Skip generator expressions (typically BUILD_INTERFACE dependencies) + if(LIB MATCHES "^\\$<") + continue() + endif() + + # Parse namespaced target: PackageName::Component + if(LIB MATCHES "^([^:]+)::(.+)$") + set(PKG_NAME "${CMAKE_MATCH_1}") + set(COMPONENT "${CMAKE_MATCH_2}") + + # Determine find_dependency() call based on package pattern + if(PKG_NAME STREQUAL NAMESPACE) + # Internal dependency: use component as package name (e.g., stlab::copy-on-write → copy-on-write) + list(APPEND DEPENDENCY_LIST "find_dependency(${COMPONENT})") + elseif(PKG_NAME STREQUAL "Threads") + list(APPEND DEPENDENCY_LIST "find_dependency(Threads)") + elseif(PKG_NAME MATCHES "^Qt[56]$") + # Qt with component (e.g., Qt5::Core → find_dependency(Qt5 COMPONENTS Core)) + list(APPEND DEPENDENCY_LIST "find_dependency(${PKG_NAME} COMPONENTS ${COMPONENT})") + else() + # Generic package (e.g., libdispatch::libdispatch → libdispatch) + list(APPEND DEPENDENCY_LIST "find_dependency(${PKG_NAME})") + endif() + endif() + endforeach() + + # Remove duplicates and convert to newline-separated string + if(DEPENDENCY_LIST) + list(REMOVE_DUPLICATES DEPENDENCY_LIST) + list(JOIN DEPENDENCY_LIST "\n" DEPENDENCY_LINES) + else() + set(DEPENDENCY_LINES "") + endif() + + set(${OUTPUT_VAR} "${DEPENDENCY_LINES}" PARENT_SCOPE) +endfunction() + # Configures CMake install rules for library target and package config files. # - Precondition: NAME, PACKAGE_NAME, VERSION, and NAMESPACE specified; target NAME exists # - Postcondition: install rules created for target, config files, and export with NAMESPACE:: prefix @@ -65,6 +117,9 @@ function(_cpp_library_setup_install) ) endif() + # Generate find_dependency() calls for package dependencies + _cpp_library_generate_dependencies(PACKAGE_DEPENDENCIES ${ARG_NAME} ${ARG_NAMESPACE}) + # Generate package version file # Uses SameMajorVersion compatibility (e.g., 2.1.0 is compatible with 2.0.0) write_basic_package_version_file( @@ -74,6 +129,7 @@ function(_cpp_library_setup_install) ) # Generate package config file from template + # PACKAGE_DEPENDENCIES will be substituted via @PACKAGE_DEPENDENCIES@ configure_file( "${CPP_LIBRARY_ROOT}/templates/Config.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/${ARG_PACKAGE_NAME}Config.cmake" diff --git a/templates/Config.cmake.in b/templates/Config.cmake.in index 2828e4e..d917a3e 100644 --- a/templates/Config.cmake.in +++ b/templates/Config.cmake.in @@ -3,4 +3,7 @@ include(CMakeFindDependencyMacro) -include("${CMAKE_CURRENT_LIST_DIR}/@ARG_NAME@Targets.cmake") +# Find dependencies required by this package +@PACKAGE_DEPENDENCIES@ + +include("${CMAKE_CURRENT_LIST_DIR}/@ARG_PACKAGE_NAME@Targets.cmake") From c1bce1c14c9154383a182c6b30d48868409fdf77 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Mon, 17 Nov 2025 14:50:24 -0800 Subject: [PATCH 16/23] Refactor to use PACKAGE_NAME for target and CI consistency Replaces usage of a 'clean' name with PACKAGE_NAME for library target aliases and CI workflow templates. Ensures consistent naming between CMake targets and package discovery, improving clarity and reducing potential mismatches. --- cmake/cpp-library-setup.cmake | 14 ++++++-------- cpp-library.cmake | 4 ++++ templates/.github/workflows/ci.yml.in | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cmake/cpp-library-setup.cmake b/cmake/cpp-library-setup.cmake index 06e9633..cd3c5e9 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -33,14 +33,15 @@ function(_cpp_library_get_git_version OUTPUT_VAR) endfunction() # Creates library target (INTERFACE or compiled) with headers and proper configuration. -# - Precondition: NAME, NAMESPACE, and REQUIRES_CPP_VERSION specified -# - Postcondition: library target created with alias NAMESPACE::CLEAN_NAME, install configured if TOP_LEVEL +# - Precondition: NAME, NAMESPACE, PACKAGE_NAME, and REQUIRES_CPP_VERSION specified +# - Postcondition: library target created with alias NAMESPACE::PACKAGE_NAME, install configured if TOP_LEVEL function(_cpp_library_setup_core) set(oneValueArgs NAME VERSION DESCRIPTION NAMESPACE + PACKAGE_NAME REQUIRES_CPP_VERSION TOP_LEVEL ) @@ -60,13 +61,10 @@ function(_cpp_library_setup_core) # Note: Project declaration is now handled in the main cpp_library_setup function # No need to check ARG_TOP_LEVEL here for project declaration - # Extract the library name without namespace prefix for target naming - string(REPLACE "${ARG_NAMESPACE}-" "" CLEAN_NAME "${ARG_NAME}") - if(ARG_SOURCES) # Create a library with sources (respects BUILD_SHARED_LIBS variable) add_library(${ARG_NAME} ${ARG_SOURCES}) - add_library(${ARG_NAMESPACE}::${CLEAN_NAME} ALIAS ${ARG_NAME}) + add_library(${ARG_NAMESPACE}::${ARG_PACKAGE_NAME} ALIAS ${ARG_NAME}) target_include_directories(${ARG_NAME} PUBLIC $ $ @@ -83,7 +81,7 @@ function(_cpp_library_setup_core) else() # Header-only INTERFACE target add_library(${ARG_NAME} INTERFACE) - add_library(${ARG_NAMESPACE}::${CLEAN_NAME} ALIAS ${ARG_NAME}) + add_library(${ARG_NAMESPACE}::${ARG_PACKAGE_NAME} ALIAS ${ARG_NAME}) target_include_directories(${ARG_NAME} INTERFACE $ $ @@ -103,7 +101,7 @@ function(_cpp_library_setup_core) if(ARG_TOP_LEVEL) _cpp_library_setup_install( NAME "${ARG_NAME}" - PACKAGE_NAME "${CLEAN_NAME}" + PACKAGE_NAME "${ARG_PACKAGE_NAME}" VERSION "${ARG_VERSION}" NAMESPACE "${ARG_NAMESPACE}" HEADERS "${ARG_HEADERS}" diff --git a/cpp-library.cmake b/cpp-library.cmake index b4c36e4..48ad743 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -140,6 +140,9 @@ function(cpp_library_setup) endif() set(ARG_NAME "${PROJECT_NAME}") + # Calculate PACKAGE_NAME (clean name without namespace prefix) for template substitution + string(REPLACE "${ARG_NAMESPACE}-" "" PACKAGE_NAME "${ARG_NAME}") + # Set defaults if(NOT ARG_REQUIRES_CPP_VERSION) set(ARG_REQUIRES_CPP_VERSION 17) @@ -184,6 +187,7 @@ function(cpp_library_setup) VERSION "${ARG_VERSION}" DESCRIPTION "${ARG_DESCRIPTION}" NAMESPACE "${ARG_NAMESPACE}" + PACKAGE_NAME "${PACKAGE_NAME}" HEADERS "${GENERATED_HEADERS}" SOURCES "${GENERATED_SOURCES}" REQUIRES_CPP_VERSION "${ARG_REQUIRES_CPP_VERSION}" diff --git a/templates/.github/workflows/ci.yml.in b/templates/.github/workflows/ci.yml.in index ac88633..9418d0d 100644 --- a/templates/.github/workflows/ci.yml.in +++ b/templates/.github/workflows/ci.yml.in @@ -83,9 +83,9 @@ jobs: cmake_minimum_required(VERSION 3.20) project(test-find-package CXX) - find_package(@PROJECT_NAME@ REQUIRED) + find_package(@PACKAGE_NAME@ REQUIRED) - message(STATUS "Successfully found @PROJECT_NAME@") + message(STATUS "Successfully found @PACKAGE_NAME@") EOF # Convert paths to forward slashes for CMake (works on all platforms) From 39419e2eb68019fcf8db3af237316308ddf3ec59 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Mon, 17 Nov 2025 15:17:17 -0800 Subject: [PATCH 17/23] Add custom dependency mapping for CMake package generation Introduces the cpp_library_map_dependency() function to allow custom find_dependency() calls for specific targets, such as Qt components requiring COMPONENTS syntax. Updates documentation and dependency generation logic to prioritize custom mappings, improving flexibility for complex dependencies in installed CMake package config files. --- README.md | 71 ++++++++++++++++++++++++++++++--- cmake/cpp-library-install.cmake | 30 +++++++++----- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d43b8b3..54c250c 100644 --- a/README.md +++ b/README.md @@ -156,12 +156,35 @@ find_dependency(Threads) include("${CMAKE_CURRENT_LIST_DIR}/my-libTargets.cmake") ``` -**Supported dependency patterns:** +**Default dependency handling:** -- **CPM/Internal dependencies** (`namespace::target`): Automatically mapped to their package names (e.g., `stlab::copy-on-write` → `find_dependency(copy-on-write)`) -- **Threading** (`Threads::Threads`): Generates `find_dependency(Threads)` -- **Qt libraries** (`Qt5::Core`, `Qt6::Widgets`): Generates `find_dependency(Qt5 COMPONENTS Core)` with proper components -- **Generic packages** (`PackageName::Target`): Generates `find_dependency(PackageName)` +- **cpp-library dependencies** (matching your project's `NAMESPACE`): Automatically mapped to their package names (e.g., `stlab::copy-on-write` → `find_dependency(copy-on-write)`) +- **Other packages**: Uses the package name only by default (e.g., `PackageName::Target` → `find_dependency(PackageName)`) + +**Custom dependency mappings:** + +For dependencies that require special `find_dependency()` syntax (e.g., Qt with COMPONENTS), use `cpp_library_map_dependency()` to specify the exact call: + +```cmake +# Map Qt components to use COMPONENTS syntax +cpp_library_map_dependency("Qt5::Core" "Qt5 COMPONENTS Core") +cpp_library_map_dependency("Qt5::Widgets" "Qt5 COMPONENTS Widgets") + +# Then link as usual +target_link_libraries(my-lib INTERFACE + Qt5::Core + Qt5::Widgets + Threads::Threads # Works automatically, no mapping needed +) +``` + +The generated config file will use your custom mappings where specified: + +```cmake +find_dependency(Qt5 COMPONENTS Core) +find_dependency(Qt5 COMPONENTS Widgets) +find_dependency(Threads) # Automatic from Threads::Threads +``` ### Updating cpp-library @@ -259,6 +282,44 @@ cpp_library_setup( Results in `namespace-library` and `namespace::library` targets. +### `cpp_library_map_dependency` + +```cmake +cpp_library_map_dependency(target find_dependency_call) +``` + +Registers a custom dependency mapping for `find_dependency()` generation in installed CMake package config files. + +**Parameters:** + +- `target`: The namespaced target (e.g., `"Qt5::Core"`, `"Threads::Threads"`) +- `find_dependency_call`: The exact arguments to pass to `find_dependency()` (e.g., `"Qt5 COMPONENTS Core"`, `"Threads"`) + +**When to use:** + +- Dependencies requiring `COMPONENTS` syntax (e.g., Qt) +- Dependencies requiring `OPTIONAL_COMPONENTS` or other special arguments +- Dependencies where the target name pattern doesn't match the desired `find_dependency()` call + +**Note:** Most common dependencies like `Threads::Threads`, `Boost::filesystem`, etc. work automatically with the default behavior and don't need mapping. + +**Example:** + +```cmake +# Register mappings for dependencies needing special syntax +cpp_library_map_dependency("Qt5::Core" "Qt5 COMPONENTS Core") +cpp_library_map_dependency("Qt5::Widgets" "Qt5 COMPONENTS Widgets") + +# Then link normally +target_link_libraries(my-target INTERFACE + Qt5::Core + Qt5::Widgets + Threads::Threads # No mapping needed +) +``` + +See [Dependency Handling in Installed Packages](#dependency-handling-in-installed-packages) for more details. + ### Path Conventions The template uses consistent path conventions for all file specifications: diff --git a/cmake/cpp-library-install.cmake b/cmake/cpp-library-install.cmake index 17bf847..a2e20ac 100644 --- a/cmake/cpp-library-install.cmake +++ b/cmake/cpp-library-install.cmake @@ -12,10 +12,19 @@ include(GNUInstallDirs) include(CMakePackageConfigHelpers) +# Registers a custom dependency mapping for find_dependency() generation +# - Precondition: TARGET is a namespaced target (e.g., "Qt5::Core", "Qt5::Widgets") +# - Postcondition: FIND_DEPENDENCY_CALL stored for TARGET, used in package config generation +# - Example: cpp_library_map_dependency("Qt5::Core" "Qt5 COMPONENTS Core") +function(cpp_library_map_dependency TARGET FIND_DEPENDENCY_CALL) + set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEPENDENCY_MAP_${TARGET} "${FIND_DEPENDENCY_CALL}") +endfunction() + # Generates find_dependency() calls for target's INTERFACE link libraries # - Precondition: TARGET_NAME specifies existing target with INTERFACE_LINK_LIBRARIES # - Postcondition: OUTPUT_VAR contains newline-separated find_dependency() calls for public dependencies -# - Handles common patterns: namespace::target from CPM, Qt5/Qt6::Component, Threads::Threads, etc. +# - Uses cpp_library_map_dependency() mappings if registered, otherwise uses defaults +# - Automatically handles cpp-library dependencies (matching NAMESPACE) function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE) get_target_property(LINK_LIBS ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) @@ -37,17 +46,18 @@ function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE) set(PKG_NAME "${CMAKE_MATCH_1}") set(COMPONENT "${CMAKE_MATCH_2}") - # Determine find_dependency() call based on package pattern - if(PKG_NAME STREQUAL NAMESPACE) - # Internal dependency: use component as package name (e.g., stlab::copy-on-write → copy-on-write) + # Check for custom mapping first + get_property(CUSTOM_MAPPING GLOBAL PROPERTY _CPP_LIBRARY_DEPENDENCY_MAP_${LIB}) + + if(CUSTOM_MAPPING) + # Use custom mapping (e.g., "Qt5 COMPONENTS Core" for Qt5::Core) + list(APPEND DEPENDENCY_LIST "find_dependency(${CUSTOM_MAPPING})") + elseif(PKG_NAME STREQUAL NAMESPACE) + # Internal cpp-library dependency: use component as package name + # (e.g., stlab::copy-on-write → find_dependency(copy-on-write)) list(APPEND DEPENDENCY_LIST "find_dependency(${COMPONENT})") - elseif(PKG_NAME STREQUAL "Threads") - list(APPEND DEPENDENCY_LIST "find_dependency(Threads)") - elseif(PKG_NAME MATCHES "^Qt[56]$") - # Qt with component (e.g., Qt5::Core → find_dependency(Qt5 COMPONENTS Core)) - list(APPEND DEPENDENCY_LIST "find_dependency(${PKG_NAME} COMPONENTS ${COMPONENT})") else() - # Generic package (e.g., libdispatch::libdispatch → libdispatch) + # Default: use package name only (e.g., libdispatch::libdispatch → find_dependency(libdispatch)) list(APPEND DEPENDENCY_LIST "find_dependency(${PKG_NAME})") endif() endif() From e940916f9d3d3de0c2993fcbd33b18b3e1a1e767 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Mon, 17 Nov 2025 17:26:53 -0800 Subject: [PATCH 18/23] Refactor template and CI setup to use PACKAGE_NAME Updated internal CMake functions to require PACKAGE_NAME as a parameter instead of relying on PROJECT_NAME. This clarifies the preconditions for template and CI workflow generation, ensuring correct substitution and improving maintainability. --- cmake/cpp-library-ci.cmake | 5 +++-- cmake/cpp-library-install.cmake | 2 +- cmake/cpp-library-setup.cmake | 9 +++++---- cpp-library.cmake | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cmake/cpp-library-ci.cmake b/cmake/cpp-library-ci.cmake index 8dee755..13a7942 100644 --- a/cmake/cpp-library-ci.cmake +++ b/cmake/cpp-library-ci.cmake @@ -4,10 +4,11 @@ # # This module handles GitHub Actions workflow generation with PROJECT_NAME substitution -# Generates GitHub Actions CI workflow from template with PROJECT_NAME substitution. +# Generates GitHub Actions CI workflow from template with PACKAGE_NAME substitution. +# - Precondition: PACKAGE_NAME must be set in parent scope # - Postcondition: .github/workflows/ci.yml created from template if not present # - With force_init: overwrites existing workflow file -function(_cpp_library_setup_ci force_init) +function(_cpp_library_setup_ci PACKAGE_NAME force_init) set(ci_template "${CPP_LIBRARY_ROOT}/templates/.github/workflows/ci.yml.in") set(ci_dest "${CMAKE_CURRENT_SOURCE_DIR}/.github/workflows/ci.yml") diff --git a/cmake/cpp-library-install.cmake b/cmake/cpp-library-install.cmake index a2e20ac..b48147c 100644 --- a/cmake/cpp-library-install.cmake +++ b/cmake/cpp-library-install.cmake @@ -24,7 +24,7 @@ endfunction() # - Precondition: TARGET_NAME specifies existing target with INTERFACE_LINK_LIBRARIES # - Postcondition: OUTPUT_VAR contains newline-separated find_dependency() calls for public dependencies # - Uses cpp_library_map_dependency() mappings if registered, otherwise uses defaults -# - Automatically handles cpp-library dependencies (matching NAMESPACE) +# - Automatically handles cpp-library dependencies (namespace::package → find_dependency(package)) function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE) get_target_property(LINK_LIBS ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) diff --git a/cmake/cpp-library-setup.cmake b/cmake/cpp-library-setup.cmake index cd3c5e9..c8755b3 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -111,9 +111,10 @@ function(_cpp_library_setup_core) endfunction() # Copies template files (.clang-format, .gitignore, etc.) to project root if not present. -# - Postcondition: missing template files copied to project, CI workflow configured with PROJECT_NAME substitution +# - Precondition: PACKAGE_NAME must be passed as first parameter +# - Postcondition: missing template files copied to project, CI workflow configured with PACKAGE_NAME substitution # - With FORCE_INIT: overwrites existing files -function(_cpp_library_copy_templates) +function(_cpp_library_copy_templates PACKAGE_NAME) set(options FORCE_INIT) cmake_parse_arguments(ARG "${options}" "" "" ${ARGN}) @@ -141,6 +142,6 @@ function(_cpp_library_copy_templates) endif() endforeach() - # Setup CI workflow with PROJECT_NAME substitution - _cpp_library_setup_ci(${ARG_FORCE_INIT}) + # Setup CI workflow with PACKAGE_NAME substitution + _cpp_library_setup_ci("${PACKAGE_NAME}" ${ARG_FORCE_INIT}) endfunction() diff --git a/cpp-library.cmake b/cpp-library.cmake index 48ad743..cedfa16 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -211,9 +211,9 @@ function(cpp_library_setup) # Copy static template files (like .clang-format, .gitignore, CMakePresets.json, etc.) if(DEFINED CPP_LIBRARY_FORCE_INIT AND CPP_LIBRARY_FORCE_INIT) - _cpp_library_copy_templates(FORCE_INIT) + _cpp_library_copy_templates("${PACKAGE_NAME}" FORCE_INIT) else() - _cpp_library_copy_templates() + _cpp_library_copy_templates("${PACKAGE_NAME}") endif() # Setup testing (if tests are specified) From 0516b9be7607caaea6737b552b1acdb9c9c620bc Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 18 Nov 2025 10:12:37 -0800 Subject: [PATCH 19/23] Remove redundant install-test job from CI workflow The install-test job was removed from the GitHub Actions workflow because it duplicated functionality already provided by the main test job. This simplifies the workflow and reduces unnecessary runs across multiple operating systems. Also fixed a quoting issue with the install prefix on Windows. --- templates/.github/workflows/ci.yml.in | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/templates/.github/workflows/ci.yml.in b/templates/.github/workflows/ci.yml.in index 9418d0d..7b6e3ca 100644 --- a/templates/.github/workflows/ci.yml.in +++ b/templates/.github/workflows/ci.yml.in @@ -53,18 +53,6 @@ jobs: - name: Test run: ctest --preset=test - install-test: - name: Test Installation (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - needs: test - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - uses: actions/checkout@v5 - - name: Build and Install run: | cmake --preset=default @@ -89,7 +77,7 @@ jobs: EOF # Convert paths to forward slashes for CMake (works on all platforms) - INSTALL_PREFIX=$(echo "${{ runner.temp }}/install" | sed 's|\\|/|g') + INSTALL_PREFIX=$(echo '${{ runner.temp }}/install' | sed 's|\\|/|g') # Test find_package with CMAKE_PREFIX_PATH cmake -B build -S . -DCMAKE_PREFIX_PATH="${INSTALL_PREFIX}" From e4a3ee5dc2da1d05d44a7b74a142c94a53de8003 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 18 Nov 2025 11:37:54 -0800 Subject: [PATCH 20/23] Improve dependency mapping and version override support Updated dependency mapping logic to distinguish between internal cpp-library and external dependencies, ensuring package names are collision-safe and consistent. Added support for overriding the version using the CPP_LIBRARY_VERSION variable, which is useful for package managers and CI systems without git history. Improved documentation in README.md to clarify dependency handling, target naming patterns, and version management. Refactored CMake scripts to align with these changes and updated comments for clarity. --- README.md | 66 ++++++++++++++++++++++++++------- cmake/cpp-library-install.cmake | 20 +++++++--- cmake/cpp-library-setup.cmake | 17 +++++---- cpp-library.cmake | 18 +++++++-- 4 files changed, 89 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 54c250c..1a040ed 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,8 @@ When installed, the generated `my-libConfig.cmake` will include: include(CMakeFindDependencyMacro) # Find dependencies required by this package -find_dependency(copy-on-write) -find_dependency(enum-ops) +find_dependency(stlab-copy-on-write) +find_dependency(stlab-enum-ops) find_dependency(Threads) include("${CMAKE_CURRENT_LIST_DIR}/my-libTargets.cmake") @@ -158,8 +158,13 @@ include("${CMAKE_CURRENT_LIST_DIR}/my-libTargets.cmake") **Default dependency handling:** -- **cpp-library dependencies** (matching your project's `NAMESPACE`): Automatically mapped to their package names (e.g., `stlab::copy-on-write` → `find_dependency(copy-on-write)`) -- **Other packages**: Uses the package name only by default (e.g., `PackageName::Target` → `find_dependency(PackageName)`) +- **cpp-library dependencies** (matching your project's `NAMESPACE`): + - When namespace and component match: `namespace::namespace` → `find_dependency(namespace)` + - When they differ: `namespace::component` → `find_dependency(namespace-component)` + - Example: `stlab::copy-on-write` → `find_dependency(stlab-copy-on-write)` +- **Other packages**: Uses the package name only by default + - Example: `Threads::Threads` → `find_dependency(Threads)` + - Example: `Boost::filesystem` → `find_dependency(Boost)` **Custom dependency mappings:** @@ -222,6 +227,8 @@ git push origin v1.0.0 Tags should follow [semantic versioning](https://semver.org/) (e.g., `v1.0.0`, `v2.1.3`). +Alternatively, you can override the version using `-DCPP_LIBRARY_VERSION=x.y.z` (useful for package managers). See [Version Management](#version-management) for details. + #### GitHub Pages Deployment To enable automatic documentation deployment to GitHub Pages: @@ -257,30 +264,52 @@ cpp_library_setup( **Notes:** - The project name is automatically taken from `PROJECT_NAME` (set by the `project()` command). You must call `project(your-library)` before `cpp_library_setup()`. -- Version is automatically detected from git tags (see [Version Tagging](#version-tagging)). +- Version is automatically detected from git tags, or can be overridden with `-DCPP_LIBRARY_VERSION=x.y.z` (see [Version Management](#version-management)). - Examples using doctest should include `test` in the filename to be visible in the [C++ TestMate](https://marketplace.visualstudio.com/items?itemName=matepek.vscode-catch2-test-adapter) extension for VS Code test explorer. ### Target Naming -For `project(your-library)`, `cpp_library_setup` will create a target called `your-library`. - -The utility will additionally create an alias target based on the `NAMESPACE` option: `your_namespace::your-library`. +**Recommended Pattern** (collision-safe): -If your project name starts with the namespace followed by a dash, the namespace in the project name is stripped from the alias target: +Use the component name as your project name, and specify the organizational namespace separately: ```cmake -cmake_minimum_required(VERSION 3.20) -project(namespace-library) +project(enum-ops) # Component name only + +cpp_library_setup( + NAMESPACE stlab # Organizational namespace + # ... +) +``` + +This produces: + +- Target name: `enum-ops` +- Package name: `stlab-enum-ops` (used in `find_package(stlab-enum-ops)`) +- Target alias: `stlab::enum-ops` (used in `target_link_libraries()`) + +**Alternative Patterns:** -# ... CPM setup ... +You can also use `project(namespace-component)` - the namespace prefix will be detected and stripped from the target alias: + +```cmake +project(stlab-enum-ops) # Includes namespace prefix cpp_library_setup( - NAMESPACE namespace + NAMESPACE stlab # ... ) ``` -Results in `namespace-library` and `namespace::library` targets. +Produces the same result as above. + +**Single-component namespace** (e.g., `project(stlab)` with `NAMESPACE stlab`): + +- Target name: `stlab` +- Package name: `stlab` (used in `find_package(stlab)`) +- Target alias: `stlab::stlab` (used in `target_link_libraries()`) + +All package names include the namespace prefix for collision prevention. ### `cpp_library_map_dependency` @@ -380,6 +409,15 @@ Version is automatically detected from git tags: - Falls back to `0.0.0` if no tag is found (with warning) - Version used in CMake package config files +For package managers or CI systems building from source archives without git history, you can override the version using the `CPP_LIBRARY_VERSION` cache variable: + +```bash +cmake -DCPP_LIBRARY_VERSION=1.2.3 -B build +cmake --build build +``` + +This is particularly useful for vcpkg, Conan, or other package managers that don't have access to git tags. + ### Testing - **Test framework**: [doctest](https://github.com/doctest/doctest) diff --git a/cmake/cpp-library-install.cmake b/cmake/cpp-library-install.cmake index b48147c..ec6aca3 100644 --- a/cmake/cpp-library-install.cmake +++ b/cmake/cpp-library-install.cmake @@ -16,6 +16,7 @@ include(CMakePackageConfigHelpers) # - Precondition: TARGET is a namespaced target (e.g., "Qt5::Core", "Qt5::Widgets") # - Postcondition: FIND_DEPENDENCY_CALL stored for TARGET, used in package config generation # - Example: cpp_library_map_dependency("Qt5::Core" "Qt5 COMPONENTS Core") +# - Note: Most dependencies work automatically; only use for special syntax (COMPONENTS, etc.) function(cpp_library_map_dependency TARGET FIND_DEPENDENCY_CALL) set_property(GLOBAL PROPERTY _CPP_LIBRARY_DEPENDENCY_MAP_${TARGET} "${FIND_DEPENDENCY_CALL}") endfunction() @@ -24,7 +25,8 @@ endfunction() # - Precondition: TARGET_NAME specifies existing target with INTERFACE_LINK_LIBRARIES # - Postcondition: OUTPUT_VAR contains newline-separated find_dependency() calls for public dependencies # - Uses cpp_library_map_dependency() mappings if registered, otherwise uses defaults -# - Automatically handles cpp-library dependencies (namespace::package → find_dependency(package)) +# - cpp-library dependencies: namespace::namespace → find_dependency(namespace), namespace::component → find_dependency(namespace-component) +# - External dependencies: name::name → find_dependency(name), name::component → find_dependency(name) function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE) get_target_property(LINK_LIBS ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) @@ -53,11 +55,17 @@ function(_cpp_library_generate_dependencies OUTPUT_VAR TARGET_NAME NAMESPACE) # Use custom mapping (e.g., "Qt5 COMPONENTS Core" for Qt5::Core) list(APPEND DEPENDENCY_LIST "find_dependency(${CUSTOM_MAPPING})") elseif(PKG_NAME STREQUAL NAMESPACE) - # Internal cpp-library dependency: use component as package name - # (e.g., stlab::copy-on-write → find_dependency(copy-on-write)) - list(APPEND DEPENDENCY_LIST "find_dependency(${COMPONENT})") + # Internal cpp-library dependency + if(PKG_NAME STREQUAL COMPONENT) + # Namespace and component match: namespace::namespace → find_dependency(namespace) + list(APPEND DEPENDENCY_LIST "find_dependency(${PKG_NAME})") + else() + # Different names: namespace::component → find_dependency(namespace-component) + list(APPEND DEPENDENCY_LIST "find_dependency(${PKG_NAME}-${COMPONENT})") + endif() else() - # Default: use package name only (e.g., libdispatch::libdispatch → find_dependency(libdispatch)) + # External dependency: use package name only + # (e.g., Threads::Threads → find_dependency(Threads), Boost::filesystem → find_dependency(Boost)) list(APPEND DEPENDENCY_LIST "find_dependency(${PKG_NAME})") endif() endif() @@ -81,7 +89,7 @@ endfunction() function(_cpp_library_setup_install) set(oneValueArgs NAME # Target name (e.g., "stlab-enum-ops") - PACKAGE_NAME # Package name for find_package() (e.g., "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") ) diff --git a/cmake/cpp-library-setup.cmake b/cmake/cpp-library-setup.cmake index c8755b3..727d7ba 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -2,13 +2,13 @@ # # cpp-library-setup.cmake - Core library setup functionality -# Returns version string from PROJECT_VERSION (if set), git tag (with 'v' prefix removed), or +# Returns version string from CPP_LIBRARY_VERSION cache variable (if set), git tag (with 'v' prefix removed), or # "0.0.0" fallback function(_cpp_library_get_git_version OUTPUT_VAR) - # If PROJECT_VERSION is already set (e.g., by vcpkg or other package manager), + # If CPP_LIBRARY_VERSION is set (e.g., by vcpkg or other package manager via -DCPP_LIBRARY_VERSION=x.y.z), # use it instead of trying to query git (which may not be available in source archives) - if(DEFINED PROJECT_VERSION AND NOT PROJECT_VERSION STREQUAL "") - set(${OUTPUT_VAR} "${PROJECT_VERSION}" PARENT_SCOPE) + if(DEFINED CPP_LIBRARY_VERSION AND NOT CPP_LIBRARY_VERSION STREQUAL "") + set(${OUTPUT_VAR} "${CPP_LIBRARY_VERSION}" PARENT_SCOPE) return() endif() @@ -33,8 +33,8 @@ function(_cpp_library_get_git_version OUTPUT_VAR) endfunction() # Creates library target (INTERFACE or compiled) with headers and proper configuration. -# - Precondition: NAME, NAMESPACE, PACKAGE_NAME, and REQUIRES_CPP_VERSION specified -# - Postcondition: library target created with alias NAMESPACE::PACKAGE_NAME, install configured if TOP_LEVEL +# - Precondition: NAME, NAMESPACE, PACKAGE_NAME, CLEAN_NAME, and REQUIRES_CPP_VERSION specified +# - Postcondition: library target created with alias NAMESPACE::CLEAN_NAME, install configured if TOP_LEVEL function(_cpp_library_setup_core) set(oneValueArgs NAME @@ -42,6 +42,7 @@ function(_cpp_library_setup_core) DESCRIPTION NAMESPACE PACKAGE_NAME + CLEAN_NAME REQUIRES_CPP_VERSION TOP_LEVEL ) @@ -64,7 +65,7 @@ function(_cpp_library_setup_core) if(ARG_SOURCES) # Create a library with sources (respects BUILD_SHARED_LIBS variable) add_library(${ARG_NAME} ${ARG_SOURCES}) - add_library(${ARG_NAMESPACE}::${ARG_PACKAGE_NAME} ALIAS ${ARG_NAME}) + add_library(${ARG_NAMESPACE}::${ARG_CLEAN_NAME} ALIAS ${ARG_NAME}) target_include_directories(${ARG_NAME} PUBLIC $ $ @@ -81,7 +82,7 @@ function(_cpp_library_setup_core) else() # Header-only INTERFACE target add_library(${ARG_NAME} INTERFACE) - add_library(${ARG_NAMESPACE}::${ARG_PACKAGE_NAME} ALIAS ${ARG_NAME}) + add_library(${ARG_NAMESPACE}::${ARG_CLEAN_NAME} ALIAS ${ARG_NAME}) target_include_directories(${ARG_NAME} INTERFACE $ $ diff --git a/cpp-library.cmake b/cpp-library.cmake index cedfa16..e1d6fcb 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -34,8 +34,8 @@ function(_cpp_library_setup_executables) cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - # Extract the clean library name for linking - string(REPLACE "${ARG_NAMESPACE}-" "" CLEAN_NAME "${ARG_NAME}") + # Extract the clean library name for linking (strip namespace prefix if present) + string(REGEX REPLACE "^${ARG_NAMESPACE}-" "" CLEAN_NAME "${ARG_NAME}") # Download doctest dependency via CPM if(NOT TARGET doctest::doctest) @@ -140,8 +140,17 @@ function(cpp_library_setup) endif() set(ARG_NAME "${PROJECT_NAME}") - # Calculate PACKAGE_NAME (clean name without namespace prefix) for template substitution - string(REPLACE "${ARG_NAMESPACE}-" "" PACKAGE_NAME "${ARG_NAME}") + # Calculate clean name (without namespace prefix) for target alias + # If PROJECT_NAME starts with NAMESPACE-, strip it; otherwise use PROJECT_NAME as-is + string(REGEX REPLACE "^${ARG_NAMESPACE}-" "" CLEAN_NAME "${ARG_NAME}") + + # Always prefix package name with namespace for collision prevention + # Special case: if namespace equals clean name, don't duplicate (e.g., stlab::stlab → stlab) + if(ARG_NAMESPACE STREQUAL CLEAN_NAME) + set(PACKAGE_NAME "${ARG_NAMESPACE}") + else() + set(PACKAGE_NAME "${ARG_NAMESPACE}-${CLEAN_NAME}") + endif() # Set defaults if(NOT ARG_REQUIRES_CPP_VERSION) @@ -188,6 +197,7 @@ function(cpp_library_setup) DESCRIPTION "${ARG_DESCRIPTION}" NAMESPACE "${ARG_NAMESPACE}" PACKAGE_NAME "${PACKAGE_NAME}" + CLEAN_NAME "${CLEAN_NAME}" HEADERS "${GENERATED_HEADERS}" SOURCES "${GENERATED_SOURCES}" REQUIRES_CPP_VERSION "${ARG_REQUIRES_CPP_VERSION}" From 4f09b3b7f0abcc4ef1b39060ce03784920ce32e5 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Tue, 18 Nov 2025 11:54:24 -0800 Subject: [PATCH 21/23] Update CI to set compiler env vars conditionally Adds conditional steps in the CI workflow to set CC and CXX environment variables only when matrix.cc is defined. Ensures builds use the correct compiler configuration based on the matrix setup. --- templates/.github/workflows/ci.yml.in | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/templates/.github/workflows/ci.yml.in b/templates/.github/workflows/ci.yml.in index 7b6e3ca..96bb856 100644 --- a/templates/.github/workflows/ci.yml.in +++ b/templates/.github/workflows/ci.yml.in @@ -58,6 +58,17 @@ jobs: cmake --preset=default cmake --build --preset=default cmake --install build/default --prefix ${{ runner.temp }}/install + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + if: ${{ matrix.cc }} + + - name: Build and Install + run: | + cmake --preset=default + cmake --build --preset=default + cmake --install build/default --prefix ${{ runner.temp }}/install + if: ${{ !matrix.cc }} - name: Test find_package shell: bash From 579b1edf41e47ade0df0cf695ad618663c05d4c2 Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 20 Nov 2025 13:55:48 -0800 Subject: [PATCH 22/23] Update README for CPM repository naming compatibility Clarifies that GitHub repository names must match package names, including namespace prefixes, for CPM compatibility. Updates usage examples, instructions, and project links to reflect this requirement and prevent issues with CPM's local package finding and source fetching. Repository naming requirements may be backed out if [this CPM PR](https://github.com/cpm-cmake/CPM.cmake/pull/682) lands. --- README.md | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1a040ed..ad62b1c 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,10 @@ cmake_minimum_required(VERSION 3.20) # Project declaration - cpp_library_setup will use this name and detect version from git tags project(your-library) -# Only set CPM cache when building as top-level project -if(PROJECT_IS_TOP_LEVEL) - set(CPM_SOURCE_CACHE ${CMAKE_SOURCE_DIR}/.cache/cpm CACHE PATH "CPM cache") +# Setup CPM +if(PROJECT_IS_TOP_LEVEL AND NOT CPM_SOURCE_CACHE AND NOT DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE "${CMAKE_SOURCE_DIR}/.cache/cpm" CACHE PATH "CPM source cache") + message(STATUS "Setting cpm cache dir to: ${CPM_SOURCE_CACHE}") endif() include(cmake/CPM.cmake) @@ -99,14 +100,17 @@ project(my-app) include(cmake/CPM.cmake) # Fetch the library directly from GitHub -CPMAddPackage("gh:your-org/your-library@1.0.0") +# Note: Repository name must match the package name (including namespace prefix) +CPMAddPackage("gh:stlab/stlab-enum-ops@1.0.0") add_executable(my-app main.cpp) -target_link_libraries(my-app PRIVATE your_namespace::your-library) +target_link_libraries(my-app PRIVATE stlab::enum-ops) ``` The library will be automatically fetched and built as part of your project. +**Important:** For CPM compatibility, especially with `CPM_USE_LOCAL_PACKAGES`, your GitHub repository name should match the package name. For a library with package name `stlab-enum-ops`, name your repository `stlab/stlab-enum-ops`, not `stlab/enum-ops`. This ensures CPM's abbreviated syntax works correctly with both source fetching and local package finding. + #### Installation (optional) Installation is optional and typically not required when using CPM. If you need to install your library (e.g., for system-wide deployment or use with a package manager) use: @@ -216,6 +220,17 @@ This ensures your project uses the latest presets and CI configurations from the ### Setting Up GitHub Repository +#### Repository Naming + +**Critical:** Your GitHub repository name must match your package name for CPM compatibility. + +For the recommended pattern (`project(enum-ops)` with `NAMESPACE stlab`): + +- Package name will be: `stlab-enum-ops` +- Repository should be: `github.com/stlab/stlab-enum-ops` + +This ensures `CPMAddPackage("gh:stlab/stlab-enum-ops@1.0.0")` works correctly with both source builds and `CPM_USE_LOCAL_PACKAGES`. + #### Version Tagging cpp-library automatically detects your library version from git tags. To version your library: @@ -287,6 +302,7 @@ This produces: - Target name: `enum-ops` - Package name: `stlab-enum-ops` (used in `find_package(stlab-enum-ops)`) - Target alias: `stlab::enum-ops` (used in `target_link_libraries()`) +- GitHub repository should be named: `stlab/stlab-enum-ops` (for CPM compatibility) **Alternative Patterns:** @@ -441,8 +457,10 @@ These files are generated automatically. To regenerate with the latest templates See these projects using cpp-library: -- [stlab/enum-ops](https://github.com/stlab/enum-ops) - Type-safe operators for enums -- [stlab/copy-on-write](https://github.com/stlab/copy-on-write) - Copy-on-write wrapper +- [stlab/stlab-enum-ops](https://github.com/stlab/stlab-enum-ops) - Type-safe operators for enums +- [stlab/stlab-copy-on-write](https://github.com/stlab/stlab-copy-on-write) - Copy-on-write wrapper + +Note: Repository names include the namespace prefix for CPM compatibility and collision prevention. ## License From f13bae3e3c5bcc43b8a9e3e841ed2f605acef59d Mon Sep 17 00:00:00 2001 From: Sean Parent Date: Thu, 20 Nov 2025 16:16:27 -0800 Subject: [PATCH 23/23] Add interactive setup script and update README Introduces setup.cmake, an interactive script for initializing new C++ library projects using the cpp-library template. The README is updated with detailed quick start instructions for using the script in both interactive and non-interactive modes, as well as guidance for manual setup. --- README.md | 71 +++++++++- setup.cmake | 374 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 setup.cmake diff --git a/README.md b/README.md index ad62b1c..7e716f9 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,76 @@ Modern CMake template for C++ libraries with comprehensive infrastructure. - **CI/CD**: [GitHub Actions](https://docs.github.com/en/actions) workflows with multi-platform testing and installation verification - **Dependency Management**: [CPM.cmake](https://github.com/cpm-cmake/CPM.cmake) integration -## Usage +## Quick Start + +The easiest way to create a new library project using cpp-library is with the `setup.cmake` script. This interactive script will guide you through creating a new project with the correct structure, downloading dependencies, and generating all necessary files. + +### Using setup.cmake + +**Interactive mode:** + +```bash +cmake -P <(curl -sSL https://raw.githubusercontent.com/stlab/cpp-library/main/setup.cmake) +``` + +Or download and run: + +```bash +curl -O https://raw.githubusercontent.com/stlab/cpp-library/main/setup.cmake +cmake -P setup.cmake +``` + +The script will prompt you for: + +- **Library name** (e.g., `my-library`) +- **Namespace** (e.g., `mycompany`) +- **Description** +- **Header-only library?** (yes/no) +- **Include examples?** (yes/no) +- **Include tests?** (yes/no) + +**Non-interactive mode:** + +```bash +cmake -P setup.cmake -- \ + --name=my-library \ + --namespace=mycompany \ + --description="My awesome library" \ + --header-only=yes \ + --examples=yes \ + --tests=yes +``` + +The script will: + +1. Create the project directory structure +2. Download CPM.cmake +3. Generate CMakeLists.txt with correct configuration +4. Create template header files +5. Create example and test files (if requested) +6. Initialize a git repository + +After setup completes: + +```bash +cd my-library +cmake --preset=test +cmake --build --preset=test +ctest --preset=test +``` + +To regenerate template files (CMakePresets.json, CI workflows): + +```bash +cmake --preset=init +cmake --build --preset=init +``` + +## Manual Setup + +If you prefer to set up your project manually, or need to integrate cpp-library into an existing project, follow these steps. + +### Usage Use `CPMAddPackage` to fetch cpp-library directly in your `CMakeLists.txt`: diff --git a/setup.cmake b/setup.cmake new file mode 100644 index 0000000..8859edb --- /dev/null +++ b/setup.cmake @@ -0,0 +1,374 @@ +#!/usr/bin/env -S cmake -P +# SPDX-License-Identifier: BSL-1.0 +# +# setup.cmake - Interactive project setup script for cpp-library +# +# Usage: +# cmake -P setup.cmake +# cmake -P setup.cmake -- --name=my-lib --namespace=myns --description="My library" + +cmake_minimum_required(VERSION 3.20) + +# Parse command line arguments +set(CMD_LINE_ARGS "") +if(CMAKE_ARGV3) + # Arguments after -- are available starting from CMAKE_ARGV3 + math(EXPR ARGC "${CMAKE_ARGC} - 3") + foreach(i RANGE ${ARGC}) + math(EXPR idx "${i} + 3") + if(CMAKE_ARGV${idx}) + list(APPEND CMD_LINE_ARGS "${CMAKE_ARGV${idx}}") + endif() + endforeach() +endif() + +# Parse named arguments +set(ARG_NAME "") +set(ARG_NAMESPACE "") +set(ARG_DESCRIPTION "") +set(ARG_HEADER_ONLY "") +set(ARG_EXAMPLES "") +set(ARG_TESTS "") + +foreach(arg IN LISTS CMD_LINE_ARGS) + if(arg MATCHES "^--name=(.+)$") + set(ARG_NAME "${CMAKE_MATCH_1}") + elseif(arg MATCHES "^--namespace=(.+)$") + set(ARG_NAMESPACE "${CMAKE_MATCH_1}") + elseif(arg MATCHES "^--description=(.+)$") + set(ARG_DESCRIPTION "${CMAKE_MATCH_1}") + elseif(arg MATCHES "^--header-only=(yes|no|true|false|1|0)$") + string(TOLOWER "${CMAKE_MATCH_1}" val) + if(val MATCHES "^(yes|true|1)$") + set(ARG_HEADER_ONLY YES) + else() + set(ARG_HEADER_ONLY NO) + endif() + elseif(arg MATCHES "^--examples=(yes|no|true|false|1|0)$") + string(TOLOWER "${CMAKE_MATCH_1}" val) + if(val MATCHES "^(yes|true|1)$") + set(ARG_EXAMPLES YES) + else() + set(ARG_EXAMPLES NO) + endif() + elseif(arg MATCHES "^--tests=(yes|no|true|false|1|0)$") + string(TOLOWER "${CMAKE_MATCH_1}" val) + if(val MATCHES "^(yes|true|1)$") + set(ARG_TESTS YES) + else() + set(ARG_TESTS NO) + endif() + elseif(arg MATCHES "^--help$") + message([[ +Usage: cmake -P setup.cmake [OPTIONS] + +Interactive setup script for cpp-library projects. + +OPTIONS: + --name=NAME Library name (e.g., my-library) + --namespace=NAMESPACE Namespace (e.g., mycompany) + --description=DESC Brief description + --header-only=yes|no Header-only library (default: yes) + --examples=yes|no Include examples (default: yes) + --tests=yes|no Include tests (default: yes) + --help Show this help message + +If options are not provided, the script will prompt interactively. + +Examples: + cmake -P setup.cmake + cmake -P setup.cmake -- --name=my-lib --namespace=myns --description="My library" +]]) + return() + endif() +endforeach() + +# Helper function to prompt user for input +function(prompt_user PROMPT_TEXT OUTPUT_VAR DEFAULT_VALUE) + if(CMAKE_HOST_WIN32) + # Windows: Use PowerShell for input + execute_process( + COMMAND powershell -NoProfile -Command "Write-Host -NoNewline '${PROMPT_TEXT}'; Read-Host" + OUTPUT_VARIABLE USER_INPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + else() + # Unix: Use bash/sh for input + execute_process( + COMMAND bash -c "read -p '${PROMPT_TEXT}' input && echo -n \"$input\"" + OUTPUT_VARIABLE USER_INPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + endif() + + if(USER_INPUT STREQUAL "" AND NOT DEFAULT_VALUE STREQUAL "") + set(${OUTPUT_VAR} "${DEFAULT_VALUE}" PARENT_SCOPE) + else() + set(${OUTPUT_VAR} "${USER_INPUT}" PARENT_SCOPE) + endif() +endfunction() + +# Helper function to prompt for yes/no +function(prompt_yes_no PROMPT_TEXT OUTPUT_VAR DEFAULT_VALUE) + if(DEFAULT_VALUE) + set(prompt_suffix " [Y/n]: ") + set(default_result YES) + else() + set(prompt_suffix " [y/N]: ") + set(default_result NO) + endif() + + prompt_user("${PROMPT_TEXT}${prompt_suffix}" USER_INPUT "") + + string(TOLOWER "${USER_INPUT}" USER_INPUT_LOWER) + if(USER_INPUT_LOWER STREQUAL "y" OR USER_INPUT_LOWER STREQUAL "yes") + set(${OUTPUT_VAR} YES PARENT_SCOPE) + elseif(USER_INPUT_LOWER STREQUAL "n" OR USER_INPUT_LOWER STREQUAL "no") + set(${OUTPUT_VAR} NO PARENT_SCOPE) + elseif(USER_INPUT STREQUAL "") + set(${OUTPUT_VAR} ${default_result} PARENT_SCOPE) + else() + set(${OUTPUT_VAR} ${default_result} PARENT_SCOPE) + endif() +endfunction() + +message("=== cpp-library Project Setup ===\n") + +# Collect information interactively if not provided +if(ARG_NAME STREQUAL "") + prompt_user("Library name (e.g., my-library): " ARG_NAME "") + if(ARG_NAME STREQUAL "") + message(FATAL_ERROR "Library name is required") + endif() +endif() + +if(ARG_NAMESPACE STREQUAL "") + prompt_user("Namespace (e.g., mycompany): " ARG_NAMESPACE "") + if(ARG_NAMESPACE STREQUAL "") + message(FATAL_ERROR "Namespace is required") + endif() +endif() + +if(ARG_DESCRIPTION STREQUAL "") + prompt_user("Description: " ARG_DESCRIPTION "A C++ library") +endif() + +if(ARG_HEADER_ONLY STREQUAL "") + prompt_yes_no("Header-only library?" ARG_HEADER_ONLY YES) +endif() + +if(ARG_EXAMPLES STREQUAL "") + prompt_yes_no("Include examples?" ARG_EXAMPLES YES) +endif() + +if(ARG_TESTS STREQUAL "") + prompt_yes_no("Include tests?" ARG_TESTS YES) +endif() + +# Display summary +message("\n=== Configuration Summary ===") +message("Library name: ${ARG_NAME}") +message("Namespace: ${ARG_NAMESPACE}") +message("Description: ${ARG_DESCRIPTION}") +message("Header-only: ${ARG_HEADER_ONLY}") +message("Include examples: ${ARG_EXAMPLES}") +message("Include tests: ${ARG_TESTS}") +message("") + +# Create project directory +set(PROJECT_DIR "${CMAKE_CURRENT_LIST_DIR}/${ARG_NAME}") +if(EXISTS "${PROJECT_DIR}") + message(FATAL_ERROR "Directory '${ARG_NAME}' already exists!") +endif() + +message("Creating project structure in: ${ARG_NAME}/") +file(MAKE_DIRECTORY "${PROJECT_DIR}") + +# Create directory structure +file(MAKE_DIRECTORY "${PROJECT_DIR}/include/${ARG_NAMESPACE}") +file(MAKE_DIRECTORY "${PROJECT_DIR}/cmake") + +if(NOT ARG_HEADER_ONLY) + file(MAKE_DIRECTORY "${PROJECT_DIR}/src") +endif() + +if(ARG_EXAMPLES) + file(MAKE_DIRECTORY "${PROJECT_DIR}/examples") +endif() + +if(ARG_TESTS) + file(MAKE_DIRECTORY "${PROJECT_DIR}/tests") +endif() + +# Download CPM.cmake +message("Downloading CPM.cmake...") +file(DOWNLOAD + "https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake" + "${PROJECT_DIR}/cmake/CPM.cmake" + STATUS DOWNLOAD_STATUS + TIMEOUT 30 +) + +list(GET DOWNLOAD_STATUS 0 STATUS_CODE) +if(NOT STATUS_CODE EQUAL 0) + list(GET DOWNLOAD_STATUS 1 ERROR_MESSAGE) + message(WARNING "Failed to download CPM.cmake: ${ERROR_MESSAGE}") + message(WARNING "You'll need to download it manually from https://github.com/cpm-cmake/CPM.cmake") +endif() + +# Create main header file +set(HEADER_FILE "${ARG_NAME}.hpp") +file(WRITE "${PROJECT_DIR}/include/${ARG_NAMESPACE}/${HEADER_FILE}" +"// SPDX-License-Identifier: BSL-1.0 + +#ifndef ${ARG_NAMESPACE}_${ARG_NAME}_HPP +#define ${ARG_NAMESPACE}_${ARG_NAME}_HPP + +namespace ${ARG_NAMESPACE} { + +// Your library code here + +} // namespace ${ARG_NAMESPACE} + +#endif // ${ARG_NAMESPACE}_${ARG_NAME}_HPP +") + +# Create source file if not header-only +set(SOURCE_FILES "") +if(NOT ARG_HEADER_ONLY) + set(SOURCE_FILE "${ARG_NAME}.cpp") + set(SOURCE_FILES "SOURCES ${SOURCE_FILE}") + file(WRITE "${PROJECT_DIR}/src/${SOURCE_FILE}" +"// SPDX-License-Identifier: BSL-1.0 + +#include <${ARG_NAMESPACE}/${HEADER_FILE}> + +namespace ${ARG_NAMESPACE} { + +// Implementation here + +} // namespace ${ARG_NAMESPACE} +") +endif() + +# Create example file +set(EXAMPLE_FILES "") +if(ARG_EXAMPLES) + set(EXAMPLE_FILES "EXAMPLES example.cpp") + file(WRITE "${PROJECT_DIR}/examples/example.cpp" +"// SPDX-License-Identifier: BSL-1.0 + +#include <${ARG_NAMESPACE}/${HEADER_FILE}> + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +TEST_CASE(\"example test\") { + // Your example code here + CHECK(true); +} +") +endif() + +# Create test file +set(TEST_FILES "") +if(ARG_TESTS) + set(TEST_FILES "TESTS tests.cpp") + file(WRITE "${PROJECT_DIR}/tests/tests.cpp" +"// SPDX-License-Identifier: BSL-1.0 + +#include <${ARG_NAMESPACE}/${HEADER_FILE}> + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +TEST_CASE(\"basic test\") { + // Your tests here + CHECK(true); +} +") +endif() + +# Generate CMakeLists.txt +file(WRITE "${PROJECT_DIR}/CMakeLists.txt" +"cmake_minimum_required(VERSION 3.20) + +# Project declaration - cpp_library_setup will use this name and detect version from git tags +project(${ARG_NAME}) + +# Setup CPM +if(PROJECT_IS_TOP_LEVEL AND NOT CPM_SOURCE_CACHE AND NOT DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE \"\${CMAKE_SOURCE_DIR}/.cache/cpm\" CACHE PATH \"CPM source cache\") + message(STATUS \"Setting cpm cache dir to: \${CPM_SOURCE_CACHE}\") +endif() +include(cmake/CPM.cmake) + +# Fetch cpp-library via CPM +CPMAddPackage(\"gh:stlab/cpp-library@4.0.3\") +include(\${cpp-library_SOURCE_DIR}/cpp-library.cmake) + +cpp_library_setup( + DESCRIPTION \"${ARG_DESCRIPTION}\" + NAMESPACE ${ARG_NAMESPACE} + HEADERS ${HEADER_FILE} + ${SOURCE_FILES} + ${EXAMPLE_FILES} + ${TEST_FILES} +) +") + +# Create .gitignore +file(WRITE "${PROJECT_DIR}/.gitignore" +"build/ +.cache/ +compile_commands.json +.DS_Store +*.swp +*.swo +*~ +") + +# Initialize git repository +message("\nInitializing git repository...") +execute_process( + COMMAND git init + WORKING_DIRECTORY "${PROJECT_DIR}" + OUTPUT_QUIET + ERROR_QUIET +) + +execute_process( + COMMAND git add . + WORKING_DIRECTORY "${PROJECT_DIR}" + OUTPUT_QUIET + ERROR_QUIET +) + +execute_process( + COMMAND git commit -m "Initial commit" + WORKING_DIRECTORY "${PROJECT_DIR}" + OUTPUT_QUIET + ERROR_QUIET + RESULT_VARIABLE GIT_COMMIT_RESULT +) + +if(GIT_COMMIT_RESULT EQUAL 0) + message("✓ Git repository initialized with initial commit") +else() + message("✓ Git repository initialized (commit manually)") +endif() + +# Success message +message("\n=== Setup Complete! ===\n") +message("Your library has been created in: ${ARG_NAME}/") +message("\nNext steps:") +message(" cd ${ARG_NAME}") +message(" cmake --preset=test") +message(" cmake --build --preset=test") +message(" ctest --preset=test") +message("\nTo regenerate template files (CMakePresets.json, CI workflows):") +message(" cmake --preset=init") +message(" cmake --build --preset=init") +message("\nFor more information, visit: https://github.com/stlab/cpp-library")