From 8102e30d8177dce64dba6a9d534e0fd67fd1c194 Mon Sep 17 00:00:00 2001 From: Martin Leitner-Ankerl Date: Wed, 5 Aug 2026 05:53:29 +0200 Subject: [PATCH] Make svector installable, and consumable from meson #31 asked in 2022 for nlohmann/json style CMake integration and was closed four days later with "Should work now". What shipped was the add_subdirectory subset: no install rule, no export set, no package config, no version file, in either build system. So find_package(svector) did not work, a distribution had nothing to call, and meson users could not consume this at all. There was a fossil of the abandoned attempt too. CMakeLists.txt referenced $ without ever including GNUInstallDirs, so that expanded to nothing. It was harmless only because nothing installed or exported. CMake now defines the target unconditionally and installs when svector is the project being built. It used to define the target only when included, so configuring this repository on its own produced literally nothing -- which is what the reporter of #48 ran into and reasonably read as the project being broken. A parent that pulled us in with add_subdirectory still gets no install rules of ours, and CMake still never builds the tests. The package version comes from project(), which lint-version.py already keeps in step with the ANKERL_SVECTOR_VERSION_* macros, so a package cannot claim a version the header does not. Compatibility is SameMajorVersion, which is what the header's own semver comment promises. A pkg-config file is installed for the consumers that use neither build system, which is most of what a distribution packages. On the meson side, declare_dependency plus override_dependency means a parent's dependency('svector') resolves through a wrap, and the test suite and header install are now skipped when we are a subproject rather than imposed on whoever included us. The cmake-consumer CI job covers both paths: add_subdirectory, then an install followed by find_package against the prefix, plus pkg-config. It also asserts that find_package refuses a version that is not there, because the happy path passing says nothing about whether the version is checked at all. It asks for major version 1 rather than an exact one so it needs no editing per release. Closes #89. Co-Authored-By: Claude Fable 5 --- .github/workflows/main.yml | 30 ++++++- CMakeLists.txt | 85 +++++++++++++++++--- README.md | 21 ++++- cmake/svector.pc.in | 8 ++ cmake/svectorConfig.cmake.in | 5 ++ meson.build | 13 ++- test/cmake_consumer/installed/CMakeLists.txt | 14 ++++ test/cmake_consumer/main.cpp | 3 +- 8 files changed, 160 insertions(+), 19 deletions(-) create mode 100644 cmake/svector.pc.in create mode 100644 cmake/svectorConfig.cmake.in create mode 100644 test/cmake_consumer/installed/CMakeLists.txt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 565ac47..ca2871f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -172,6 +172,30 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - run: cmake -S test/cmake_consumer -B builddir-cmake - - run: cmake --build builddir-cmake - - run: ./builddir-cmake/consumer + - name: add_subdirectory + run: | + cmake -S test/cmake_consumer -B builddir-cmake + cmake --build builddir-cmake + ./builddir-cmake/consumer + - name: install + run: | + cmake -S . -B builddir-install -DCMAKE_INSTALL_PREFIX="$PWD/prefix" + cmake --build builddir-install --target install + # Asking for major version 1 rather than the exact one, so this does not need editing on + # every release. SameMajorVersion is the compatibility the config declares. + - name: find_package + run: | + cmake -S test/cmake_consumer/installed -B builddir-find \ + -DCMAKE_PREFIX_PATH="$PWD/prefix" -DSVECTOR_REQUIRED_VERSION=1 + cmake --build builddir-find + ./builddir-find/consumer-installed + # The happy path passing says nothing about whether the version is actually checked. + - name: a version that is not installed has to be refused + run: | + if cmake -S test/cmake_consumer/installed -B builddir-badver \ + -DCMAKE_PREFIX_PATH="$PWD/prefix" -DSVECTOR_REQUIRED_VERSION=99 >/dev/null 2>&1; then + echo "::error::find_package accepted version 99, so it is not checking versions" + exit 1 + fi + - name: pkg-config + run: PKG_CONFIG_PATH="$PWD/prefix/share/pkgconfig" pkg-config --cflags --modversion svector diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ac9064..a0d64cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,19 +4,80 @@ project("svector" DESCRIPTION " Compact SVO optimized vector for C++17 or higher" HOMEPAGE_URL "https://github.com/martinus/svector") -# determine whether this is a standalone project or included by other projects -set(_svector_is_included ON) +include(GNUInstallDirs) + +# PROJECT_IS_TOP_LEVEL only arrived in CMake 3.21 and the floor here is 3.12, so this is spelled +# by hand. +set(_svector_is_top_level OFF) if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) - set(_svector_is_included OFF) + set(_svector_is_top_level ON) endif() -if(_svector_is_included) - add_library(svector INTERFACE) - add_library(svector::svector ALIAS svector) - target_compile_features(svector INTERFACE cxx_std_17) - target_include_directories( - svector - INTERFACE - $ - $) +# Defined unconditionally. It used to exist only when svector was pulled in with +# add_subdirectory, which meant configuring this repository on its own produced nothing at all -- +# see issue #48, where exactly that looked like the project was broken. An install also needs a +# target to export. +add_library(svector INTERFACE) +add_library(svector::svector ALIAS svector) +target_compile_features(svector INTERFACE cxx_std_17) +target_include_directories( + svector + INTERFACE + $ + $) + +# CMake still never builds or runs the tests, that is meson's job. What it does now is install, +# so that find_package(svector) works and packagers have something to call. +# +# Only when svector is the project being built: a parent that pulled it in with add_subdirectory +# should not find svector's headers turning up in its own install. +if(_svector_is_top_level) + include(CMakePackageConfigHelpers) + + set(_svector_cmakedir "${CMAKE_INSTALL_LIBDIR}/cmake/svector") + + install(TARGETS svector EXPORT svectorTargets) + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + install(EXPORT svectorTargets + FILE svectorTargets.cmake + NAMESPACE svector:: + DESTINATION "${_svector_cmakedir}") + + # ARCH_INDEPENDENT stops the version file rejecting a package whose word size differs from + # the consumer's, which for a header only library is not a real incompatibility. It needs + # CMake 3.14, above the floor, so it is added only where it exists. + set(_svector_version_args) + if(NOT CMAKE_VERSION VERSION_LESS 3.14) + list(APPEND _svector_version_args ARCH_INDEPENDENT) + endif() + + # The version comes from project() above, which scripts/lint/lint-version.py keeps in step + # with the ANKERL_SVECTOR_VERSION_* macros, so a package can never claim a version the + # header does not. SameMajorVersion is the compatibility the header's own semver comment + # promises. + write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/svectorConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion + ${_svector_version_args}) + + configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/svectorConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/svectorConfig.cmake" + INSTALL_DESTINATION "${_svector_cmakedir}") + + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/svectorConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/svectorConfigVersion.cmake" + DESTINATION "${_svector_cmakedir}") + + # For the consumers that never touch CMake at all, which is most of what a distribution + # packages. + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/svector.pc.in" + "${CMAKE_CURRENT_BINARY_DIR}/svector.pc" + @ONLY) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/svector.pc" + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig") endif() diff --git a/README.md b/README.md index db6609a..da8f744 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,23 @@ auto v = ankerl::svector(); // 7 int inline, heap only if it outgrows th v.push_back(42); ``` +Copying that one header into your project is a perfectly good way to use it. If you would rather a build system +did it, take your pick: + +```cmake +add_subdirectory(svector) # a submodule, or FetchContent +target_link_libraries(you PRIVATE svector::svector) + +find_package(svector 1 REQUIRED) # after cmake --install, or from a package +target_link_libraries(you PRIVATE svector::svector) +``` + +```meson +svector_dep = dependency('svector') # with subprojects/svector.wrap +``` + +A `pkg-config` file is installed as well, for builds that use neither. + ## How compact can it get? What is the smallest each implementation can be made, how many `uint8_t` fit inline at that size, and what does @@ -290,8 +307,8 @@ A few things are deliberately not the same as `std::vector`: ## Building & Testing -This project uses the [Meson](https://mesonbuild.com/) build system. The `CMakeLists.txt` is a convenience for -consumers and does not build or run the tests. +This project uses the [Meson](https://mesonbuild.com/) build system. The `CMakeLists.txt` is there for consumers +— it defines the target and installs the package — and never builds or runs the tests. ```sh meson setup builddir diff --git a/cmake/svector.pc.in b/cmake/svector.pc.in new file mode 100644 index 0000000..ebceabb --- /dev/null +++ b/cmake/svector.pc.in @@ -0,0 +1,8 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@ + +Name: svector +Description: @PROJECT_DESCRIPTION@ +URL: @PROJECT_HOMEPAGE_URL@ +Version: @PROJECT_VERSION@ +Cflags: -I${includedir} diff --git a/cmake/svectorConfig.cmake.in b/cmake/svectorConfig.cmake.in new file mode 100644 index 0000000..51a2788 --- /dev/null +++ b/cmake/svectorConfig.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/svectorTargets.cmake") + +check_required_components(svector) diff --git a/meson.build b/meson.build index d45fa91..abc0729 100644 --- a/meson.build +++ b/meson.build @@ -28,5 +28,16 @@ project('svector', 'cpp', ]) incdir = include_directories('include') -subdir('test') + +# So a parent project can use svector as a meson subproject: with this, its dependency('svector') +# resolves to us through the wrap. There was no way to consume this from meson at all before. +svector_dep = declare_dependency(include_directories: incdir) +meson.override_dependency('svector', svector_dep) + +# A parent pulling svector in wants the header, not our test suite and not our headers in its +# install tree. +if not meson.is_subproject() + install_headers('include/ankerl/svector.h', subdir: 'ankerl') + subdir('test') +endif diff --git a/test/cmake_consumer/installed/CMakeLists.txt b/test/cmake_consumer/installed/CMakeLists.txt new file mode 100644 index 0000000..d58456c --- /dev/null +++ b/test/cmake_consumer/installed/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.12) +project("svector-find-package-consumer" CXX) + +# Consume svector the way someone does after `cmake --install`, or the way a distribution's +# package expects to be used. Point CMAKE_PREFIX_PATH at the install prefix. +# +# SVECTOR_REQUIRED_VERSION exists so CI can also check that a version that is not there is +# actually refused, rather than only checking the happy path. +set(SVECTOR_REQUIRED_VERSION "" CACHE STRING "version to demand from find_package") + +find_package(svector ${SVECTOR_REQUIRED_VERSION} REQUIRED) + +add_executable(consumer-installed ../main.cpp) +target_link_libraries(consumer-installed PRIVATE svector::svector) diff --git a/test/cmake_consumer/main.cpp b/test/cmake_consumer/main.cpp index 918cfc9..6843340 100644 --- a/test/cmake_consumer/main.cpp +++ b/test/cmake_consumer/main.cpp @@ -2,6 +2,7 @@ #include +// Shared by both consumer projects here, the add_subdirectory one and the find_package one. // Deliberately shallow: this checks that the CMake target carries the include directory and // the C++17 requirement, not that svector behaves. The meson suite owns behaviour. auto main() -> int { @@ -13,7 +14,7 @@ auto main() -> int { return 1; } - std::printf("consumed svector %d.%d.%d through add_subdirectory\n", + std::printf("consumed svector %d.%d.%d through CMake\n", ANKERL_SVECTOR_VERSION_MAJOR, ANKERL_SVECTOR_VERSION_MINOR, ANKERL_SVECTOR_VERSION_PATCH);