How to generate .pc (pkg-config) file supporting --prefix of the cmake --install?

The typical approach

It seems to me that the common way of generating a .pc (pkg-config) file is to use configure_file command. For example, something like this:

configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/share/pkgconfig/${PROJECT_NAME}.pc.in
    ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc
    @ONLY
)

where the share/pkgconfig/${PROJECT_NAME}.pc.in file might look like this:

prefix=@CMAKE_INSTALL_PREFIX@
includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@/@PROJECT_NAME@

Name: @PROJECT_NAME@
Description: @PROJECT_DESCRIPTION@
URL: @PROJECT_HOMEPAGE_URL@
Version: @PROJECT_VERSION@
Cflags: -I"${includedir}"

Then the .pc file is installed for example like this:

install(
    FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc
    DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig
)

As a side note, I’m not sure if CMAKE_INSTALL_FULL_... variables shouldn’t be used instead due to special cases mentioned by GNUInstallDirs. Yet, while important in general, it is out of scope for this question.

Then, when you generate, build, and install like:

cmake -D "CMAKE_BUILD_TYPE=<config>" -D "CMAKE_INSTALL_PREFIX=<prefix>" -S "<source>" -B "<bin>"
cmake --build "<bin>" --config "<config>"
cmake --install "<bin>" --config "<config>"

it all works perfectly!

The problem

However, the problem with this approach is that it doesn’t work with setting prefix (by the --prefix command-line argument) in the install step above. If I do something like:

cmake --instal "<bin>" --config "<config>" --prefix "<other-prefix>"

all seems to work fine with the exception that the .pc file has a wrong value of the prefix variable. It has the value provided (or defaulted by CMake itself) in the CMAKE_INSTALL_PREFIX variable during the generate step. The value from the --prefix command-line argument is not used since the .pc file was already generated during the generate step!

A possible solution

The approach I found so far is to do configure_file twice. The first run is done during generate step as before, however, it keeps “variable” for the prefix. The second run is done with install(CODE) where the CMAKE_INSTALL_PREFIX has the new value.

So, instead of the configure_file above we would have something like this:

set(DEFERED_CMAKE_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@")
configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/share/pkgconfig/${PROJECT_NAME}.pc.in.in
    ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc.in
    @ONLY
)
install(
    CODE "configure_file(\"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc.in\" \"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc\" @ONLY)"
)

while the share/pkgconfig/${PROJECT_NAME}.pc.in.in file is the same as before with the exception of the first line that now is:

prefix=@DEFERED_CMAKE_INSTALL_PREFIX@
(...)

For the record, a few important points on this solution:

  1. DEFERED_CMAKE_INSTALL_PREFIX is needed because it seems configure_file doesn’t have any way of escaping the @. One could think that using @@CMAKE_INSTALL_PREFIX@@ will do the trick since the first run will change it to @CMAKE_INSTALL_PREFIX@ and the second run will finish off. However, this doesn’t work. Nor does \@. I expect there is no way and we need a custom variable for this.
  2. Two runs are needed since during the second run (in install(CODE)) all the other variables no longer exist. The second run is executed from the cmake_install.cmake.
  3. Quoting of arguments in the CODE argument is needed (unlike with “ordinary CMake”) since the values are replaced during the generate step and the code ending in cmake_install.cmake has just a raw string. Any space in that string breaks the argument from the configure_file point of view.

Another approach

I haven’t tried it, but I expect we could keep the original approach and only add an install(CODE) that would modify the ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc file by rewriting the prefix= line. It could do so either in-place or by making a new file and installing it instead.

However, somehow I like this even less than the above approach. Especially with the in-place modification alternative.

Is there any better approach?

The above approaches are significantly more verbose and not so obvious - at least in my opinion. So, an obvious question is: is there any better solution?

There is CMakePackageConfigHelpers with its configure_package_config_file commnad. The documentation there says:

