6

I am having a hard time figuring out how to install PUBLIC headers specified in target_sources().

It appears that target_sources() is somewhat of a mystery for anything other than adding private sources to an executable. After reading a lot of material, where especially this blog entry was helpful, I managed to understand & bypass the issue with target_sources() and PUBLIC files. A CMakeLists.txt in one of the many subdirectories of my C++ library project looks like this:

target_sources(mylib
    PRIVATE
        foo.cpp

    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/foo.hpp>
        $<INSTALL_INTERFACE:foo.hpp>
)

This allows me to successfully build the library. However, upon installation, the header file(s) listed in the PUBLIC section of target_sources() are never installed.

My installation looks like this:

install(
    TARGETS
        mylib
    EXPORT mylib-targets
    LIBRARY
        DESTINATION ${CMAKE_INSTALL_LIBDIR}
        COMPONENT lib
    ARCHIVE
        DESTINATION ${CMAKE_INSTALL_LIBDIR}
        COMPONENT lib
    RUNTIME
        DESTINATION ${CMAKE_INSTALL_BINDIR}
        COMPONENT bin
    INCLUDES
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mylib

However, this doesn't install any of the headers. This stackoverflow answer mentions the use of PUBLIC_HEADER but reading the documentation doesn't give me the feeling that that is relevant in my case.

Question: What is the proper way of installing PUBLIC or INTERFACE headers using install()?

Background: My goal is to have a separate CMakeLists.txt in every subdirectory of my source tree. Each of these lists is supposed to use target_sources() to add PRIVATE and PUBLIC files to the build. All PUBLIC (and INTERFACE) sources should be installed during installation.

Joel Bodenmann
  • 2,152
  • 2
  • 17
  • 44
  • So did you end up adding `set_target_properties` and `target_sources` to each of your subdirectories `CMakeLists.txt`? – WBuck Mar 03 '21 at 14:45
  • 1
    Yes. Everything is now done using the various `target()` functions in the subdirectories. However, public headers are added to a list named `HEADERS_PUBLIC`, which is then passed to `target_sources()` but also to the corresponding installation directive. I can publish a blog post on this with a skeleton if that would help you out. Might take a few days tho. – Joel Bodenmann Mar 03 '21 at 16:40
  • No, that's alright. Thanks for the offer though. I believe I have it working now; although I'm not using the `install` command within the subdirectories `CMakeLists.txt` (which from your comment it sounds like that's what you're doing). – WBuck Mar 03 '21 at 16:57

1 Answers1

5

PUBLIC section of target_sources command has nothing common with public headers installed with PUBLIC_HEADER option of install command.

The headers you want to treat as public for your library should be listed in PUBLIC_HEADER property for the target.

Documentation for install(TARGETS) has a nice example of setting and installing private headers. In your case this would be:

# This defines `foo.hpp` located in the current source directory as a 'public header'.
set_target_properties(mylib PROPERTIES PUBLIC_HEADER foo.hpp)
# This install public headers among with the library.
install(
  TARGETS
    mylib
  PUBLIC_HEADER
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mylib
  # ... other options
)

See also that related question: How to configure CMakeLists.txt to install public headers of a shared library?.


Note also, that INCLUDES and PUBLIC_HEADERS clauses for install(TARGETS) command are different things:

  • INCLUDES specifies include directory for the installed library
  • PUBLIC_HEADERS specifies directory where all target's public headers will be copied upon installation.

E.g. if you want your header file to be used via

#include <mylib/foo.h>

then you need provide following options for install() command:

  PUBLIC_HEADER
    # The header will be places at ${CMAKE_INSTALL_INCLUDEDIR}/mylib/foo.h
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mylib
  INCLUDES
    # This directory will be used as include directory.
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}

So <include-directory> joined (via /) with the relative path in #include<> directive will give absolute path to the header file.

Tsyvarev
  • 60,011
  • 17
  • 110
  • 153
  • The `PUBLIC_HEADER` documentation always gives me the sensation that this is a macOS/iOS only feature. Given your post I assume that this is incorrect? – Joel Bodenmann Mar 18 '20 at 09:59
  • 1
    I would interpret this property as: "This property is **intended** for macOS/iOS, but is **usable** for other platforms too". See the phrase "On non-Apple platforms these headers may be installed using the `PUBLIC_HEADER` option to the `install(TARGETS)` command." at the end of the description of this property in the [documentation](https://cmake.org/cmake/help/v3.16/prop_tgt/PUBLIC_HEADER.html). – Tsyvarev Mar 18 '20 at 10:03
  • 1
    My verdict after all of this is: `target_sources()` has absolutely no effect when using `install(TARGETS)`. Therefore, when trying to have a separate `CMakeLists.txt` in each of my library's sub-directories, I just use the "old" method of adding each source file's path (relative to the top-level `CMakeLists.txt`) to a variable (list) and then use `install(FILES)`. Is that correct? All I was looking for is a way of having a `CMakeLists.txt` in each sub-directory listing that directory's source files relatively to that `CMakeLists.txt` while preserving the directory structure when installing. – Joel Bodenmann Mar 18 '20 at 11:29
  • Yes, on non-Apple platforms `PUBLIC_HEADER` has no advantage over simple `install(FILES)`. As for `target_sources`, I find it useful only for `INTERFACE` targets. For normal libraries it is simpler to list its sources directly in `add_library` call. – Tsyvarev Mar 18 '20 at 13:56