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..7e716f9 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,86 @@ 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 +## 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: +Use `CPMAddPackage` to fetch cpp-library directly in your `CMakeLists.txt`: ```cmake cmake_minimum_required(VERSION 3.20) @@ -28,9 +99,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) -# Setup cpp-library infrastructure -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) @@ -50,11 +122,206 @@ cpp_library_setup( ) ``` -### Prerequisites +### Getting Started + +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 +# 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 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: + +```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). + +#### 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(stlab-copy-on-write) +find_dependency(stlab-enum-ops) +find_dependency(Threads) + +include("${CMAKE_CURRENT_LIST_DIR}/my-libTargets.cmake") +``` + +**Default dependency handling:** + +- **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:** + +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 + +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 + +#### 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: + +```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`). + +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: -- **CPM.cmake**: Must be included before using cpp-library -- **CMake 3.20+**: Required for modern CMake features -- **C++17+**: Default requirement (configurable) +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 +345,94 @@ 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. +**Notes:** -**NOTE**: Examples using doctest should have `test` in the name if you want them to be visible in -the TestMate test explorer. +- 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, 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. -### Template Regeneration +### Target Naming -To force regeneration of template files (CMakePresets.json, CI workflows, etc.), you can use the `init` preset: +**Recommended Pattern** (collision-safe): -```bash -cmake --preset=init -cmake --build --preset=init +Use the component name as your project name, and specify the organizational namespace separately: + +```cmake +project(enum-ops) # Component name only + +cpp_library_setup( + NAMESPACE stlab # Organizational namespace + # ... +) ``` -Alternatively, you can set the CMake variable `CPP_LIBRARY_FORCE_INIT` to `ON`: +This produces: -```bash -cmake -DCPP_LIBRARY_FORCE_INIT=ON -B build/init +- 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:** + +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 stlab + # ... +) +``` + +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` + +```cmake +cpp_library_map_dependency(target find_dependency_call) ``` -This will regenerate all template files, overwriting any existing ones. +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 @@ -115,8 +447,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,158 +471,65 @@ 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 - -- **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 - -### Smart Defaults - -- **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 - -### Testing Features - -- **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) - -### Development Tools - -- **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.) +Libraries with sources build as static libraries by default. Set `BUILD_SHARED_LIBS=ON` to build shared libraries instead. -### CI/CD Features +## Reference -- **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 +### CMake Presets -### Dependency Management +cpp-library generates a `CMakePresets.json` file with the following configurations: -- **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 +- **`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.) ### Version Management -- **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 +Version is automatically detected from git tags: -## Example Projects - -This template is used by: - -- [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) +- 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 -# 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) +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: -# 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" -) +```bash +cmake -DCPP_LIBRARY_VERSION=1.2.3 -B build +cmake --build build ``` -## Quick Start - -1. **Initialize a new project**: +This is particularly useful for vcpkg, Conan, or other package managers that don't have access to git tags. - ```bash - # Clone or create your project - mkdir my-library && cd my-library +### Testing - # Create basic structure - mkdir -p include/your_namespace src examples tests cmake +- **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 - # 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`) +## Template Files Generated -5. **Add tests** to `tests/` (use `_fail` suffix for compile-fail tests, e.g., `tests.cpp`, `tests_fail.cpp`) +cpp-library automatically generates infrastructure files on first configuration and when using the `init` preset: -6. **Build and test**: +- **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) - ```bash - cmake --preset=test - cmake --build --preset=test - ctest --preset=test - ``` +These files are generated automatically. To regenerate with the latest templates, use `cmake --preset=init`. -7. **Regenerate templates** (if needed): - ```bash - cmake --preset=init - cmake --build --preset=init - ``` +## Example Projects -## Template Files Generated +See these projects using cpp-library: -The template automatically generates: +- [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 -- **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 +Note: Repository names include the namespace prefix for CPM compatibility and collision prevention. ## License diff --git a/cmake/cpp-library-ci.cmake b/cmake/cpp-library-ci.cmake new file mode 100644 index 0000000..13a7942 --- /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 + +# 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 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") + + 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) + 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-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 new file mode 100644 index 0000000..ec6aca3 --- /dev/null +++ b/cmake/cpp-library-install.cmake @@ -0,0 +1,173 @@ +# 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) + +# 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") +# - 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() + +# 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 +# - Uses cpp_library_map_dependency() mappings if registered, otherwise uses defaults +# - 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) + + 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}") + + # 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 + 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() + # 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() + 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 +# - 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") + PACKAGE_NAME # Package name for find_package() (e.g., "stlab-enum-ops") + VERSION # Version string (e.g., "1.2.3") + NAMESPACE # Namespace for alias (e.g., "stlab") + ) + set(multiValueArgs + HEADERS # List of header file paths (for FILE_SET support check) + ) + + cmake_parse_arguments(ARG "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + # Validate required arguments + if(NOT ARG_NAME) + message(FATAL_ERROR "_cpp_library_setup_install: NAME is required") + endif() + if(NOT ARG_PACKAGE_NAME) + message(FATAL_ERROR "_cpp_library_setup_install: PACKAGE_NAME is required") + endif() + if(NOT ARG_VERSION) + message(FATAL_ERROR "_cpp_library_setup_install: VERSION is required") + endif() + if(NOT ARG_NAMESPACE) + message(FATAL_ERROR "_cpp_library_setup_install: NAMESPACE is required") + endif() + + # 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 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( + "${CMAKE_CURRENT_BINARY_DIR}/${ARG_PACKAGE_NAME}ConfigVersion.cmake" + VERSION ${ARG_VERSION} + COMPATIBILITY SameMajorVersion + ) + + # 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" + @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_PACKAGE_NAME}Targets.cmake + NAMESPACE ${ARG_NAMESPACE}:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${ARG_PACKAGE_NAME} + ) + + # Install package config and version files + install(FILES + "${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 131bf23..727d7ba 100644 --- a/cmake/cpp-library-setup.cmake +++ b/cmake/cpp-library-setup.cmake @@ -2,8 +2,16 @@ # # cpp-library-setup.cmake - Core library setup functionality -# Function to get version from git tags +# 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 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 CPP_LIBRARY_VERSION AND NOT CPP_LIBRARY_VERSION STREQUAL "") + set(${OUTPUT_VAR} "${CPP_LIBRARY_VERSION}" PARENT_SCOPE) + return() + endif() + # Try to get version from git tags execute_process( COMMAND git describe --tags --abbrev=0 @@ -24,12 +32,17 @@ function(_cpp_library_get_git_version OUTPUT_VAR) endif() endfunction() +# Creates library target (INTERFACE or compiled) with headers and proper configuration. +# - Precondition: NAME, NAMESPACE, PACKAGE_NAME, CLEAN_NAME, and REQUIRES_CPP_VERSION specified +# - Postcondition: library target created with alias NAMESPACE::CLEAN_NAME, install configured if TOP_LEVEL function(_cpp_library_setup_core) set(oneValueArgs NAME VERSION DESCRIPTION NAMESPACE + PACKAGE_NAME + CLEAN_NAME REQUIRES_CPP_VERSION TOP_LEVEL ) @@ -49,15 +62,13 @@ 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 regular library if sources are present - add_library(${ARG_NAME} STATIC ${ARG_SOURCES}) - add_library(${ARG_NAMESPACE}::${CLEAN_NAME} ALIAS ${ARG_NAME}) + # Create a library with sources (respects BUILD_SHARED_LIBS variable) + add_library(${ARG_NAME} ${ARG_SOURCES}) + add_library(${ARG_NAMESPACE}::${ARG_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) @@ -71,9 +82,10 @@ 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_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,11 +97,25 @@ 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}" + PACKAGE_NAME "${ARG_PACKAGE_NAME}" + VERSION "${ARG_VERSION}" + NAMESPACE "${ARG_NAMESPACE}" + HEADERS "${ARG_HEADERS}" + ) + endif() endfunction() -# Function to copy static template files -function(_cpp_library_copy_templates) +# Copies template files (.clang-format, .gitignore, etc.) to project root if not present. +# - 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 PACKAGE_NAME) set(options FORCE_INIT) cmake_parse_arguments(ARG "${options}" "" "" ${ARGN}) @@ -101,27 +127,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 PACKAGE_NAME substitution + _cpp_library_setup_ci("${PACKAGE_NAME}" ${ARG_FORCE_INIT}) endfunction() 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 4014741..e1d6fcb 100644 --- a/cpp-library.cmake +++ b/cpp-library.cmake @@ -15,8 +15,13 @@ 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") +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 @@ -29,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) @@ -97,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 @@ -132,6 +140,18 @@ function(cpp_library_setup) endif() set(ARG_NAME "${PROJECT_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) set(ARG_REQUIRES_CPP_VERSION 17) @@ -176,6 +196,8 @@ function(cpp_library_setup) VERSION "${ARG_VERSION}" 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}" @@ -199,9 +221,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) 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") diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml deleted file mode 100644 index d08a155..0000000 --- a/templates/.github/workflows/ci.yml +++ /dev/null @@ -1,91 +0,0 @@ -# Auto-generated from cpp-library (https://github.com/stlab/cpp-library) -# Do not edit this file directly - it will be overwritten when templates are regenerated - -name: CI - -on: - push: - branches: [main, develop] - pull_request: - branches: [main] - release: - types: [published] - -jobs: - test: - strategy: - 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 - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v5 - - - name: Configure CMake - run: cmake --preset=test - - - name: Build - run: cmake --build --preset=test - - - name: Test - run: ctest --preset=test - - clang-tidy: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v5 - - - name: Configure CMake with clang-tidy - run: cmake --preset=clang-tidy - - - name: Build with clang-tidy - run: cmake --build --preset=clang-tidy - - - name: Run tests with clang-tidy - run: ctest --preset=clang-tidy - - docs: - runs-on: ubuntu-latest - if: github.event_name == 'release' && github.event.action == 'published' - permissions: - id-token: write - pages: write - contents: read - - steps: - - uses: actions/checkout@v5 - - - name: Install Doxygen - uses: ssciwr/doxygen-install@v1 - - - name: Configure CMake - run: cmake --preset=docs - - - name: Build Documentation - run: cmake --build --preset=docs - - - name: Setup Pages - uses: actions/configure-pages@v5 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v4 - with: - path: build/docs/html - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/templates/.github/workflows/ci.yml.in b/templates/.github/workflows/ci.yml.in new file mode 100644 index 0000000..96bb856 --- /dev/null +++ b/templates/.github/workflows/ci.yml.in @@ -0,0 +1,141 @@ +# Auto-generated from cpp-library (https://github.com/stlab/cpp-library) +# Do not edit this file directly - it will be overwritten when templates are regenerated + +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + release: + types: [published] + +jobs: + test: + name: Test (${{ matrix.name }}) + strategy: + fail-fast: false + matrix: + 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 }} + + steps: + - uses: actions/checkout@v5 + + - name: Configure CMake + run: cmake --preset=test + 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 + + - name: Test + run: ctest --preset=test + + - name: Build and Install + run: | + 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 + 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 + + # Create test CMakeLists.txt + cat > CMakeLists.txt << EOF + cmake_minimum_required(VERSION 3.20) + project(test-find-package CXX) + + find_package(@PACKAGE_NAME@ REQUIRED) + + message(STATUS "Successfully found @PACKAGE_NAME@") + EOF + + # 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 + + steps: + - uses: actions/checkout@v5 + + - name: Configure CMake with clang-tidy + run: cmake --preset=clang-tidy + + - name: Build with clang-tidy + run: cmake --build --preset=clang-tidy + + - name: Run tests with clang-tidy + run: ctest --preset=clang-tidy + + docs: + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + permissions: + id-token: write + pages: write + contents: read + + steps: + - uses: actions/checkout@v5 + + - name: Install Doxygen + uses: ssciwr/doxygen-install@v1 + + - name: Configure CMake + run: cmake --preset=docs + + - name: Build Documentation + run: cmake --build --preset=docs + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: build/docs/html + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 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 } }, 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")