Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to auto generate pkgconfig files from cmake targets

I would like to generate pkgconfig files in cmake from the targets. I stared 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?

like image 236
Paul Fultz II Avatar asked May 31 '17 18:05

Paul Fultz II


2 Answers

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.

like image 146
Alex Reinking Avatar answered Nov 04 '22 22:11

Alex Reinking


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

like image 38
Mila Nautikus Avatar answered Nov 04 '22 23:11

Mila Nautikus