Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +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 ${{ matrix.platform.apt }}
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
Expand Down Expand Up @@ -147,6 +147,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: >
Expand Down
3 changes: 3 additions & 0 deletions Brewfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
brew "ccache"
brew "cmake"
brew "doxygen"
brew "meson"
brew "ninja"
brew "pkgconf"
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ if(SOURCEMETA_CORE_DIFF)
add_subdirectory(src/core/diff)
endif()

if(SOURCEMETA_CORE_INSTALL)
sourcemeta_pkgconfig_install(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")
Expand Down
1 change: 1 addition & 0 deletions cmake/FindCMarkGFM.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmake/FindLibDeflate.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions cmake/FindMimalloc.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmake/FindPCRE2.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmake/Sourcemeta.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions cmake/common/targets/library.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,6 @@ function(sourcemeta_library_install)
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${SOURCEMETA_LIBRARY_PROJECT}"
NAMESPACE ${NAMESPACE_PREFIX}
COMPONENT ${COMPONENT_NAME}_dev)

sourcemeta_pkgconfig_register(${TARGET_NAME})
endfunction()
224 changes: 224 additions & 0 deletions cmake/common/targets/pkgconfig.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# 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()

# 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)
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 "^\\$<LINK_ONLY:(.+)>$" "\\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(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}")
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. 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)
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()

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_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()
endforeach()
list(APPEND FLAGS ${LIBS})
list(JOIN FLAGS " " FLAGS)

# 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: ${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()

# 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 ""
"NAME;DESCRIPTION;COMPONENT" "" ${ARGN})

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
# 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_NAME}\" \"${OUTPUT}\"
\"${SOURCEMETA_PKGCONFIG_DESCRIPTION}\")")

include(GNUInstallDirs)
install(FILES "${OUTPUT}"
DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig"
COMPONENT ${SOURCEMETA_PKGCONFIG_COMPONENT})
endfunction()
1 change: 1 addition & 0 deletions src/core/crypto/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/core/http/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/lang/parallel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
1 change: 1 addition & 0 deletions src/lang/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ if(SOURCEMETA_CORE_INSTALL)
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/core"
NAMESPACE sourcemeta::
COMPONENT sourcemeta_core_dev)
sourcemeta_pkgconfig_register(sourcemeta_core_test_main)
endif()
30 changes: 30 additions & 0 deletions test/packaging/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,33 @@ 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 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()
Loading
Loading