This has the effect that the resulting FooConfig.cmake file would work poorly under Windows and OSX, where users are used to choose the install location of a binary package at install time, independent from how CMAKE_INSTALL_PREFIX was set at build/cmake time.

which is exactly what I’m trying to address here.

However, the configure_package_config_file command is dedicated for creating package ...Config.cmake files and doesn’t apply to any arbitrary files, like the .pc file here.

So, is there anything else?

Or at least is there a “canonical guideline” on this? Since to my surprise. I didn’t find much on this. As if only I had this problem - so maybe in the end I’m just making this up? Maybe Windows users aren’t using pkg-config that much (even though they could) while *nix users are used to pick the CMAKE_INSTALL_PREFIX in generate step…

If you somehow “make it work” for the install step you will break installing into a staging directory. The pc file should only ever use values defined in the configure/generate step.

I agree with @sera here. Additionally, on Windows, pkg-config behaves differently by automatically picking the prefix from the file location

@sera, @hsattler, I did make it work. It’s only I’m not sure if this is the best way to do it, since it seems somewhat verbose.

@hsattler, I know about that pkg-config behavior. In fact, you can have it on *nix as well by adding --define-prefix command-line argument. (Or block it on Windows by adding --dont-define-prefix.) I regrat that the --define-prefix behavior is not the default one on *nix as well.

@sera, @hsattler, I’m not sure what do you mean by “installing into a staging directory”. It seems to me that “staging” is done by using DESTDIR. (A good read on the topic is also 7.2.4 DESTDIR: Support for Staged Installs.)

Whereas the --prefix command-line argument to install step says:

Override the installation prefix, CMAKE_INSTALL_PREFIX.

So, it doesn’t seem to infer with staging done by DESTDIR. The only difference is that in this “mode” the CMAKE_INSTALL_PREFIX is provided not during generate step but instead during install step.

If that would “break installing” then why do we have the --prefix command-line argument in the install step? Or why the configure_package_config_file command explicitly addresses this issue?

Consider the case where someone creates a tarball archive of your project rather than installing directly from the build tree (e.g.via cmake --install). They might unpack that tarball anywhere, which is both a desirable capability and common practice. In that scenario, you can’t hard-code the absolute path of the install location in the .pc file.

Perhaps a better solution might be to define your prefix=... line in terms of the ${pcfiledir} variable, something like prefix=${pcfiledir}/../... The following link might be helpful:

https://bugs.freedesktop.org/show_bug.cgi?id=62018

2 Likes

To support a “no installation mode” a component must be well-prepared. For example, a component might have a different headers tree for its internal use and restructure (possibly also stripping) it in the installation step. Such a component would be broken by the “no installation mode” and I think I’m OK with that.

As to ${pcfiledir}, I am aware of this variable and how it works. However, it seems to be a kind of workaround for the problem. A good one, but applying only to .pc files.

While the problem of configure_file command fixing to CMAKE_INSTALL_PREFIX from the generation step is a generic problem as shown by existence of the configure_package_config_file command. Although perhaps in practis those are the only two (.pc and ...Config.cmake) typical cases where it matters.

I have been exploring this topic somewhat more. A side finding (besides a typo in DEFERED…) is that

set(DEFERED_CMAKE_INSTALL_PREFIX "@CMAKE_INSTALL_PREFIX@")

is a so-so idea. A much more flexible one is to do it like this:

set(DEFERRED "@")

and then use it like this:

prefix=@DEFERRED@CMAKE_INSTALL_PREFIX@DEFERRED@
(...)

This way we can defer the replacement of any variable by any number of iteration without introducing more and more DEFERRED_... variables.

Of course, it would be better if configure_file could handle it natively.

Further work on this topic indicates some more doubts around GNUInstallDirs interaction with the install command.

It seems the install command prefers that either DESTINATION parameter is a relative path (like CMAKE_INSTALL_<dir> instead of CMAKE_INSTALL_FULL_<dir>) or even TYPE be used instead (that seems in effect do the same).

