From c20c75bf1dc95a72272fae9c29aab83d6acc2a58 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 1 Sep 2026 19:14:20 -0300 Subject: [PATCH 1/2] Support Meson as a consumption build system See: https://github.com/sourcemeta/blaze/issues/1017 Signed-off-by: Juan Cruz Viotti --- .github/workflows/ci.yml | 11 +- Brewfile | 3 + CMakeLists.txt | 6 + cmake/Sourcemeta.cmake | 1 + cmake/common/targets/library.cmake | 4 + cmake/common/targets/pkgconfig.cmake | 316 +++++++++++++++++++++ doxygen/index.markdown | 31 ++ src/lang/test/CMakeLists.txt | 3 + test/packaging/CMakeLists.txt | 41 +++ test/packaging/meson_pkgconfig/hello.cc | 76 +++++ test/packaging/meson_pkgconfig/meson.build | 8 + 11 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 cmake/common/targets/pkgconfig.cmake create mode 100644 test/packaging/meson_pkgconfig/hello.cc create mode 100644 test/packaging/meson_pkgconfig/meson.build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1967fdc502..6d2a5536b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,14 @@ jobs: - name: Install dependencies (Linux) if: runner.os == 'Linux' timeout-minutes: 5 - run: sudo apt-get update && sudo apt-get install --yes ccache ${{ matrix.platform.apt }} + run: sudo apt-get update && sudo apt-get install --yes ccache pkg-config ${{ matrix.platform.apt }} + # The packaged Meson trails the current release by years, and the + # packaging tests exercise how it reads what we install + - name: Install Meson (Linux) + if: runner.os == 'Linux' + run: | + pipx install meson + pipx install ninja - name: Install dependencies (Windows) if: runner.os == 'Windows' run: choco install ccache --no-progress --yes @@ -147,6 +154,8 @@ jobs: - run: ccache --zero-stats - run: cmake --version + - run: meson --version + if: runner.os != 'Windows' - name: Configure (static) if: matrix.platform.type == 'static' run: > diff --git a/Brewfile b/Brewfile index 65799b9e8d..5945ce08ff 100644 --- a/Brewfile +++ b/Brewfile @@ -1,3 +1,6 @@ brew "ccache" brew "cmake" brew "doxygen" +brew "meson" +brew "ninja" +brew "pkgconf" diff --git a/CMakeLists.txt b/CMakeLists.txt index af73da73a5..88be6349f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -267,6 +267,12 @@ if(SOURCEMETA_CORE_DIFF) add_subdirectory(src/core/diff) endif() +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_pkgconfig_install_aggregate(NAME sourcemeta_core + DESCRIPTION "${PROJECT_DESCRIPTION}" + COMPONENT sourcemeta_core_dev) +endif() + if(SOURCEMETA_CORE_DOCS) sourcemeta_target_doxygen(CONFIG "${PROJECT_SOURCE_DIR}/doxygen/Doxyfile.in" OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/website") diff --git a/cmake/Sourcemeta.cmake b/cmake/Sourcemeta.cmake index 70eaef5ab9..56da62fbec 100644 --- a/cmake/Sourcemeta.cmake +++ b/cmake/Sourcemeta.cmake @@ -8,6 +8,7 @@ include("${SOURCEMETA_UTILITIES_DIRECTORY}/compiler/options.cmake") include("${SOURCEMETA_UTILITIES_DIRECTORY}/options/enum.cmake") include("${SOURCEMETA_UTILITIES_DIRECTORY}/commands/copy-file.cmake") include("${SOURCEMETA_UTILITIES_DIRECTORY}/targets/library.cmake") +include("${SOURCEMETA_UTILITIES_DIRECTORY}/targets/pkgconfig.cmake") include("${SOURCEMETA_UTILITIES_DIRECTORY}/targets/executable.cmake") include("${SOURCEMETA_UTILITIES_DIRECTORY}/targets/clang-format.cmake") include("${SOURCEMETA_UTILITIES_DIRECTORY}/targets/shellcheck.cmake") diff --git a/cmake/common/targets/library.cmake b/cmake/common/targets/library.cmake index 8085dee0d7..f42fae14c4 100644 --- a/cmake/common/targets/library.cmake +++ b/cmake/common/targets/library.cmake @@ -177,4 +177,8 @@ function(sourcemeta_library_install) DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${SOURCEMETA_LIBRARY_PROJECT}" NAMESPACE ${NAMESPACE_PREFIX} COMPONENT ${COMPONENT_NAME}_dev) + + sourcemeta_pkgconfig_install(TARGET ${TARGET_NAME} + DESCRIPTION "The ${SOURCEMETA_LIBRARY_NAME} library of ${PROJECT_DESCRIPTION}" + COMPONENT ${COMPONENT_NAME}_dev) endfunction() diff --git a/cmake/common/targets/pkgconfig.cmake b/cmake/common/targets/pkgconfig.cmake new file mode 100644 index 0000000000..0f055d63fe --- /dev/null +++ b/cmake/common/targets/pkgconfig.cmake @@ -0,0 +1,316 @@ +# Nothing refers to the entry points that replace the standard allocator by +# name, so a linker that only pulls in the archive members it needs would leave +# the program running on the allocator it was trying to replace +function(sourcemeta_pkgconfig_allocator OUTPUT_VARIABLE) + if(BUILD_SHARED_LIBS) + set(FLAGS "-lmimalloc") + elseif(APPLE) + set(FLAGS "-Wl,-force_load,\${libdir}/libmimalloc.a") + elseif(SOURCEMETA_COMPILER_MSVC) + set(FLAGS "/WHOLEARCHIVE:mimalloc.lib") + else() + set(FLAGS + "-Wl,--whole-archive \${libdir}/libmimalloc.a -Wl,--no-whole-archive") + endif() + + set("${OUTPUT_VARIABLE}" "${FLAGS}" PARENT_SCOPE) +endfunction() + +function(sourcemeta_pkgconfig_dependency LIBRARY MODULES_VARIABLE LIBS_VARIABLE) + set(MODULES ${${MODULES_VARIABLE}}) + set(LIBS ${${LIBS_VARIABLE}}) + + # CMake keeps the private dependencies of a static library in its exported + # link interface behind this expression, as they take part in linking but not + # in compiling against the library + string(REGEX REPLACE "^\\$$" "\\1" LIBRARY "${LIBRARY}") + + if(LIBRARY MATCHES "^sourcemeta::") + string(REPLACE "::" "_" MODULE "${LIBRARY}") + list(APPEND MODULES "${MODULE}") + elseif(LIBRARY STREQUAL "Mimalloc::Mimalloc") + # The allocator is a property of the program rather than of any one module, + # and it is pulled in whole. Naming it here would make a consumer that asks + # for two modules at once hand the same archive to the linker twice, which + # reports every symbol in it as duplicated. It ships as a module of its own + # instead, which the aggregate carries + elseif(LIBRARY STREQUAL "PCRE2::pcre2") + list(APPEND LIBS "-lpcre2" "-lsljit") + elseif(LIBRARY STREQUAL "LibDeflate::LibDeflate") + list(APPEND LIBS "-ldeflate") + elseif(LIBRARY STREQUAL "CMarkGFM::cmark_gfm") + list(APPEND LIBS "-lcmark_gfm") + elseif(LIBRARY STREQUAL "OpenSSL::Crypto") + list(APPEND LIBS "-lcrypto") + elseif(LIBRARY STREQUAL "CURL::libcurl") + list(APPEND LIBS "-lcurl") + elseif(LIBRARY STREQUAL "Threads::Threads") + if(CMAKE_USE_PTHREADS_INIT) + list(APPEND LIBS "-pthread") + endif() + elseif(LIBRARY MATCHES "\\.framework$") + get_filename_component(FRAMEWORK "${LIBRARY}" NAME_WE) + list(APPEND LIBS "-framework ${FRAMEWORK}") + elseif(LIBRARY MATCHES "^-") + list(APPEND LIBS "${LIBRARY}") + elseif(NOT TARGET "${LIBRARY}" AND LIBRARY MATCHES "^[a-zA-Z0-9_]+$") + # A bare name that names no target is what CMake reads as a library the + # system already provides, which is linked the same way here + list(APPEND LIBS "-l${LIBRARY}") + else() + message(FATAL_ERROR "Cannot express the dependency on ${LIBRARY} as a " + "pkg-config flag. Teach ${CMAKE_CURRENT_FUNCTION} how to spell it, so " + "that consumers that do not read CMake keep linking against it") + endif() + + set("${MODULES_VARIABLE}" ${MODULES} PARENT_SCOPE) + set("${LIBS_VARIABLE}" ${LIBS} PARENT_SCOPE) +endfunction() + +function(sourcemeta_pkgconfig_direct + TARGET_NAME MODULES_VARIABLE OPTIONS_VARIABLE LIBS_VARIABLE) + set(MODULES ${${MODULES_VARIABLE}}) + set(OPTIONS ${${OPTIONS_VARIABLE}}) + set(LIBS ${${LIBS_VARIABLE}}) + + # A module that reaches for something the toolchain does not find on its own, + # such as a runtime that sits outside the default search paths, says so here + # rather than through a library it links against + get_target_property(LINK_OPTIONS ${TARGET_NAME} INTERFACE_LINK_OPTIONS) + if(LINK_OPTIONS) + foreach(OPTION IN LISTS LINK_OPTIONS) + if(OPTION MATCHES "^(SHELL|LINKER):") + message(FATAL_ERROR "The link option ${OPTION} of ${TARGET_NAME} is " + "spelled in a way that only CMake knows how to expand. Write it as " + "a single token instead, so that it survives into the packaging " + "that consumers which do not read CMake rely on") + endif() + list(APPEND OPTIONS "${OPTION}") + endforeach() + endif() + + set(ENTRIES) + get_target_property(TARGET_TYPE ${TARGET_NAME} TYPE) + get_target_property(PUBLIC_LIBRARIES + ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) + if(PUBLIC_LIBRARIES) + list(APPEND ENTRIES ${PUBLIC_LIBRARIES}) + endif() + + # An interface library carries its entire dependency set in its link + # interface, and has no private one to read + if(NOT TARGET_TYPE STREQUAL "INTERFACE_LIBRARY") + get_target_property(PRIVATE_LIBRARIES ${TARGET_NAME} LINK_LIBRARIES) + if(PRIVATE_LIBRARIES) + list(APPEND ENTRIES ${PRIVATE_LIBRARIES}) + endif() + endif() + + foreach(LIBRARY IN LISTS ENTRIES) + sourcemeta_pkgconfig_dependency("${LIBRARY}" MODULES LIBS) + endforeach() + + set("${MODULES_VARIABLE}" ${MODULES} PARENT_SCOPE) + set("${OPTIONS_VARIABLE}" ${OPTIONS} PARENT_SCOPE) + set("${LIBS_VARIABLE}" ${LIBS} PARENT_SCOPE) +endfunction() + +# Walk the whole graph below a module rather than pointing at the neighbours it +# happens to touch. Naming the neighbours reads better, but pkg-config expands +# such a chain once per path that reaches a module, and a linker handed the +# resulting hundreds of repeated archives either takes minutes or gives up +function(sourcemeta_pkgconfig_closure + ROOTS MODULES_VARIABLE OPTIONS_VARIABLE LIBS_VARIABLE) + set(PENDING ${ROOTS}) + set(VISITED) + set(OPTIONS) + set(LIBS) + + while(PENDING) + list(POP_FRONT PENDING CURRENT) + if(CURRENT IN_LIST VISITED) + continue() + endif() + list(APPEND VISITED "${CURRENT}") + set(DIRECT) + sourcemeta_pkgconfig_direct("${CURRENT}" DIRECT OPTIONS LIBS) + list(APPEND PENDING ${DIRECT}) + endwhile() + + # A module is declared after everything it depends on, as CMake could not + # resolve the dependency otherwise, so walking the declarations backwards + # gives a linker the order it needs + get_property(DECLARED GLOBAL PROPERTY SOURCEMETA_PKGCONFIG_TARGETS) + list(REVERSE DECLARED) + set(MODULES) + foreach(MODULE IN LISTS DECLARED) + if(MODULE IN_LIST VISITED) + list(APPEND MODULES "${MODULE}") + endif() + endforeach() + + list(LENGTH VISITED EXPECTED) + list(LENGTH MODULES ACTUAL) + if(NOT EXPECTED EQUAL ACTUAL) + message(FATAL_ERROR "Some of the modules that ${ROOTS} depends on are not " + "packaged, and consumers that do not read CMake would fail to link " + "against them: ${VISITED}") + endif() + + if(OPTIONS) + list(REMOVE_DUPLICATES OPTIONS) + endif() + if(LIBS) + list(REMOVE_DUPLICATES LIBS) + endif() + + set("${MODULES_VARIABLE}" ${MODULES} PARENT_SCOPE) + set("${OPTIONS_VARIABLE}" ${OPTIONS} PARENT_SCOPE) + set("${LIBS_VARIABLE}" ${LIBS} PARENT_SCOPE) +endfunction() + +function(sourcemeta_pkgconfig_flags TARGET_NAME OUTPUT_VARIABLE) + get_target_property(TARGET_TYPE ${TARGET_NAME} TYPE) + if(TARGET_TYPE STREQUAL "INTERFACE_LIBRARY") + set("${OUTPUT_VARIABLE}" "" PARENT_SCOPE) + else() + get_target_property(OUTPUT_NAME ${TARGET_NAME} OUTPUT_NAME) + if(NOT OUTPUT_NAME) + set(OUTPUT_NAME "${TARGET_NAME}") + endif() + set("${OUTPUT_VARIABLE}" "-l${OUTPUT_NAME}" PARENT_SCOPE) + endif() +endfunction() + +function(sourcemeta_pkgconfig_file OUTPUT NAME DESCRIPTION LIBS PRIVATE_LIBS) + # Anchoring at the location of the file itself, rather than at the prefix + # that was configured, keeps the result correct for an installation that is + # staged, relocated, or packaged somewhere other than where it was built + if(IS_ABSOLUTE "${CMAKE_INSTALL_LIBDIR}") + set(PREFIX "${CMAKE_INSTALL_PREFIX}") + else() + string(REGEX REPLACE "[^/]+" ".." PREFIX + "${CMAKE_INSTALL_LIBDIR}/pkgconfig") + set(PREFIX "\${pcfiledir}/${PREFIX}") + endif() + + string(CONCAT CONTENT + "prefix=${PREFIX}\n" + "exec_prefix=\${prefix}\n" + "libdir=\${prefix}/${CMAKE_INSTALL_LIBDIR}\n" + "includedir=\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}\n" + "\n" + "Name: ${NAME}\n" + "Description: ${DESCRIPTION}\n" + "Version: ${PROJECT_VERSION}\n" + "Cflags: -I\${includedir}\n" + "Libs: ${LIBS}\n" + "Libs.private: ${PRIVATE_LIBS}\n") + + # Generated rather than written, as a link option is allowed to be a + # generator expression and only CMake itself knows what it stands for + file(GENERATE OUTPUT "${OUTPUT}" CONTENT "${CONTENT}") +endfunction() + +function(sourcemeta_pkgconfig_write TARGET_NAME OUTPUT DESCRIPTION) + sourcemeta_pkgconfig_closure("${TARGET_NAME}" MODULES OPTIONS LIBS) + list(REMOVE_ITEM MODULES "${TARGET_NAME}") + + sourcemeta_pkgconfig_flags("${TARGET_NAME}" LIBRARY_FLAGS) + if(LIBRARY_FLAGS) + set(LIBRARY_FLAGS "-L\${libdir} ${LIBRARY_FLAGS}") + endif() + + # Ahead of the libraries, as some of them are the search paths that the + # libraries are then looked up in + set(PRIVATE_FLAGS ${OPTIONS}) + foreach(MODULE IN LISTS MODULES) + sourcemeta_pkgconfig_flags("${MODULE}" MODULE_FLAGS) + if(MODULE_FLAGS) + list(APPEND PRIVATE_FLAGS "${MODULE_FLAGS}") + endif() + endforeach() + list(APPEND PRIVATE_FLAGS ${LIBS}) + list(JOIN PRIVATE_FLAGS " " PRIVATE_FLAGS) + + sourcemeta_pkgconfig_file("${OUTPUT}" "${TARGET_NAME}" "${DESCRIPTION}" + "${LIBRARY_FLAGS}" "${PRIVATE_FLAGS}") +endfunction() + +function(sourcemeta_pkgconfig_write_aggregate NAME OUTPUT DESCRIPTION) + get_property(TARGETS GLOBAL PROPERTY SOURCEMETA_PKGCONFIG_TARGETS) + sourcemeta_pkgconfig_closure("${TARGETS}" MODULES OPTIONS LIBS) + + set(FLAGS ${OPTIONS}) + list(APPEND FLAGS "-L\${libdir}") + foreach(MODULE IN LISTS MODULES) + sourcemeta_pkgconfig_flags("${MODULE}" MODULE_FLAGS) + if(MODULE_FLAGS) + list(APPEND FLAGS "${MODULE_FLAGS}") + endif() + endforeach() + list(APPEND FLAGS ${LIBS}) + if(TARGET Mimalloc::Mimalloc) + sourcemeta_pkgconfig_allocator(ALLOCATOR_FLAGS) + list(APPEND FLAGS "${ALLOCATOR_FLAGS}") + endif() + list(JOIN FLAGS " " FLAGS) + + sourcemeta_pkgconfig_file("${OUTPUT}" "${NAME}" "${DESCRIPTION}" "${FLAGS}" "") +endfunction() + +function(sourcemeta_pkgconfig_install) + cmake_parse_arguments(SOURCEMETA_PKGCONFIG "" + "TARGET;DESCRIPTION;COMPONENT" "" ${ARGN}) + + set_property(GLOBAL APPEND PROPERTY SOURCEMETA_PKGCONFIG_TARGETS + "${SOURCEMETA_PKGCONFIG_TARGET}") + set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${SOURCEMETA_PKGCONFIG_TARGET}.pc") + + # A module states some of its dependencies after asking to be installed, so + # the link interface is only complete once the project has been walked. The + # arguments of a deferred call are evaluated when the call finally runs, by + # which point these variables are long gone, so bake them in here + cmake_language(EVAL CODE + "cmake_language(DEFER DIRECTORY \"${PROJECT_SOURCE_DIR}\" + CALL sourcemeta_pkgconfig_write + \"${SOURCEMETA_PKGCONFIG_TARGET}\" \"${OUTPUT}\" + \"${SOURCEMETA_PKGCONFIG_DESCRIPTION}\")") + + include(GNUInstallDirs) + install(FILES "${OUTPUT}" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" + COMPONENT ${SOURCEMETA_PKGCONFIG_COMPONENT}) +endfunction() + +# The whole library behind a single name, which is how a consumer that does not +# read CMake gets the allocator along with everything else +function(sourcemeta_pkgconfig_install_aggregate) + cmake_parse_arguments(SOURCEMETA_PKGCONFIG "" + "NAME;DESCRIPTION;COMPONENT" "" ${ARGN}) + + set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${SOURCEMETA_PKGCONFIG_NAME}.pc") + cmake_language(EVAL CODE + "cmake_language(DEFER DIRECTORY \"${PROJECT_SOURCE_DIR}\" + CALL sourcemeta_pkgconfig_write_aggregate + \"${SOURCEMETA_PKGCONFIG_NAME}\" \"${OUTPUT}\" + \"${SOURCEMETA_PKGCONFIG_DESCRIPTION}\")") + + include(GNUInstallDirs) + install(FILES "${OUTPUT}" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" + COMPONENT ${SOURCEMETA_PKGCONFIG_COMPONENT}) + + if(TARGET Mimalloc::Mimalloc) + sourcemeta_pkgconfig_allocator(ALLOCATOR_FLAGS) + set(ALLOCATOR_OUTPUT + "${CMAKE_CURRENT_BINARY_DIR}/${SOURCEMETA_PKGCONFIG_NAME}_mimalloc.pc") + sourcemeta_pkgconfig_file("${ALLOCATOR_OUTPUT}" + "${SOURCEMETA_PKGCONFIG_NAME}_mimalloc" + "The bundled mimalloc allocator of ${SOURCEMETA_PKGCONFIG_DESCRIPTION}" + "-L\${libdir} ${ALLOCATOR_FLAGS}" "") + install(FILES "${ALLOCATOR_OUTPUT}" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" + COMPONENT ${SOURCEMETA_PKGCONFIG_COMPONENT}) + endif() +endfunction() diff --git a/doxygen/index.markdown b/doxygen/index.markdown index 4ca641233e..8cf8311099 100644 --- a/doxygen/index.markdown +++ b/doxygen/index.markdown @@ -69,6 +69,37 @@ target_link_libraries(my_executable_or_library PUBLIC sourcemeta::core::jsonpoin target_link_libraries(my_executable_or_library PUBLIC sourcemeta::core::jsonl) ``` +### Using pkg-config + +Assuming you have installed Sourcemeta Core in a place where pkg-config can +find it, the entire library is available under a single name: + +```sh +$ c++ -std=c++23 my_example.cc $(pkg-config --cflags --libs --static sourcemeta_core) +``` + +Every module is also packaged on its own, for programs that would rather link +against only what they use: + +```sh +$ c++ -std=c++23 my_example.cc $(pkg-config --cflags --libs --static sourcemeta_core_json sourcemeta_core_jsonpointer) +``` + +The bundled mimalloc allocator comes with the aggregate but not with the +individual modules, as it is linked whole and a linker rejects being handed it +more than once. Name it alongside the modules to opt in: + +```sh +$ c++ -std=c++23 my_example.cc $(pkg-config --cflags --libs --static sourcemeta_core_json sourcemeta_core_mimalloc) +``` + +Build systems that read pkg-config find the library the same way. For example, +with Meson: + +```meson +core_dependency = dependency('sourcemeta_core', static : true) +``` + CMake ----- diff --git a/src/lang/test/CMakeLists.txt b/src/lang/test/CMakeLists.txt index ecf71d6fc2..347f4ca58f 100644 --- a/src/lang/test/CMakeLists.txt +++ b/src/lang/test/CMakeLists.txt @@ -43,4 +43,7 @@ if(SOURCEMETA_CORE_INSTALL) DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/core" NAMESPACE sourcemeta:: COMPONENT sourcemeta_core_dev) + sourcemeta_pkgconfig_install(TARGET sourcemeta_core_test_main + DESCRIPTION "The test entry point library of ${PROJECT_DESCRIPTION}" + COMPONENT sourcemeta_core_dev) endif() diff --git a/test/packaging/CMakeLists.txt b/test/packaging/CMakeLists.txt index 982ee84169..ceac8a159f 100644 --- a/test/packaging/CMakeLists.txt +++ b/test/packaging/CMakeLists.txt @@ -12,3 +12,44 @@ add_test(NAME ${PROJECT_NAME}.find_package_build COMMAND --config "${CMAKE_BUILD_TYPE}") set_tests_properties(${PROJECT_NAME}.find_package_build PROPERTIES DEPENDS ${PROJECT_NAME}.find_package_configure) + +# This consumption path speaks in `-l` flags and relies on pkg-config being +# around, neither of which is how a library is found on Windows +find_program(MESON_EXECUTABLE NAMES meson) +if(MESON_EXECUTABLE AND NOT WIN32) + # Unlike a plain setup, this works both on a fresh build directory and on one + # left behind by a previous run, which is what makes the test repeatable + add_test(NAME ${PROJECT_NAME}.meson_pkgconfig_setup COMMAND + "${MESON_EXECUTABLE}" setup --reconfigure + "${CMAKE_CURRENT_BINARY_DIR}/meson_pkgconfig" + "${CMAKE_CURRENT_SOURCE_DIR}/meson_pkgconfig" + "-Dpkg_config_path=${PROJECT_SOURCE_DIR}/build/dist/${CMAKE_INSTALL_LIBDIR}/pkgconfig") + add_test(NAME ${PROJECT_NAME}.meson_pkgconfig_compile COMMAND + "${MESON_EXECUTABLE}" compile -C "${CMAKE_CURRENT_BINARY_DIR}/meson_pkgconfig") + set_tests_properties(${PROJECT_NAME}.meson_pkgconfig_compile + PROPERTIES DEPENDS ${PROJECT_NAME}.meson_pkgconfig_setup) +endif() + +# The allocator is the one dependency whose absence a consumer cannot notice on +# its own, as a program links and runs perfectly well without it +find_program(PKG_CONFIG_EXECUTABLE NAMES pkg-config) +if(PKG_CONFIG_EXECUTABLE AND NOT WIN32) + set(PKG_CONFIG_ENVIRONMENT + "PKG_CONFIG_PATH=${PROJECT_SOURCE_DIR}/build/dist/${CMAKE_INSTALL_LIBDIR}/pkgconfig") + if(TARGET Mimalloc::Mimalloc) + add_test(NAME ${PROJECT_NAME}.pkg_config_allocator COMMAND + "${PKG_CONFIG_EXECUTABLE}" --static --libs sourcemeta_core) + set_tests_properties(${PROJECT_NAME}.pkg_config_allocator + PROPERTIES + PASS_REGULAR_EXPRESSION "mimalloc" + ENVIRONMENT "${PKG_CONFIG_ENVIRONMENT}") + endif() + + # Asking for every module at once only resolves if each of them is packaged + # and none of them names something that is not + get_property(PKG_CONFIG_MODULES GLOBAL PROPERTY SOURCEMETA_PKGCONFIG_TARGETS) + add_test(NAME ${PROJECT_NAME}.pkg_config_modules COMMAND + "${PKG_CONFIG_EXECUTABLE}" --static --libs ${PKG_CONFIG_MODULES}) + set_tests_properties(${PROJECT_NAME}.pkg_config_modules + PROPERTIES ENVIRONMENT "${PKG_CONFIG_ENVIRONMENT}") +endif() diff --git a/test/packaging/meson_pkgconfig/hello.cc b/test/packaging/meson_pkgconfig/hello.cc new file mode 100644 index 0000000000..9c236e83ac --- /dev/null +++ b/test/packaging/meson_pkgconfig/hello.cc @@ -0,0 +1,76 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // std::uint8_t +#include // EXIT_SUCCESS +#include // std::cout +#include // std::string + +auto main() -> int { + const sourcemeta::core::JSON document{"Hello World"}; + sourcemeta::core::stringify(document, std::cout); + std::cout << std::endl; + + // Every one of these reaches a dependency that the module itself does not + // name, which is what a consumer needs the packaging to resolve for it + std::cout << sourcemeta::core::matches_if_valid("^Hello", "Hello World") + << std::endl; + std::cout << sourcemeta::core::markdown_to_html("# Hello") << std::endl; + const std::string input{"Hello World"}; + std::cout << sourcemeta::core::gzip( + reinterpret_cast(input.data()), + input.size()) + .size() + << std::endl; + std::cout << sourcemeta::core::is_idn_email("hello@\xCE\xB4.example") + << std::endl; + std::cout << sourcemeta::core::uuidv4().size() << std::endl; + + // Reaches the Swift shim, which autolinks a runtime that lives outside the + // paths a linker searches on its own + const auto key{sourcemeta::core::make_eddsa_public_key( + sourcemeta::core::EdwardsCurve::Ed25519, std::string(32, '\x01'))}; + std::cout << (key.has_value() && + sourcemeta::core::eddsa_verify(key.value(), "message", + std::string(64, '\x00'))) + << std::endl; + return EXIT_SUCCESS; +} diff --git a/test/packaging/meson_pkgconfig/meson.build b/test/packaging/meson_pkgconfig/meson.build new file mode 100644 index 0000000000..0e74b676ef --- /dev/null +++ b/test/packaging/meson_pkgconfig/meson.build @@ -0,0 +1,8 @@ +project('core_hello', 'cpp', + default_options : [ 'cpp_std=c++23' ]) + +# The whole library behind a single name, with no knowledge of how it is split +# into modules or of what any of them link against +core_dependency = dependency('sourcemeta_core', static : true, required : true) + +executable('core_hello', 'hello.cc', dependencies : [ core_dependency ]) From c698bd676c07b0a814ea59cbbf1c3e5de8c4649b Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 2 Sep 2026 13:28:17 -0300 Subject: [PATCH 2/2] Simpler? Signed-off-by: Juan Cruz Viotti --- .github/workflows/ci.yml | 9 +- CMakeLists.txt | 2 +- cmake/FindCMarkGFM.cmake | 1 + cmake/FindLibDeflate.cmake | 1 + cmake/FindMimalloc.cmake | 16 ++ cmake/FindPCRE2.cmake | 1 + cmake/common/targets/library.cmake | 4 +- cmake/common/targets/pkgconfig.cmake | 184 ++++++--------------- doxygen/index.markdown | 31 ---- src/core/crypto/CMakeLists.txt | 1 + src/core/http/CMakeLists.txt | 1 + src/lang/parallel/CMakeLists.txt | 5 + src/lang/test/CMakeLists.txt | 4 +- test/packaging/CMakeLists.txt | 27 +-- test/packaging/meson_pkgconfig/meson.build | 2 +- 15 files changed, 85 insertions(+), 204 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d2a5536b0..3497fe6125 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,14 +113,7 @@ jobs: - name: Install dependencies (Linux) if: runner.os == 'Linux' timeout-minutes: 5 - run: sudo apt-get update && sudo apt-get install --yes ccache pkg-config ${{ matrix.platform.apt }} - # The packaged Meson trails the current release by years, and the - # packaging tests exercise how it reads what we install - - name: Install Meson (Linux) - if: runner.os == 'Linux' - run: | - pipx install meson - pipx install ninja + run: sudo apt-get update && sudo apt-get install --yes ccache pkg-config meson ninja-build ${{ matrix.platform.apt }} - name: Install dependencies (Windows) if: runner.os == 'Windows' run: choco install ccache --no-progress --yes diff --git a/CMakeLists.txt b/CMakeLists.txt index 88be6349f3..a512c08e58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -268,7 +268,7 @@ if(SOURCEMETA_CORE_DIFF) endif() if(SOURCEMETA_CORE_INSTALL) - sourcemeta_pkgconfig_install_aggregate(NAME sourcemeta_core + sourcemeta_pkgconfig_install(NAME sourcemeta_core DESCRIPTION "${PROJECT_DESCRIPTION}" COMPONENT sourcemeta_core_dev) endif() diff --git a/cmake/FindCMarkGFM.cmake b/cmake/FindCMarkGFM.cmake index 6d50b32b36..e8a290cb3b 100644 --- a/cmake/FindCMarkGFM.cmake +++ b/cmake/FindCMarkGFM.cmake @@ -112,6 +112,7 @@ if(NOT CMarkGFM_FOUND) EXPORT_FILE_NAME "${CMARK_GFM_BINARY_DIR}/include/cmark-gfm_export.h") add_library(CMarkGFM::cmark_gfm ALIAS cmark_gfm) + sourcemeta_pkgconfig_declare(TARGET CMarkGFM::cmark_gfm LIBS "-lcmark_gfm") set_target_properties(cmark_gfm PROPERTIES diff --git a/cmake/FindLibDeflate.cmake b/cmake/FindLibDeflate.cmake index 1b830bbbb8..94fa5ccf11 100644 --- a/cmake/FindLibDeflate.cmake +++ b/cmake/FindLibDeflate.cmake @@ -136,6 +136,7 @@ if(NOT LibDeflate_FOUND) EXPORT_NAME LibDeflate) add_library(LibDeflate::LibDeflate ALIAS libdeflate) + sourcemeta_pkgconfig_declare(TARGET LibDeflate::LibDeflate LIBS "-ldeflate") if(SOURCEMETA_CORE_INSTALL) include(GNUInstallDirs) diff --git a/cmake/FindMimalloc.cmake b/cmake/FindMimalloc.cmake index 9ac939c40e..626f99576e 100644 --- a/cmake/FindMimalloc.cmake +++ b/cmake/FindMimalloc.cmake @@ -127,6 +127,22 @@ if(NOT Mimalloc_FOUND) add_library(Mimalloc::Mimalloc ALIAS mimalloc_interface) + # The same whole archive requirement as above, spelled for the consumers that + # read what we package rather than what CMake exports + if(BUILD_SHARED_LIBS) + sourcemeta_pkgconfig_declare(TARGET Mimalloc::Mimalloc LIBS "-lmimalloc") + elseif(APPLE) + sourcemeta_pkgconfig_declare(TARGET Mimalloc::Mimalloc + LIBS "-Wl,-force_load,\${libdir}/libmimalloc.a") + elseif(SOURCEMETA_COMPILER_MSVC) + sourcemeta_pkgconfig_declare(TARGET Mimalloc::Mimalloc + LIBS "/WHOLEARCHIVE:mimalloc.lib") + else() + sourcemeta_pkgconfig_declare(TARGET Mimalloc::Mimalloc + LIBS "-Wl,--whole-archive \${libdir}/libmimalloc.a" + "-Wl,--no-whole-archive") + endif() + if(SOURCEMETA_CORE_INSTALL) install(TARGETS mimalloc mimalloc_interface EXPORT mimalloc diff --git a/cmake/FindPCRE2.cmake b/cmake/FindPCRE2.cmake index 21756978b6..52256fefce 100644 --- a/cmake/FindPCRE2.cmake +++ b/cmake/FindPCRE2.cmake @@ -178,6 +178,7 @@ if(NOT PCRE2_FOUND) target_link_libraries(pcre2 PRIVATE sljit) add_library(PCRE2::pcre2 ALIAS pcre2) + sourcemeta_pkgconfig_declare(TARGET PCRE2::pcre2 LIBS "-lpcre2" "-lsljit") set_target_properties(pcre2 PROPERTIES diff --git a/cmake/common/targets/library.cmake b/cmake/common/targets/library.cmake index f42fae14c4..8ca1231a4e 100644 --- a/cmake/common/targets/library.cmake +++ b/cmake/common/targets/library.cmake @@ -178,7 +178,5 @@ function(sourcemeta_library_install) NAMESPACE ${NAMESPACE_PREFIX} COMPONENT ${COMPONENT_NAME}_dev) - sourcemeta_pkgconfig_install(TARGET ${TARGET_NAME} - DESCRIPTION "The ${SOURCEMETA_LIBRARY_NAME} library of ${PROJECT_DESCRIPTION}" - COMPONENT ${COMPONENT_NAME}_dev) + sourcemeta_pkgconfig_register(${TARGET_NAME}) endfunction() diff --git a/cmake/common/targets/pkgconfig.cmake b/cmake/common/targets/pkgconfig.cmake index 0f055d63fe..ea8d8d772b 100644 --- a/cmake/common/targets/pkgconfig.cmake +++ b/cmake/common/targets/pkgconfig.cmake @@ -1,19 +1,17 @@ -# Nothing refers to the entry points that replace the standard allocator by -# name, so a linker that only pulls in the archive members it needs would leave -# the program running on the allocator it was trying to replace -function(sourcemeta_pkgconfig_allocator OUTPUT_VARIABLE) - if(BUILD_SHARED_LIBS) - set(FLAGS "-lmimalloc") - elseif(APPLE) - set(FLAGS "-Wl,-force_load,\${libdir}/libmimalloc.a") - elseif(SOURCEMETA_COMPILER_MSVC) - set(FLAGS "/WHOLEARCHIVE:mimalloc.lib") - else() - set(FLAGS - "-Wl,--whole-archive \${libdir}/libmimalloc.a -Wl,--no-whole-archive") - endif() +# Teach the packaging how to link against something that is not one of our own +# modules. A project states this next to wherever it brings the dependency in, +# so that adding one never means editing this file +function(sourcemeta_pkgconfig_declare) + cmake_parse_arguments(SOURCEMETA_PKGCONFIG "" "TARGET" "LIBS" ${ARGN}) + set_property(GLOBAL PROPERTY + "SOURCEMETA_PKGCONFIG_LIBS_${SOURCEMETA_PKGCONFIG_TARGET}" + "${SOURCEMETA_PKGCONFIG_LIBS}") +endfunction() - set("${OUTPUT_VARIABLE}" "${FLAGS}" PARENT_SCOPE) +# The order matters, as a module is declared after everything it depends on +function(sourcemeta_pkgconfig_register TARGET_NAME) + set_property(GLOBAL APPEND PROPERTY + SOURCEMETA_PKGCONFIG_TARGETS "${TARGET_NAME}") endfunction() function(sourcemeta_pkgconfig_dependency LIBRARY MODULES_VARIABLE LIBS_VARIABLE) @@ -25,29 +23,16 @@ function(sourcemeta_pkgconfig_dependency LIBRARY MODULES_VARIABLE LIBS_VARIABLE) # in compiling against the library string(REGEX REPLACE "^\\$$" "\\1" LIBRARY "${LIBRARY}") + get_property(DECLARED GLOBAL PROPERTY + "SOURCEMETA_PKGCONFIG_LIBS_${LIBRARY}" SET) + if(LIBRARY MATCHES "^sourcemeta::") string(REPLACE "::" "_" MODULE "${LIBRARY}") list(APPEND MODULES "${MODULE}") - elseif(LIBRARY STREQUAL "Mimalloc::Mimalloc") - # The allocator is a property of the program rather than of any one module, - # and it is pulled in whole. Naming it here would make a consumer that asks - # for two modules at once hand the same archive to the linker twice, which - # reports every symbol in it as duplicated. It ships as a module of its own - # instead, which the aggregate carries - elseif(LIBRARY STREQUAL "PCRE2::pcre2") - list(APPEND LIBS "-lpcre2" "-lsljit") - elseif(LIBRARY STREQUAL "LibDeflate::LibDeflate") - list(APPEND LIBS "-ldeflate") - elseif(LIBRARY STREQUAL "CMarkGFM::cmark_gfm") - list(APPEND LIBS "-lcmark_gfm") - elseif(LIBRARY STREQUAL "OpenSSL::Crypto") - list(APPEND LIBS "-lcrypto") - elseif(LIBRARY STREQUAL "CURL::libcurl") - list(APPEND LIBS "-lcurl") - elseif(LIBRARY STREQUAL "Threads::Threads") - if(CMAKE_USE_PTHREADS_INIT) - list(APPEND LIBS "-pthread") - endif() + elseif(DECLARED) + get_property(DECLARED_LIBS GLOBAL PROPERTY + "SOURCEMETA_PKGCONFIG_LIBS_${LIBRARY}") + list(APPEND LIBS ${DECLARED_LIBS}) elseif(LIBRARY MATCHES "\\.framework$") get_filename_component(FRAMEWORK "${LIBRARY}" NAME_WE) list(APPEND LIBS "-framework ${FRAMEWORK}") @@ -59,8 +44,8 @@ function(sourcemeta_pkgconfig_dependency LIBRARY MODULES_VARIABLE LIBS_VARIABLE) list(APPEND LIBS "-l${LIBRARY}") else() message(FATAL_ERROR "Cannot express the dependency on ${LIBRARY} as a " - "pkg-config flag. Teach ${CMAKE_CURRENT_FUNCTION} how to spell it, so " - "that consumers that do not read CMake keep linking against it") + "pkg-config flag. Declare it with sourcemeta_pkgconfig_declare, so that " + "consumers that do not read CMake keep linking against it") endif() set("${MODULES_VARIABLE}" ${MODULES} PARENT_SCOPE) @@ -115,10 +100,6 @@ function(sourcemeta_pkgconfig_direct set("${LIBS_VARIABLE}" ${LIBS} PARENT_SCOPE) endfunction() -# Walk the whole graph below a module rather than pointing at the neighbours it -# happens to touch. Naming the neighbours reads better, but pkg-config expands -# such a chain once per path that reaches a module, and a linker handed the -# resulting hundreds of repeated archives either takes minutes or gives up function(sourcemeta_pkgconfig_closure ROOTS MODULES_VARIABLE OPTIONS_VARIABLE LIBS_VARIABLE) set(PENDING ${ROOTS}) @@ -169,20 +150,27 @@ function(sourcemeta_pkgconfig_closure set("${LIBS_VARIABLE}" ${LIBS} PARENT_SCOPE) endfunction() -function(sourcemeta_pkgconfig_flags TARGET_NAME OUTPUT_VARIABLE) - get_target_property(TARGET_TYPE ${TARGET_NAME} TYPE) - if(TARGET_TYPE STREQUAL "INTERFACE_LIBRARY") - set("${OUTPUT_VARIABLE}" "" PARENT_SCOPE) - else() - get_target_property(OUTPUT_NAME ${TARGET_NAME} OUTPUT_NAME) - if(NOT OUTPUT_NAME) - set(OUTPUT_NAME "${TARGET_NAME}") +function(sourcemeta_pkgconfig_write NAME OUTPUT DESCRIPTION) + get_property(TARGETS GLOBAL PROPERTY SOURCEMETA_PKGCONFIG_TARGETS) + sourcemeta_pkgconfig_closure("${TARGETS}" MODULES OPTIONS LIBS) + + # Ahead of the libraries, as some of them are the search paths that the + # libraries are then looked up in + set(FLAGS ${OPTIONS}) + list(APPEND FLAGS "-L\${libdir}") + foreach(MODULE IN LISTS MODULES) + get_target_property(TARGET_TYPE ${MODULE} TYPE) + if(NOT TARGET_TYPE STREQUAL "INTERFACE_LIBRARY") + get_target_property(OUTPUT_NAME ${MODULE} OUTPUT_NAME) + if(NOT OUTPUT_NAME) + set(OUTPUT_NAME "${MODULE}") + endif() + list(APPEND FLAGS "-l${OUTPUT_NAME}") endif() - set("${OUTPUT_VARIABLE}" "-l${OUTPUT_NAME}" PARENT_SCOPE) - endif() -endfunction() + endforeach() + list(APPEND FLAGS ${LIBS}) + list(JOIN FLAGS " " FLAGS) -function(sourcemeta_pkgconfig_file OUTPUT NAME DESCRIPTION LIBS PRIVATE_LIBS) # Anchoring at the location of the file itself, rather than at the prefix # that was configured, keeps the result correct for an installation that is # staged, relocated, or packaged somewhere other than where it was built @@ -204,68 +192,20 @@ function(sourcemeta_pkgconfig_file OUTPUT NAME DESCRIPTION LIBS PRIVATE_LIBS) "Description: ${DESCRIPTION}\n" "Version: ${PROJECT_VERSION}\n" "Cflags: -I\${includedir}\n" - "Libs: ${LIBS}\n" - "Libs.private: ${PRIVATE_LIBS}\n") + "Libs: ${FLAGS}\n") # Generated rather than written, as a link option is allowed to be a # generator expression and only CMake itself knows what it stands for file(GENERATE OUTPUT "${OUTPUT}" CONTENT "${CONTENT}") endfunction() -function(sourcemeta_pkgconfig_write TARGET_NAME OUTPUT DESCRIPTION) - sourcemeta_pkgconfig_closure("${TARGET_NAME}" MODULES OPTIONS LIBS) - list(REMOVE_ITEM MODULES "${TARGET_NAME}") - - sourcemeta_pkgconfig_flags("${TARGET_NAME}" LIBRARY_FLAGS) - if(LIBRARY_FLAGS) - set(LIBRARY_FLAGS "-L\${libdir} ${LIBRARY_FLAGS}") - endif() - - # Ahead of the libraries, as some of them are the search paths that the - # libraries are then looked up in - set(PRIVATE_FLAGS ${OPTIONS}) - foreach(MODULE IN LISTS MODULES) - sourcemeta_pkgconfig_flags("${MODULE}" MODULE_FLAGS) - if(MODULE_FLAGS) - list(APPEND PRIVATE_FLAGS "${MODULE_FLAGS}") - endif() - endforeach() - list(APPEND PRIVATE_FLAGS ${LIBS}) - list(JOIN PRIVATE_FLAGS " " PRIVATE_FLAGS) - - sourcemeta_pkgconfig_file("${OUTPUT}" "${TARGET_NAME}" "${DESCRIPTION}" - "${LIBRARY_FLAGS}" "${PRIVATE_FLAGS}") -endfunction() - -function(sourcemeta_pkgconfig_write_aggregate NAME OUTPUT DESCRIPTION) - get_property(TARGETS GLOBAL PROPERTY SOURCEMETA_PKGCONFIG_TARGETS) - sourcemeta_pkgconfig_closure("${TARGETS}" MODULES OPTIONS LIBS) - - set(FLAGS ${OPTIONS}) - list(APPEND FLAGS "-L\${libdir}") - foreach(MODULE IN LISTS MODULES) - sourcemeta_pkgconfig_flags("${MODULE}" MODULE_FLAGS) - if(MODULE_FLAGS) - list(APPEND FLAGS "${MODULE_FLAGS}") - endif() - endforeach() - list(APPEND FLAGS ${LIBS}) - if(TARGET Mimalloc::Mimalloc) - sourcemeta_pkgconfig_allocator(ALLOCATOR_FLAGS) - list(APPEND FLAGS "${ALLOCATOR_FLAGS}") - endif() - list(JOIN FLAGS " " FLAGS) - - sourcemeta_pkgconfig_file("${OUTPUT}" "${NAME}" "${DESCRIPTION}" "${FLAGS}" "") -endfunction() - +# The whole library behind a single name, which is all a consumer that does not +# read CMake needs to know about how we are put together function(sourcemeta_pkgconfig_install) cmake_parse_arguments(SOURCEMETA_PKGCONFIG "" - "TARGET;DESCRIPTION;COMPONENT" "" ${ARGN}) + "NAME;DESCRIPTION;COMPONENT" "" ${ARGN}) - set_property(GLOBAL APPEND PROPERTY SOURCEMETA_PKGCONFIG_TARGETS - "${SOURCEMETA_PKGCONFIG_TARGET}") - set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${SOURCEMETA_PKGCONFIG_TARGET}.pc") + set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${SOURCEMETA_PKGCONFIG_NAME}.pc") # A module states some of its dependencies after asking to be installed, so # the link interface is only complete once the project has been walked. The @@ -274,25 +214,6 @@ function(sourcemeta_pkgconfig_install) cmake_language(EVAL CODE "cmake_language(DEFER DIRECTORY \"${PROJECT_SOURCE_DIR}\" CALL sourcemeta_pkgconfig_write - \"${SOURCEMETA_PKGCONFIG_TARGET}\" \"${OUTPUT}\" - \"${SOURCEMETA_PKGCONFIG_DESCRIPTION}\")") - - include(GNUInstallDirs) - install(FILES "${OUTPUT}" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" - COMPONENT ${SOURCEMETA_PKGCONFIG_COMPONENT}) -endfunction() - -# The whole library behind a single name, which is how a consumer that does not -# read CMake gets the allocator along with everything else -function(sourcemeta_pkgconfig_install_aggregate) - cmake_parse_arguments(SOURCEMETA_PKGCONFIG "" - "NAME;DESCRIPTION;COMPONENT" "" ${ARGN}) - - set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${SOURCEMETA_PKGCONFIG_NAME}.pc") - cmake_language(EVAL CODE - "cmake_language(DEFER DIRECTORY \"${PROJECT_SOURCE_DIR}\" - CALL sourcemeta_pkgconfig_write_aggregate \"${SOURCEMETA_PKGCONFIG_NAME}\" \"${OUTPUT}\" \"${SOURCEMETA_PKGCONFIG_DESCRIPTION}\")") @@ -300,17 +221,4 @@ function(sourcemeta_pkgconfig_install_aggregate) install(FILES "${OUTPUT}" DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" COMPONENT ${SOURCEMETA_PKGCONFIG_COMPONENT}) - - if(TARGET Mimalloc::Mimalloc) - sourcemeta_pkgconfig_allocator(ALLOCATOR_FLAGS) - set(ALLOCATOR_OUTPUT - "${CMAKE_CURRENT_BINARY_DIR}/${SOURCEMETA_PKGCONFIG_NAME}_mimalloc.pc") - sourcemeta_pkgconfig_file("${ALLOCATOR_OUTPUT}" - "${SOURCEMETA_PKGCONFIG_NAME}_mimalloc" - "The bundled mimalloc allocator of ${SOURCEMETA_PKGCONFIG_DESCRIPTION}" - "-L\${libdir} ${ALLOCATOR_FLAGS}" "") - install(FILES "${ALLOCATOR_OUTPUT}" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" - COMPONENT ${SOURCEMETA_PKGCONFIG_COMPONENT}) - endif() endfunction() diff --git a/doxygen/index.markdown b/doxygen/index.markdown index 8cf8311099..4ca641233e 100644 --- a/doxygen/index.markdown +++ b/doxygen/index.markdown @@ -69,37 +69,6 @@ target_link_libraries(my_executable_or_library PUBLIC sourcemeta::core::jsonpoin target_link_libraries(my_executable_or_library PUBLIC sourcemeta::core::jsonl) ``` -### Using pkg-config - -Assuming you have installed Sourcemeta Core in a place where pkg-config can -find it, the entire library is available under a single name: - -```sh -$ c++ -std=c++23 my_example.cc $(pkg-config --cflags --libs --static sourcemeta_core) -``` - -Every module is also packaged on its own, for programs that would rather link -against only what they use: - -```sh -$ c++ -std=c++23 my_example.cc $(pkg-config --cflags --libs --static sourcemeta_core_json sourcemeta_core_jsonpointer) -``` - -The bundled mimalloc allocator comes with the aggregate but not with the -individual modules, as it is linked whole and a linker rejects being handed it -more than once. Name it alongside the modules to opt in: - -```sh -$ c++ -std=c++23 my_example.cc $(pkg-config --cflags --libs --static sourcemeta_core_json sourcemeta_core_mimalloc) -``` - -Build systems that read pkg-config find the library the same way. For example, -with Meson: - -```meson -core_dependency = dependency('sourcemeta_core', static : true) -``` - CMake ----- diff --git a/src/core/crypto/CMakeLists.txt b/src/core/crypto/CMakeLists.txt index 03d0d32734..35fdd01e60 100644 --- a/src/core/crypto/CMakeLists.txt +++ b/src/core/crypto/CMakeLists.txt @@ -30,6 +30,7 @@ if(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) crypto_hkdf_openssl.cc crypto_openssl.h crypto_pkcs8.h) target_link_libraries(sourcemeta_core_crypto PRIVATE OpenSSL::Crypto) + sourcemeta_pkgconfig_declare(TARGET OpenSSL::Crypto LIBS "-lcrypto") elseif(APPLE) enable_language(OBJCXX) diff --git a/src/core/http/CMakeLists.txt b/src/core/http/CMakeLists.txt index ba553b9224..a1f610d911 100644 --- a/src/core/http/CMakeLists.txt +++ b/src/core/http/CMakeLists.txt @@ -37,6 +37,7 @@ if(SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) target_compile_definitions(sourcemeta_core_http PRIVATE SOURCEMETA_CORE_HTTP_USE_SYSTEM_CURL) target_link_libraries(sourcemeta_core_http PRIVATE CURL::libcurl) + sourcemeta_pkgconfig_declare(TARGET CURL::libcurl LIBS "-lcurl") elseif(APPLE) # Resolve the framework to an absolute bundle path rather than passing a raw # linker flag, as the exported interface is also read by build systems that diff --git a/src/lang/parallel/CMakeLists.txt b/src/lang/parallel/CMakeLists.txt index ef981e99bc..e237b8af24 100644 --- a/src/lang/parallel/CMakeLists.txt +++ b/src/lang/parallel/CMakeLists.txt @@ -6,3 +6,8 @@ if(SOURCEMETA_CORE_INSTALL) endif() target_link_libraries(sourcemeta_core_parallel INTERFACE Threads::Threads) +if(CMAKE_USE_PTHREADS_INIT) + sourcemeta_pkgconfig_declare(TARGET Threads::Threads LIBS "-pthread") +else() + sourcemeta_pkgconfig_declare(TARGET Threads::Threads LIBS) +endif() diff --git a/src/lang/test/CMakeLists.txt b/src/lang/test/CMakeLists.txt index 347f4ca58f..f41eb451c6 100644 --- a/src/lang/test/CMakeLists.txt +++ b/src/lang/test/CMakeLists.txt @@ -43,7 +43,5 @@ if(SOURCEMETA_CORE_INSTALL) DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/core" NAMESPACE sourcemeta:: COMPONENT sourcemeta_core_dev) - sourcemeta_pkgconfig_install(TARGET sourcemeta_core_test_main - DESCRIPTION "The test entry point library of ${PROJECT_DESCRIPTION}" - COMPONENT sourcemeta_core_dev) + sourcemeta_pkgconfig_register(sourcemeta_core_test_main) endif() diff --git a/test/packaging/CMakeLists.txt b/test/packaging/CMakeLists.txt index ceac8a159f..943da9d438 100644 --- a/test/packaging/CMakeLists.txt +++ b/test/packaging/CMakeLists.txt @@ -33,23 +33,12 @@ endif() # The allocator is the one dependency whose absence a consumer cannot notice on # its own, as a program links and runs perfectly well without it find_program(PKG_CONFIG_EXECUTABLE NAMES pkg-config) -if(PKG_CONFIG_EXECUTABLE AND NOT WIN32) - set(PKG_CONFIG_ENVIRONMENT - "PKG_CONFIG_PATH=${PROJECT_SOURCE_DIR}/build/dist/${CMAKE_INSTALL_LIBDIR}/pkgconfig") - if(TARGET Mimalloc::Mimalloc) - add_test(NAME ${PROJECT_NAME}.pkg_config_allocator COMMAND - "${PKG_CONFIG_EXECUTABLE}" --static --libs sourcemeta_core) - set_tests_properties(${PROJECT_NAME}.pkg_config_allocator - PROPERTIES - PASS_REGULAR_EXPRESSION "mimalloc" - ENVIRONMENT "${PKG_CONFIG_ENVIRONMENT}") - endif() - - # Asking for every module at once only resolves if each of them is packaged - # and none of them names something that is not - get_property(PKG_CONFIG_MODULES GLOBAL PROPERTY SOURCEMETA_PKGCONFIG_TARGETS) - add_test(NAME ${PROJECT_NAME}.pkg_config_modules COMMAND - "${PKG_CONFIG_EXECUTABLE}" --static --libs ${PKG_CONFIG_MODULES}) - set_tests_properties(${PROJECT_NAME}.pkg_config_modules - PROPERTIES ENVIRONMENT "${PKG_CONFIG_ENVIRONMENT}") +if(PKG_CONFIG_EXECUTABLE AND NOT WIN32 AND TARGET Mimalloc::Mimalloc) + add_test(NAME ${PROJECT_NAME}.pkg_config_allocator COMMAND + "${PKG_CONFIG_EXECUTABLE}" --libs sourcemeta_core) + set_tests_properties(${PROJECT_NAME}.pkg_config_allocator + PROPERTIES + PASS_REGULAR_EXPRESSION "mimalloc" + ENVIRONMENT + "PKG_CONFIG_PATH=${PROJECT_SOURCE_DIR}/build/dist/${CMAKE_INSTALL_LIBDIR}/pkgconfig") endif() diff --git a/test/packaging/meson_pkgconfig/meson.build b/test/packaging/meson_pkgconfig/meson.build index 0e74b676ef..ccbb5c1e6f 100644 --- a/test/packaging/meson_pkgconfig/meson.build +++ b/test/packaging/meson_pkgconfig/meson.build @@ -3,6 +3,6 @@ project('core_hello', 'cpp', # The whole library behind a single name, with no knowledge of how it is split # into modules or of what any of them link against -core_dependency = dependency('sourcemeta_core', static : true, required : true) +core_dependency = dependency('sourcemeta_core', required : true) executable('core_hello', 'hello.cc', dependencies : [ core_dependency ])