How to auto generate pkgconfig files from cmake targets
Asked Answered
T

2

19

I would like to generate pkgconfig files in cmake from the targets. I started by writing something like this:

function(auto_pkgconfig TARGET)

    get_target_property(INCLUDE_DIRS ${TARGET} INTERFACE_INCLUDE_DIRECTORIES)
    string(REPLACE "$<BUILD_INTERFACE:" "$<0:" INCLUDE_DIRS "${INCLUDE_DIRS}")
    string(REPLACE "$<INSTALL_INTERFACE:" "$<1:" INCLUDE_DIRS "${INCLUDE_DIRS}")
    string(REPLACE "$<INSTALL_PREFIX>" "${CMAKE_INSTALL_PREFIX}" INCLUDE_DIRS "${INCLUDE_DIRS}")


    file(GENERATE OUTPUT ${TARGET}.pc CONTENT "
Name: ${TARGET}
Cflags: -I$<JOIN:${INCLUDE_DIRS}, -I>
Libs: -L${CMAKE_INSTALL_PREFIX}/lib -l${TARGET}
")
    install(FILES ${TARGET}.pc DESTINATION lib/pkgconfig)
endfunction()

This is a simplified version but it basically reads the INTERFACE_INCLUDE_DIRECTORIES properties and processes the INSTALL_INTERFACE of the generator expressions. This works well as long as the include directories are set before calling auto_pkgconfig, like this:

add_library(foo foo.cpp)

target_include_directories(foo PUBLIC 
    $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:$<INSTALL_PREFIX>/include>
    ${OTHER_INCLUDE_DIRS}
)

auto_pkgconfig(foo)

However, sometimes properties are set after the call to auto_pkgconfig, like this:

add_library(foo foo.cpp)
auto_pkgconfig(foo)

target_include_directories(foo PUBLIC 
    $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:$<INSTALL_PREFIX>/include>
    ${OTHER_INCLUDE_DIRS}
)

However, this won't properly read the include directories anymore. I would like auto_pkgconfig to run after all the target properties are set. I could use generator expressions for this, by changing auto_pkgconfig to this:

function(auto_pkgconfig TARGET)

    file(GENERATE OUTPUT ${TARGET}.pc CONTENT "
Name: ${TARGET}
Cflags: -I$<JOIN:$<TARGET_PROPERTY:${TARGET},INTERFACE_INCLUDE_DIRECTORIES>, -I>
Libs: -L$<TARGET_FILE_DIR:${TARGET}> -l${TARGET}
")
    install(FILES ${TARGET}.pc DESTINATION lib/pkgconfig)
endfunction() 

However, this will read the BUILD_INTERFACE instead of the INSTALL_INTERFACE. So is there another way to read target properties after they have been set?

Thuythuya answered 31/5, 2017 at 18:43 Comment(2)
Wow - no answer? :/Rainband
one way is to manually make sure call to auto_pkgconfig at the end. Another way will be hook to the event that the configure is about to finish. But this is not the current cmake api can do. There is a hacky way thoughTangible
T
3

According to the CMake documentation, the contents of INSTALL_INTERFACE are only available when calling install(EXPORT). Unless they extend CMake, it will be best to do something else to generate your PkgConfig files. Ideally you would have enough control over your install layout to make this easy.


However, this doesn't mean you can't do what you ask; it's just "Tony the Pony" levels of evil. I actually hesitated to post this. Please don't take this as a recommendation.

The idea is to use install(EXPORT) to have CMake generate the appropriate scripts. Then generate a dummy CMake project that uses the file(GENERATE OUTPUT ...) code you gave above; the dummy project will see the exported, ie. INSTALL_INTERFACE properties.

I initially tried to use install(CODE [[ ... ]]) to do this, but it also sees the $<BUILD_INTERFACE:...> view. I've asked about this on the CMake Discourse.

cmake_minimum_required(VERSION 3.16)
project(example)

# Dummy library for demo

add_library(example SHARED example.cpp)

target_compile_definitions(example
  PUBLIC $<BUILD_INTERFACE:BUILD>
         $<INSTALL_INTERFACE:INSTALL>)

target_include_directories(example
  PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
         $<INSTALL_INTERFACE:$<INSTALL_PREFIX>/include>)

# Here be dragons...

function(auto_pc TARGET)
  file(CONFIGURE OUTPUT "pc.${TARGET}/CMakeLists.txt"
       CONTENT [[
cmake_minimum_required(VERSION 3.16)
project(pc_@TARGET@)

find_package(pc_@TARGET@ REQUIRED CONFIG)

file(GENERATE OUTPUT @[email protected]
     CONTENT [=[
Name: @TARGET@
Cflags: -I$<JOIN:$<TARGET_PROPERTY:INTERFACE_INCLUDE_DIRECTORIES>, -I> -D$<JOIN:$<TARGET_PROPERTY:INTERFACE_COMPILE_DEFINITIONS>, -D>
Libs: -L$<TARGET_FILE_DIR:@TARGET@> -l@TARGET@
]=]   TARGET "@TARGET@")
]] @ONLY NEWLINE_STYLE LF)

  install(TARGETS ${TARGET} EXPORT pc_${TARGET})
  install(EXPORT pc_${TARGET} DESTINATION "_auto_pc" FILE pc_${TARGET}-config.cmake)

  file(CONFIGURE OUTPUT "pc.${TARGET}/post-install.cmake"
       CONTENT [[
file(REAL_PATH "${CMAKE_INSTALL_PREFIX}" prefix)
set(proj "@CMAKE_CURRENT_BINARY_DIR@/pc.@TARGET@")
execute_process(COMMAND "@CMAKE_COMMAND@" "-Dpc_@TARGET@_DIR=${prefix}/_auto_pc" -S "${proj}" -B "${proj}/build")
file(COPY "${proj}/build/@[email protected]" DESTINATION "${prefix}")
]] @ONLY NEWLINE_STYLE LF)

  install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/pc.${TARGET}/post-install.cmake")
endfunction()

auto_pc(example)

# Clean up install path
install(CODE [[ file(REMOVE_RECURSE "${CMAKE_INSTALL_PREFIX}/_auto_pc") ]])

This results in the following:

alex@Alex-Desktop:~/test$ cmake -S . -B build
...
-- Configuring done
-- Generating done
-- Build files have been written to: /home/alex/test/build
alex@Alex-Desktop:~/test$ cmake --build build/
...
alex@Alex-Desktop:~/test$ cmake --install build --prefix install
-- Install configuration: ""
-- Installing: /home/alex/test/install/lib/libexample.so
-- Installing: /home/alex/test/install/_auto_pc/pc_example-config.cmake
-- Installing: /home/alex/test/install/_auto_pc/pc_example-config-noconfig.cmake
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/alex/test/build/pc.example/build
alex@Alex-Desktop:~/test$ ls install/
example.pc  lib
alex@Alex-Desktop:~/test$ cat install/example.pc
Name: example
Cflags: -I/home/alex/test/install/include -DINSTALL
Libs: -L/home/alex/test/install/lib -lexample

This should make you sad. It makes me sad.

Tobitobiah answered 7/12, 2020 at 7:3 Comment(2)
so, what is the orthodox solution? (besides exporting targets to cmake files) the beauty of generating the pkgconfig files is: CMakeLists.txt is the single source of truth. here is a solution with template filesCuragh
@MilaNautikus - well, that's sort of the issue at play here. There's an enormous expressivity gap between exported CMake targets and pkg-config files. The technique here, to export your targets and reconstruct a pkg-config file from the exported targets, is really the only current way to do this correctly, as long as there aren't generator expressions besides $<BUILD_INTERFACE:...> and $<INSTALL_INTERFACE:...>Tobitobiah
C
1

edit: off topic, since here, the pc files are generated manually

pkgconfig template files

motivation:

  • CMakeLists.txt should be the single source of truth (name, version)
  • pkgconfig files are about 10 times smaller than cmake files (cmake to pkgconfig is a lossy transformation)

template file: my_package.pc.in

prefix="@CMAKE_INSTALL_PREFIX@"
exec_prefix="${prefix}"
libdir="${prefix}/lib"
includedir="${prefix}/include"

Name: @PROJECT_NAME@
Description: @CMAKE_PROJECT_DESCRIPTION@
Version: @PROJECT_VERSION@
Cflags: -I${includedir}
Libs: -L${libdir} -l@target1@

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)

project(my_library VERSION 1.1.2 LANGUAGES C
  DESCRIPTION "example library")

add_library(my_library src/my_library.c)

# generate pc file for pkg-config
set(target1 my_library)
configure_file(my_package.pc.in
  lib/pkgconfig/my_package.pc @ONLY)

based on: CMake generate pkg-config .pc

related: exporting targets to cmake files

Curagh answered 17/10, 2021 at 17:39 Comment(2)
This indeed generates the .pc file. How can I move this into place with an install clause?Isolative
try install(CODE [[ configure_file(...) ]]) (install code) (bracket strings)Curagh

© 2022 - 2024 — McMap. All rights reserved.