Relative DESTINATION or TYPE

For a concrete example, if we have CMake code to install the headers of a library:

install(
    DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

or like this:

install(
    DIRECTORY include/
    TYPE INCLUDE
)

the resulting cmake_install.cmake has the following block:

if("x${CMAKE_INSTALL_COMPONENT}x" STREQUAL "xUnspecifiedx" OR NOT CMAKE_INSTALL_COMPONENT)
  file(INSTALL DESTINATION "${CMAKE_INSTALL_PREFIX}/include" TYPE DIRECTORY FILES "<SOURCE-DIR>/include/")
endif()

and the --prefix argument of the install step works as expected.

Absolute DESTINATION (with _FULL_)

However, if our CMake would be doing it like this (use of _FULL_ version):

install(
    DIRECTORY include/
    DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}
)

the resulting cmake_install.cmake has the following:

if("x${CMAKE_INSTALL_COMPONENT}x" STREQUAL "xUnspecifiedx" OR NOT CMAKE_INSTALL_COMPONENT)
  list(APPEND CMAKE_ABSOLUTE_DESTINATION_FILES
   "/usr/include/")
  if(CMAKE_WARN_ON_ABSOLUTE_INSTALL_DESTINATION)
    message(WARNING "ABSOLUTE path INSTALL DESTINATION : ${CMAKE_ABSOLUTE_DESTINATION_FILES}")
  endif()
  if(CMAKE_ERROR_ON_ABSOLUTE_INSTALL_DESTINATION)
    message(FATAL_ERROR "ABSOLUTE path INSTALL DESTINATION forbidden (by caller): ${CMAKE_ABSOLUTE_DESTINATION_FILES}")
  endif()
file(INSTALL DESTINATION "/usr/include" TYPE DIRECTORY FILES "<SOURCE-DIR>/include/")
endif()

In the _FULL_ version there is no ${CMAKE_INSTALL_PREFIX} in the path, instead, there is a raw string. Hence, the --prefix argument of the install step doesn’t work.

Special cases

The include directory in the examples above is simple to understand but less interesting since CMAKE_INSTALL_FULL_INCLUDEDIR (unless overridden by the user) will be always ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}.

However, GNUInstallDirs lists special cases. Depending on what CMAKE_INSTALL_PREFIX is the _FULL_ versions of SYSCONFDIR, LOCALSTATEDIR, and RUNSTATEDIR are different than the above concatenation.

For example, if we use SYSCONFDIR (SYSCONF for TYPE) instead, while also having CMAKE_INSTALL_PREFIX set to /usr, the _FULL_ version ends up installing to /etc whereas the relative version (and the TYPE version) install to ${CMAKE_INSTALL_PREFIX}/etc.

It seems I’m not the only one to notice this issue. For example, we have issue #21150: Extend GNUInstallDirs to preserve the ability to create relocatable packages with EXPORT with a related topic.

CPack

To make things worse, the DESTINATION documentation in install command mentions:

As absolute paths are not supported by cpack installer generators, it is preferable to use relative paths throughout. In particular, there is no need to make paths absolute by prepending CMAKE_INSTALL_PREFIX; this prefix is used by default if the DESTINATION is a relative path.

Conclusions?

It seems there are two ways here:

  1. The way of GNUInstallDirs module
    • Use _FULL_ paths in install command to support special cases of GNUInstallDirs.
    • Use DESRDIR environment variable to make “staged installation”.
      DESTDIR="<install-dir>" cmake --install "<build-dir>" --config "<config>"
      
      or system generic
      cmake -E env DESTDIR="<install-dir>" cmake --install "<build-dir>" --config "<config>"
      
    • Don’t use --prefix argument of the install step as it will not work anyway. Whatever you provided (or defaulted) as CMAKE_INSTALL_PREFIX during configuration lasts forever.
    • You cannot use CPack.
  2. The way of install command
    • Use relative paths in DESTINATION or even TYPE to avoid explicit paths altogether.
    • You still may use DESRDIR environment variable to make “staged installation”, however, keep in mind it is prepended to the CMAKE_INSTALL_PREFIX.
    • You may use --prefix argument of the install step to establish CMAKE_INSTALL_PREFIX at the time of installation (overriding the value from configuration).
    • You can use CPack.

