Installing headers the modern way, regurgitated and revisited

Some weeks ago, under Installing headers the modern way, I posed a question regarding the relationship (or lack thereof) between target_sources(mylib PUBLIC ...) and install(TARGETS mylib PUBLIC_HEADER ...). This is a part of CMake’s current design/evolution that just doesn’t seem especially cohesive.

Well, I recently undertook writing/generating my first CMake package config file. And while I’m at it, hey, let’s make this thing relocatable, too.

Oh, boy.

So, let’s say we have:

target_sources(
  mylib
  PUBLIC
    myfirstpublicheader.h
    mysecondpublicheader.h
)

Well, that just won’t do. To satisfy relocatability requirements, the $<INSTALL_INTERFACE:> generator expression must be used. For each header.

target_sources(
  mylib
  PUBLIC
    $<INSTALL_INTERFACE:myfirstpublicheader.h>
    $<INSTALL_INTERFACE:mysecondpublicheader.h>
)

Um, ew. But okay.

Oh, wait… that little bit of code I used to copy the INTERFACE_SOURCES property to the PUBLIC_HEADER property?

get_target_property(MYLIB_PUBLIC_HEADERS mylib INTERFACE_SOURCES)
set_target_properties(
  mylib
  PROPERTIES
    PUBLIC_HEADER "${MYLIB_PUBLIC_HEADERS}"
)

Well, after decorating things with $<INSTALL_INTERFACE:>, my headers aren’t getting installed anymore.

Hm. Okay. Well, it seems that has a perfectly trivial solution: just use the $<BUILD_INTERFACE:> generator expression:

target_sources(
  mylib
  PUBLIC
    $<INSTALL_INTERFACE:myfirstpublicheader.h>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/myfirstpublicheader.h>
    $<INSTALL_INTERFACE:mysecondpublicheader.h>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/mysecondpublicheader.h>
)

And now my headers install! Yay!

Oh, dear. What have I done? See, I don’t have just a couple of public headers like this contrived example code; I have tens of them, like many, many other moderately-sized C++ projects. And this thing has just exploded.

I am hoping that someone will tell me that I’ve missed a trick here. But failing that, I respectfully suggest that something has gone off the rails with this design.

2 Likes

My 2 cents sharing my experiences/test (with the help of Sarcasm)
disclaimer: tests was performed during October 2018 and I didn’t check if CMake/QtCreator change their behaviour
disclaimer 2: I don’t think it is a necro posting since it can be seen as a follow up of the author investigation but done few years before ^^;

TLDR: Don’t use “PUBLIC” in target source it will pollute your IDE project layout (for good reason)

The main problem with this solution is myfirstpublicheader.h and mysecondpublicheader.h will now be part of any dependent target sources.

e.g.
Suppose I have an App depending on A and B libraries.
B/CMakeLists.txt:

...
target_sources(B
  PRIVATE
    "src/B.cc"
  PUBLIC
    $<BUILD_INTERFACE:"${CMAKE_CURRENT_SOURCE_DIR}/include/b/B.h">
    $<INSTALL_INTERFACE:"include/b/B.h">)
...
set_target_properties(B PROPERTIES
  PUBLIC_HEADER "include/b/B.h")

But for A I’ve used:
A/CMakeLists.txt:

target_sources(A
  PRIVATE
    "include/a/A.h"
    "src/A.cc"
  )
set_target_properties(A PROPERTIES
  PUBLIC_HEADER "include/a/A.h")

and finally
App/CMakelists.txt:

target_link_libraries(App PRIVATE A B)

note: You can found the full project here: GitHub - Mizux/target_sources: Test target_source(tgt PUBLIC ...) and PUBLIC_HEADER

so here B is like your proposal/investigation unfortunately this will add the source file b/B.h as a source of App, fortunately it is a header so it will be ignored by the compiler but it is not what you want.
Also in ide (QtCreator you’ll see B.h duplicate everywhere in the project layout see: target_sources/qtcreator.png at master · Mizux/target_sources · GitHub)

I guess, PUBLIC source is only needed when you need to build several time the same .cc BUT with options/definition provided by the dependent target itself…
e.g. you have AppFoo executable which need B.cc compiled with define FOO, while you have AppBar executable which need B.cc to be compiled with define BAR in this case you want to inject the B.cc source in each executable source list so it will be compiled with the correct define but it’s a very “niche” scenario IMHO…

so it will be better to use:

set(MYLIB_HDRS
myfirstpublicheader.h
mysecondpublicheader.h
)

target_sources(mylib PRIVATE
 myprivatesrc.cc
 ${MYLIB_HRS}
)
set_property(TARGET mylib
 PROPERTY PUBLIC_HEADER ${MYLIB_HDRS})

note: here we need to use set_property(TARGET) since it allow multiple values [key, values…] (aka list of headers file) while set_target_property() while being more “modern” only support pairs [key, value]
note: I don’t know/test with PRIVATE and BUILD_INTERFACE/INSTALL_INTERFACE and use your get_target_property() trick instead.

For digging deeper, I really enjoy you to read the issue #1 open by Sarcasm (github discussion was not a thing at this time) and all the links provided…

1 Like

From CMake 3.23 version target_source contain FILE_SET option. Now you can define your headers in one place and automatically use them on an install operation. Even include_directories it’s not any more necessary. This gives more improvements and can be used for modules export, for details read the documentation, install section also.

The above example can be written like this:

target_sources(
  mylib
  PUBLIC
    FILE_SET public-headers
    TYPE HEADERS
    FILES
      myfirstpublicheader.h
      mysecondpublicheader.h
)

install(TARGETS mylib FILE_SET public-headers)
5 Likes

Does @mherok’s reply still remain the best way to go about this?

In any case, I had to update his code a bit with CMake 3.26. Here’s what seems to work well:

cmake_minimum_required(VERSION 3.23...3.26)

# ...

target_sources(${PROJECT_NAME} PUBLIC
    FILE_SET public_headers
    TYPE HEADERS
    BASE_DIRS include
    FILES include/foobar.h)

include(GNUInstallDirs)
install(TARGETS ${PROJECT_NAME} FILE_SET public_headers)
2 Likes

its getting easier to get somewhat modern cmake , but well it still does bad stuff…

installing the headers, yes, but when exporting targets and the cmake packages still hardcodes the path of the headers, so i guess it still needs some generator expressions in there somehow…

its a full time job to get the patterns right of somewhat modern cmake, worst part is that cmake still the best solution, but its far from perfect…

so far i still use good old fashioned manual install of the headers, so the target files does not hard code paths. now i will try to use FILE_SET in a real project

one day someone might figure out how all this should work without 45 workarounds.

edit: and of course i succeeded with looking in the wrong file… of course the build tree contains the hardcoded paths, the installed files actually looks correct.

from the looks of it cmake 3.23+ might actually be getting into a good place.