Clarification on PUBLIC/PRIVATE with target_source_group

I’ve read over the documentation and a few threads on when and why use PRIVATE over PUBLIC when adding files to a target but I still don’t really get it.

I see that I have a library XYZ and if I say

target_sources(XYZ
    PRIVATE
        foo.cpp
    PUBLIC
       foo.h

that if I have an executable APP target that says target_link_libraries(APP PRIVATE XYZ), that foo.h is inside the APP group on Xcode. If I make both foo.cpp and foo.h PUBLIC, then the APP group has both files in Xcode.

That’s convenient from a coding standpoint, I have all my files in one group but I don’t really see a downside. Is it just the case when you want to build the XYZ library and ship it with a bundle of header files? Or does it actually effect how the executable APP is built?

You pretty much never want a .cpp file to be anything but PRIVATE. The only target that should be compiling the .cpp file is the target you are adding it to (there are exceptions, but those scenarios are exceedingly rare). If you add a .cpp file as PUBLIC, you are saying that it should be compiled into the current target and anything that links to it. Note that this is not talking about linking the object file produced from the .cpp file, we’re talking about compiling it. In your example, foo.cpp will be compiled as part of APP and also as part of XYZ. The object file compiled into APP will be used and the one compiled into XYZ is not used. The one compiled into APP shouldn’t be there, you only want it compiled into XYZ and linking to XYZ should then pick up that one.

For a header, the situation is considerably more involved. It has been common for IDEs to not show a header in its file lists unless the header was added to a target. Thus, it was common to see headers added directly in calls to add_executable() and add_library(). By extension, it made sense to add them using target_sources() too, but then you had to decide if PRIVATE, PUBLIC or INTERFACE was the most appropriate. Since headers don’t get compiled directly, the only real difference used to be that these keywords controlled which target(s) that file would appear under in IDEs. The usual way this played out was that a header-only library would get represented as an interface library, which in earlier versions of CMake meant you had no choice but to add headers as INTERFACE. CMake 3.19 added the ability to add headers to interface libraries as PRIVATE (or equivalently to list them in the call to add_library()), which allowed the header to be seen as part of that target in IDEs instead of the target’s consumers. For targets that were not interface libraries, headers would normally be added as PRIVATE so that those headers were associated with that target in IDE file lists.

CMake 3.23 is where things got much more interesting. That release added file sets, which fundamentally changed the recommended way to add headers to your project. Now the PRIVATE, PUBLIC and INTERFACE keywords have different impacts when used for a file set, affecting not just IDEs but also things like header search paths, installation of the headers, and verification of whether headers are self-contained. File sets brought much more powerful capabilities and meaning to headers and how they are used in a project and by consumers of that project. Fundamentally though, the high level meaning of PRIVATE, PUBLIC and INTERFACE still means something similar when defining a file set with target_sources(). PRIVATE means “only used when building this target”, INTERFACE means “only used by things that consume this target, not the target itself”, and PUBLIC merges both of those together. Thus, when you define a headers file set, you need to consider whether the headers are part of that target’s public API or not. Typically, I recommend defining two separate file sets for a target, one to contain the public headers (I often call that header set “api”) and another to contain the private headers (which I typically just call “private”). IDEs should associate headers specified in a file set with the target they are being added to by the target_sources() call in all cases, but I don’t know if they will still use the old behavior of showing non-private headers under consumers of the target.

Probably not quite what you were expecting, but hopefully that fills out a bit more of the landscape for you to explore.

2 Likes

No, @craig.scott , not what I was expecting but totally an answer I was hoping for. Thanks for such a thorough explanation.
The discrepancy in how PUBLIC/PRIVATE effects compilation was exactly the type of thing I wanted to be sure I WASN’T doing so it’s good to know that I should, at the very least, go back to my original paradigm of public headers and private .cpp’s.

I’ll start looking into file sets and see if that helps with anything. I’d really like the generated Xcode and VS projects to resemble what the developers would have setup themselves so that it’s as invisible as possible.

I’ve found that the following will successfully reorganize all the code from my library target (XYZ) into the original folder structure in the IDE, so that’s really helpful.

# inside the XYZ CMakeLists.txt
get_target_property(TARGET_SOURCES XYZ SOURCES)
source_group(TREE ${CMAKE_SOURCE_DIR} FILES ${TARGET_SOURCES})

The issue I’m still trying to solve, if you have any ideas, is removing files from the IDE that are shown redundantly.

If there was a way to say something like the pseudocode below, it would be nice to hide the library code inside the APP target because it’s already in the library target in the IDE.

#inside the APP CMakeLists.txt
get_target_property(TARGET_SOURCES XYZ SOURCES)
source_group(<HIDE> FILES ${TARGET_SOURCES})

(Hope that makes sense)

That sounds to me like you still have something PUBLIC which should instead be PRIVATE.

I’ve got it all looking correct in Xcode now and the tests compile and pass so I think I’m good to go.
I needed to target all the .h files under PRIVATE as well.

Thanks again @craig.scott for all the help and feedback