A side note: I’m not sure how universal the DESTDIR is. I expect only Makefiles/Ninja support it or at least not all generators support it. Is there a universal approach?

But what about the configure_file command and .pc files?

Well… It doesn’t fit perfectly in either approach. However, the first is still significantly simpler.

The first thing to notice is that making a .pc file by a call to configure_file doesn’t support the “late binding” of CMAKE_INSTALL_PREFIX from the second approach. To make it work in that scenario we would need to use the tricks I described in the question itself.

The second thing to notice is that making .pc files that are well-behaved still requires extra work even with the first approach. A tempting idea could be to do something like:

prefix=@CMAKE_INSTALL_PREFIX@
includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@
sysconfdir=${prefix}/@CMAKE_INSTALL_SYSCONFDIR@

(...)

However, this would totally defeat the way of GNUInstallDirs module. Not only it would not take into account special cases but it would also ignore user-overridden _FULL_ paths!

A better approach would be to do:

prefix=@CMAKE_INSTALL_PREFIX@
includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@
sysconfdir=@CMAKE_INSTALL_FULL_SYSCONFDIR@

(...)

However, such a file “breaks” the pkg-config --define-variable=<var>=<value> argument. The --define-prefix still works, since it is handled in a special way (and this is why the prefix= variable is needed even if not referred to!). But since the prefix is not referred to explicitly it will not be noticed by --define-variable. Not a big issue - I haven’t yet met a use case for --define-variable… - but still a feature that got broken.

For a perfectly cooperating .pc file we would need extra processing during the generation of the .pc file that based on the _FULL_ value would either use it (if it is not related to prefix) or replace it with ${prefix}/... otherwise. But this calls for a dedicated configure_pc_file that is much “smarter”. And only to cover the exotic --define-variable feature (and perhaps the subjective feel of the elegance of the resulting .pc file).

What else?

The CMAKE_WARN_ON_ABSOLUTE_INSTALL_DESTINATION (and _ERROR_) parts are pretty disturbing. It looks like those warnings do not take into account the use of DESTDIR. Why?

Wouldn’t it be nice if install step had --destdir argument next to (or even instead of? - see below) --prefix argument. It could provide the functionality in a system-independent way (without verbosity of cmake -E) and a generator-independent way.

What is the point of the --prefix argument in the install step? While it does offer reasonable (not sure how useful) functionality it silently breaks other functionalities (like the configure_file referring to install paths, like with .pc file). And from the comments above I assume this functionality should really be used as it may also break other things. Then why was it added in the first place? (While --destdir wasn’t! - see above.)

Could we have something that covers all the cases and isn’t too verbose? I think the [#21150: Extend GNUInstallDirs to preserve the ability to create relocatable packages with EXPORT] asked for the same.

Another thing to take into account is the CMAKE_STAGING_PREFIX variable.

The cmake_install.cmake file starts with a block:

# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
  set(CMAKE_INSTALL_PREFIX "<path>")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")

Now, it seems that the <path> here is set to what CMAKE_INSTALL_PREFIX was upon configuration. Unless CMAKE_STAGING_PREFIX was also defined, in which case that value is used instead.

However, as discussed above, the value of CMAKE_INSTALL_PREFIX within cmake_install.cmake is used only if install commands used relative paths (so, for example, CMAKE_INSTALL_INCLUDEDIR instead of CMAKE_INSTALL_INCLUDEDIR).