From 2efd1ebaa682823d6416642f39777df60bc6ed7f Mon Sep 17 00:00:00 2001 From: Anthony West Date: Sat, 10 Oct 2026 15:59:11 -0700 Subject: [PATCH] Add CMake install and package config --- .github/workflows/ci.yml | 36 +++++++++++++++++ AGENTS.md | 5 ++- CHANGELOG.md | 10 +++++ CMakeLists.txt | 69 ++++++++++++++++++++++++++++++-- Deploy.bat | 3 ++ README.md | 21 ++++++++++ cmake/ASWLogConfig.cmake.in | 14 +++++++ tests/package/CMakeLists.txt | 31 +++++++++++++++ tests/package/Check_Package.cpp | 70 +++++++++++++++++++++++++++++++++ 9 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 cmake/ASWLogConfig.cmake.in create mode 100644 tests/package/CMakeLists.txt create mode 100644 tests/package/Check_Package.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f20a36..44a97dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,9 @@ jobs: exe_path: build/bin/Release/ASWLogTests.exe # cl takes -MP as well as /MP; Git Bash would turn "/MP" into a path when it starts CMake. cxxflags: -MP + # A multi-config generator, so the package check builds and installs both into the same prefix (the Debug + # library is ASWLogd) + package_configs: Debug Release sanitize: "" - name: Windows (MinGW) os: windows-latest @@ -166,3 +169,36 @@ jobs: name: test-results-${{ matrix.name }} path: junit-results.xml if-no-files-found: error + + - name: Check the installed CMake package + # Builds and installs the library (the root project) to a temporary prefix, then builds tests/package against + # it with find_package() and runs it, which checks the install rules and the package files. Not in the + # sanitizer jobs: they build the same CMake files, and the package would pass their flags on to the consumer. + if: ${{ !matrix.sanitize }} + shell: bash + run: | + if [ -n "${{ matrix.cc }}" ]; then + export CC="${{ matrix.cc }}" + export CXX="${{ matrix.cxx }}" + fi + if [ -n "${{ matrix.cxxflags }}" ]; then + export CXXFLAGS="${{ matrix.cxxflags }}" + fi + generatorArgs=() + if [ -n "${{ matrix.generator }}" ]; then + generatorArgs=(-G "${{ matrix.generator }}") + fi + # Forward slashes, which CMake takes on Windows too (RUNNER_TEMP has backslashes there) + prefix="${RUNNER_TEMP//\\//}/aswlog" + configs="${{ matrix.package_configs || 'Release' }}" + cmake -S . -B build-install "${generatorArgs[@]}" -DCMAKE_BUILD_TYPE=Release + for config in $configs; do + cmake --build build-install --config "$config" + cmake --install build-install --config "$config" --prefix "$prefix" + done + cmake -S tests/package -B build-package "${generatorArgs[@]}" -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$prefix" + for config in $configs; do + cmake --build build-package --config "$config" + ctest --test-dir build-package -C "$config" --output-on-failure + done diff --git a/AGENTS.md b/AGENTS.md index 09d22b0..6d21c27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,9 +9,10 @@ These instructions apply to the entire repository unless a more specific `AGENTS ASWLog is a thread-safe light-weight C++ logging tool for Windows and Linux projects. - `ASWLog/` contains the framework implementation. -- `tests/` contains the unit tests: test modules (`Test_ASWLog_`, the tests of `ASWLog_`) and shared test helpers (`UT_Helper_`, e.g. `UT_Helper_DateTime`) in `tests/tests/`, their CMake project in `tests/cmake/`, their RAD Studio console project in `tests/rad370/`, and their RAD Studio VCL GUI runner project in `tests/vcl/gui/rad370/` (built from the submodule's `vcl/gui/src`). `tests/compile_checks/` holds "must not compile" checks, which `tests/cmake/CMakeLists.txt` runs with `try_compile` when configuring (a failed check stops the configure). +- `tests/` contains the unit tests: test modules (`Test_ASWLog_`, the tests of `ASWLog_`) and shared test helpers (`UT_Helper_`, e.g. `UT_Helper_DateTime`) in `tests/tests/`, their CMake project in `tests/cmake/`, their RAD Studio console project in `tests/rad370/`, and their RAD Studio VCL GUI runner project in `tests/vcl/gui/rad370/` (built from the submodule's `vcl/gui/src`). `tests/compile_checks/` holds "must not compile" checks, which `tests/cmake/CMakeLists.txt` runs with `try_compile` when configuring (a failed check stops the configure). `tests/package/` is a project that builds against an installed ASWLog with `find_package` (CI installs the library and runs it). +- `CMakeLists.txt` (the root) defines the `ASWLog::ASWLog` library target and its install rules; `cmake/ASWLogConfig.cmake.in` is the template of the installed package file. The project version is read from `ASWLog/ASWLog_Version.h`. - `third_party/asw-unit-tests/` is the ASWUnitTests framework, a git submodule pinned to a release tag. Never edit files in it; test modules self-register with `ASW_REGISTER_TEST_GROUP`, so adding a test only touches `tests/` and its build files. -- `.github/workflows/ci.yml` builds and runs the unit tests on Windows (MSVC, MinGW) and Linux (GCC, Clang), plus Linux Clang builds with ThreadSanitizer and with AddressSanitizer + UBSan (`ASWLOG_SANITIZE`). +- `.github/workflows/ci.yml` builds and runs the unit tests on Windows (MSVC, MinGW) and Linux (GCC, Clang), plus Linux Clang builds with ThreadSanitizer and with AddressSanitizer + UBSan (`ASWLOG_SANITIZE`); the non-sanitizer jobs also check the installed CMake package (`tests/package/`). - `example/` contains example app that uses the logger. - `example/rad370/` contains the RAD Studio 13.1 project and Windows build scripts. - `README.md` contains repo details. diff --git a/CHANGELOG.md b/CHANGELOG.md index d914505..d171850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -244,6 +244,16 @@ see [0.26.1](#0261---2026-09-17) for the initial versioned baseline. Windows and UTF-32 on Linux); invalid input becomes U+FFFD. `std::format` has no `char16_t` format strings, so `char16_t` text goes in as a message or through `Wide()`. +- CMake install and package files: `cmake --install` on the repository root + installs the static library, the headers (to `include/ASWLog`, so code + includes them as before, e.g. `#include "ASWLog_FileLog.h"`) and + `ASWLogConfig.cmake`, after which `find_package(ASWLog CONFIG)` gives the + `ASWLog::ASWLog` target. A requested version is met by any later release + with the same major version. The install rules are on by default only when + ASWLog is the top-level project (option `ASWLOG_INSTALL`). A Debug build of + the library is now named `ASWLogd` (e.g. `ASWLogd.lib`), so Debug and + Release can share an install prefix; nothing changes for code that links to + the `ASWLog::ASWLog` target. ### Changed diff --git a/CMakeLists.txt b/CMakeLists.txt index 3443741..0117f0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,20 @@ cmake_minimum_required(VERSION 3.20) -project(ASWLog LANGUAGES CXX) +# The version lives only in ASWLog/ASWLog_Version.h, so the project version comes from its ASWLOG_VERSION_MAJOR, +# _MINOR, and _PATCH macros (a CMake version has no pre-release part). +file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/ASWLog/ASWLog_Version.h" ASWLOG_VERSION_DEFINES + REGEX "^#define ASWLOG_VERSION_(MAJOR|MINOR|PATCH) ") +foreach(versionPart MAJOR MINOR PATCH) + if(NOT ASWLOG_VERSION_DEFINES MATCHES "#define ASWLOG_VERSION_${versionPart} ([0-9]+)") + message(FATAL_ERROR "ASWLog/ASWLog_Version.h doesn't define ASWLOG_VERSION_${versionPart} as a number") + endif() + set(ASWLOG_VERSION_${versionPart} "${CMAKE_MATCH_1}") +endforeach() + +project(ASWLog + VERSION ${ASWLOG_VERSION_MAJOR}.${ASWLOG_VERSION_MINOR}.${ASWLOG_VERSION_PATCH} + LANGUAGES CXX +) # Building ASWLog itself (cmake -S .) rather than consuming it through add_subdirectory() or FetchContent. if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) @@ -11,6 +25,8 @@ endif() option(ASWLOG_BUILD_EXAMPLE "Build the ASWLog example (example/cmake)" OFF) option(ASWLOG_BUILD_TESTS "Build the ASWLog unit tests (tests/cmake; needs the asw-unit-tests submodule)" OFF) +option(ASWLOG_INSTALL "Generate the install rules for the ASWLog library, headers, and CMake package files" + ${ASWLOG_IS_TOP_LEVEL}) option(ASWLOG_WARNINGS "Compile the ASWLog sources with extra warnings" ${ASWLOG_IS_TOP_LEVEL}) set(ASWLOG_SANITIZE "" CACHE STRING @@ -64,9 +80,14 @@ add_library(ASWLog STATIC add_library(ASWLog::ASWLog ALIAS ASWLog) target_compile_features(ASWLog PUBLIC cxx_std_20) -set_target_properties(ASWLog PROPERTIES CXX_EXTENSIONS OFF) +# A Debug build is named ASWLogd, so Debug and Release can be installed to the same prefix. +set_target_properties(ASWLog PROPERTIES + CXX_EXTENSIONS OFF + DEBUG_POSTFIX d +) -target_include_directories(ASWLog PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/ASWLog") +# The installed target's include folder comes from install(TARGETS) below. +target_include_directories(ASWLog PUBLIC "$") if(WIN32) # ASWLog_Utils.cpp uses the process memory APIs from psapi, and ASWLog_WindowsEventLog.cpp the Event Log and @@ -108,6 +129,48 @@ if(ASWLOG_SANITIZE) target_link_options(ASWLog PUBLIC ${ASWLOG_SANITIZE_FLAGS}) endif() +# After `cmake --install`, find_package(ASWLog CONFIG) gives the ASWLog::ASWLog target. The headers are installed to +# /include/ASWLog, the installed target's include folder, so code includes them the same way as from the +# sources (e.g. #include "ASWLog_FileLog.h"). +if(ASWLOG_INSTALL) + include(CMakePackageConfigHelpers) + include(GNUInstallDirs) + + set(ASWLOG_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/ASWLog") + + install(TARGETS ASWLog + EXPORT ASWLogTargets + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/ASWLog" + ) + + # Every header is public. + get_target_property(ASWLOG_HEADERS ASWLog SOURCES) + list(FILTER ASWLOG_HEADERS INCLUDE REGEX "\\.h$") + install(FILES ${ASWLOG_HEADERS} DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/ASWLog") + + install(EXPORT ASWLogTargets + NAMESPACE ASWLog:: + DESTINATION "${ASWLOG_INSTALL_CMAKEDIR}" + ) + + configure_package_config_file(cmake/ASWLogConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/ASWLogConfig.cmake" + INSTALL_DESTINATION "${ASWLOG_INSTALL_CMAKEDIR}" + ) + + # As in Semantic Versioning, find_package(ASWLog 1.2) accepts version 1.2.0 or any later 1.x, but not 2.0. + write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/ASWLogConfigVersion.cmake" + VERSION "${ASWLog_VERSION}" + COMPATIBILITY SameMajorVersion + ) + + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/ASWLogConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/ASWLogConfigVersion.cmake" + DESTINATION "${ASWLOG_INSTALL_CMAKEDIR}" + ) +endif() + if(ASWLOG_BUILD_EXAMPLE) add_subdirectory(example/cmake example) endif() diff --git a/Deploy.bat b/Deploy.bat index 9edb618..8657210 100644 --- a/Deploy.bat +++ b/Deploy.bat @@ -64,6 +64,9 @@ if errorlevel 1 exit /b 1 call :CopyFile "README.md" "%TARGET_DIR%\README.md" if errorlevel 1 exit /b 1 +call :CopyFile "cmake\ASWLogConfig.cmake.in" "%TARGET_DIR%\cmake\ASWLogConfig.cmake.in" +if errorlevel 1 exit /b 1 + call :CopyFile "ASWLog\ASWLog_Base.cpp" "%TARGET_DIR%\ASWLog\ASWLog_Base.cpp" if errorlevel 1 exit /b 1 call :CopyFile "ASWLog\ASWLog_Base.h" "%TARGET_DIR%\ASWLog\ASWLog_Base.h" diff --git a/README.md b/README.md index 9280917..3f4ce05 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,27 @@ Either way, only the library is built. When ASWLog is the top-level project, `-D `-DASWLOG_BUILD_TESTS=ON` also build the example and the unit tests, and `ASWLOG_WARNINGS` (on by default only there) compiles the library with extra warnings. +To use a prebuilt copy instead, build and install the library from the repository root (a Debug build is named +`ASWLogd`, so Debug and Release can share one install prefix): + +``` +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --config Release +cmake --install build --config Release --prefix C:/Libraries/ASWLog +``` + +and find it with `find_package` (pass the prefix in `CMAKE_PREFIX_PATH`): + +``` +find_package(ASWLog 1.0 CONFIG REQUIRED) +target_link_libraries(MyApp PRIVATE ASWLog::ASWLog) +``` + +The headers are installed to `/include/ASWLog`, the target's include folder, so code includes them as it does +from the sources (`#include "ASWLog_FileLog.h"`). A requested version is met by any later release with the same major +version. The install rules are on by default only when ASWLog is the top-level project; set `ASWLOG_INSTALL` to change +that. `tests/package` is a small project that builds against an installed copy; CI runs it. + The `example/cmake` folder contains a portable CMake project for building the example with CMake, JetBrains CLion, Visual Studio, Clang, or MinGW. From the repository root, configure and build it with: diff --git a/cmake/ASWLogConfig.cmake.in b/cmake/ASWLogConfig.cmake.in new file mode 100644 index 0000000..a8ce389 --- /dev/null +++ b/cmake/ASWLogConfig.cmake.in @@ -0,0 +1,14 @@ +# The CMake package file of an installed ASWLog (see the root CMakeLists.txt). After +# find_package(ASWLog CONFIG REQUIRED) +# link to the ASWLog::ASWLog target. + +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) + +# ASWLog::ASWLog links to Threads::Threads (TASWTextLogBase runs a std::thread). +find_dependency(Threads) + +include("${CMAKE_CURRENT_LIST_DIR}/ASWLogTargets.cmake") + +check_required_components(ASWLog) diff --git a/tests/package/CMakeLists.txt b/tests/package/CMakeLists.txt new file mode 100644 index 0000000..2226d61 --- /dev/null +++ b/tests/package/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.20) + +project(ASWLogPackageCheck LANGUAGES CXX) + +# Checks the installed CMake package: builds Check_Package.cpp against an ASWLog installed with `cmake --install` +# (point CMAKE_PREFIX_PATH at the install prefix), and `ctest` runs it. See the README's CMake section. +find_package(ASWLog CONFIG REQUIRED) + +add_executable(ASWLogPackageCheck + "${CMAKE_CURRENT_SOURCE_DIR}/Check_Package.cpp" +) + +target_link_libraries(ASWLogPackageCheck PRIVATE ASWLog::ASWLog) + +# The version the package's version file reported, which the check compares with the installed headers' version +target_compile_definitions(ASWLogPackageCheck PRIVATE "ASWLOG_PACKAGE_VERSION=\"${ASWLog_VERSION}\"") + +if(MSVC) + target_compile_options(ASWLogPackageCheck PRIVATE /W4) +else() + target_compile_options(ASWLogPackageCheck PRIVATE -Wall -Wextra -Wpedantic) +endif() + +if(MINGW) + # Statically link the MinGW runtime so the executable launches without MinGW's bin directory on PATH (e.g. on a + # CI runner). + target_link_options(ASWLogPackageCheck PRIVATE -static-libgcc -static-libstdc++ -static) +endif() + +enable_testing() +add_test(NAME ASWLogPackageCheck COMMAND ASWLogPackageCheck) diff --git a/tests/package/Check_Package.cpp b/tests/package/Check_Package.cpp new file mode 100644 index 0000000..b4614c9 --- /dev/null +++ b/tests/package/Check_Package.cpp @@ -0,0 +1,70 @@ +/* ************************************************************************** +Check_Package.cpp +Author: Anthony S. West - ASW Software + +Check of the installed CMake package: tests/package/CMakeLists.txt builds this file against an ASWLog installed with +`cmake --install` and found with find_package(), and running it returns 0 when the package works. + +Copyright 2026 ASW Software + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +************************************************************************** */ + +#include +#include +#include +#include +//--------------------------------------------------------------------------- +#include "ASWLog_MemoryLog.h" +#include "ASWLog_Version.h" +//--------------------------------------------------------------------------- + +#if !defined(ASWLOG_PACKAGE_VERSION) +#error Define ASWLOG_PACKAGE_VERSION +#endif + +int main() +{ + // The version find_package() found (from ASWLogConfigVersion.cmake) must be the installed headers' version + const std::string headerVersion = std::format("{}.{}.{}", ASWLog::VersionMajor, ASWLog::VersionMinor, + ASWLog::VersionPatch); + if (headerVersion != ASWLOG_PACKAGE_VERSION) + { + std::cerr << std::format("The package version {} isn't the headers' version {}\n", ASWLOG_PACKAGE_VERSION, + headerVersion); + return 1; + } + + // The default configuration writes the startup lines, which use the system libraries the installed target links to + // (e.g. the memory usage) + constexpr std::string_view message = "Logged through the installed ASWLog package"; + ASWLog::TASWMemoryLog log; + if (!log.Initialize(ASWLog::TASWLogConfig())) + { + std::cerr << "TASWMemoryLog::Initialize() failed\n"; + return 1; + } + + log.LogInfo(message); + const auto lines = log.GetLines(); + if (lines.size() < 2 || !lines.back().ends_with(message)) + { + std::cerr << "The memory logger should have kept the startup lines and the message\n"; + return 1; + } + + std::cout << "ASWLog " << ASWLog::Version << " package works: " << lines.size() << " lines logged\n"; + + return 0; +}