CMake target_link_libraries() scopes
The CMake’s target_link_libraries() function has different scopes (PRIVATE
/INTERFACE
/PUBLIC
), and I never understood what exactly each one of them means and how do they actually affect the final result.
As there is a limit for how long one can postpone one’s ignorance, it finally came a time for me to investigate the matter, which I did by (reading the documentation and) conducting a small experiment of my own.
Theory
If anything, I am now on CMake v3.26.4, which might be important to state, as (much) older versions had different policies for things like include directories propagation, and newer versions might have something different too.
What are these scopes
Before reading my interpretation, do read the documentation and this post from Craig Scott (author of Professional CMake).
As I understood it, scopes define how dependencies of a library will affect consumers of that library. So if you only deal with direct dependencies and don’t have consumers upper in the dependency chain, then you probably don’t need to care about all that, as this is really about transitive dependencies (dependencies of dependencies).
For example, let’s say we have a Project, which is a collection of libraries. This Project is used by some Tool (so Tool depends on Project) and also it has a dependency of its own (so Project depends on Dependency). In this case that Dependency library will be a direct dependency for our Project and a transitive dependency for the Tool:
Applying to this example, my understanding of the scopes would be the following:
-
PRIVATE - the Tool gets
doThingy()
functionality of Dependency throughdoTheThing()
function of AnotherLibrary and cannot getdoThingy()
function directly from Dependency: -
INTERFACE - the Tool gets
doThingy()
function directly from Dependency, while AnotherLibrary does not (and so it no longer hasdoTheThing()
function available): -
PUBLIC - the Tool can do both: get
doThingy()
functionality of Dependency throughdoTheThing()
function of AnotherLibrary and getdoThingy()
function directly from Dependency:
While PRIVATE
and PUBLIC
scopes make sense to me, I cannot think of a real-world example of INTERFACE
linkage. Why would a library A link to library B only to make B available higher in the dependency chain, so without using B’s functionality itself?
The INTERFACE
scope would probably make sense for a so-called “header-only” library (which has no sources of its own, only headers), when it provides linking to other libraries without using them itself. But I myself never needed to create such a thing, so I’m still puzzled about the actual use of the INTERFACE
scope.
Which scope is the best
I saw this question being asked in a couple of places: either “which one is the best” or “which one should I use”. I wouldn’t say that any of the scopes is better or worse than another, it’s just different scopes for different purposes.
If we take one of the most common dependencies, such as zlib, then applying to the same example from above, if the Tool has no intention of using any of zlib’s functionality on its own, as it only needs what AnotherLibrary provides in its doTheThing()
function, then one should choose PRIVATE
scope when linking to zlib from AnotherLibrary.
However, if, in addition to whatever doTheThing()
does, Tool also needs to perform some compression/decompression operations of its own, then it indeed would require access to zlib’s API too. So now it’s both AnotherLibrary and Tool who should have access to zlib functions, and in that case one should choose PUBLIC
scope when linking to zlib from AnotherLibrary. Although I don’t quite understand why wouldn’t Tool just find_package()
zlib and link to it itself then, so AnotherLibrary could keep its linking to zlib as PRIVATE
. This would also make the Tool’s dependency on zlib more clear/visible in its CMakeLists.txt
.
If after reading the last sentence you were going to say “but in that case Tool will need to have zlib binary for linking, while when it gets zlib through AnotherLibrary’s private linking it won’t”, then I have some news for you.
A silly analogy
If previous sections didn’t add much clarity, let’s try the following silly analogy.
Say, you’d like to steal a game. Such a process usually involves getting the base game and finding a crack for it (modified executable or/and accompanying files). In general, the latter is provided by crackers, and the former is provided by repackers, who incorporate the crack into the base game and distribute the result.
To make the analogy fit a bit better, let’s assume that you don’t know the original cracker website (it frequently gets shut down and thus moves around a lot), so you cannot download the crack yourself, only repacker always knows how to get it.
The scopes in this case will be the following:
- PRIVATE - repacker takes the original crack, applies it to the base game, compresses everything one hundred times into an archive, compresses that archive one thousand times more and makes an installer out of it (which of course will require Administrator elevation on your machine) with distorted pictures, shitty music and some advertisement banners. Of course, he doesn’t provide any links/credits to the original cracker;
- this option might seem to be the worst, but most of the time you don’t have any other way to get the base game in order to apply the crack yourself (which you also don’t have);
- INTERFACE - repacker doesn’t do anything, he doesn’t even re-upload the original crack, he just posts a link to the original cracker release page, where you can download the crack from;
- assuming that you somehow already have the base game, here the
INTERFACE
scope actually makes sense;
- assuming that you somehow already have the base game, here the
- PUBLIC - repacker still does all that horrible shit as in
PRIVATE
variant, but this time he also provides a link to the original cracker release page, so if you already have the base game, you might prefer to download just the crack from its origin and apply it to the base game yourself, instead of using repacker’s garbage installer.
As you can see, the analogy is very silly indeed and not entirely correct too, so don’t hang to it too much. I almost regret adding it to the article.
Practical example
That trio of projects from above (Dependency, Project and Tool) is what I used in this repository for experimenting with target_link_libraries()
scopes to see what exactly do they do in practice.
Once again, it is 3 different entities:
- Dependency - a library, that is a direct dependency for Project (and a transitive dependency for Tool);
- Project - a set of libraries, one of which depends on Dependency;
- Tool - a CLI application that depends on libraries from Project.
This is a simplified variation of what we actually have in our project at work (as shown on the main picture): we develop an SDK or rather a framework (Project), which has several 3rd-party dependencies (Dependency), and we deliver that SDK to our customers, who in turn use it to build their own applications (Tool). So our direct dependencies are transitive dependencies for our customers.
The Dependency is a library called Thingy
, and it is built the same way for all the cases, as nothing changes for it:
$ cd /path/to/cmake-target-link-libraries-example/dpndnc
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" ..
$ cmake --build . --target install
$ tree ../install/
├── include
│ └── Thingy
│ └── thingy.h
├── lib
│ └── libThingy.a
└── share
└── Thingy
├── ThingyConfig.cmake
├── ThingyConfigVersion.cmake
├── ThingyTargets-release.cmake
└── ThingyTargets.cmake
Now let’s see how different scopes of linking to it from Project will affect Tool.
STATIC libraries
Here the Project libraries are built as STATIC
libraries.
PRIVATE
Linking to Thingy
library with PRIVATE
scope:
find_package(Thingy CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME}
PRIVATE
dpndnc::Thingy
)
Configure and build the project:
$ cd /path/to/cmake-target-link-libraries-example/prjct
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/dpndnc/install" \
..
$ cmake --build . --target install
Let’s see what we have inside install/share/AnotherLibrary/AnotherLibraryTargets.cmake
:
# Create imported target prjct::AnotherLibrary
add_library(prjct::AnotherLibrary STATIC IMPORTED)
set_target_properties(prjct::AnotherLibrary PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include;${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "\$<LINK_ONLY:dpndnc::Thingy>"
)
Okay, so INTERFACE_LINK_LIBRARIES
property got dpndnc::Thingy
linking target. That looks concerning already, because we chose PRIVATE
scope, so linking to Thingy
should have ended with AnotherLibrary
and should have not propagated upper in the dependency chain, right? That we will find out soon enough.
But first let’s check what symbols are available in the resulting binary (here I’m using nm
instrument on Mac OS, on other platforms it might be something else):
$ nm --demangle ../install/lib/libAnotherLibrary.a
another.cpp.o:
000000000000038c s GCC_except_table0
00000000000003a0 s GCC_except_table3
00000000000003e4 s GCC_except_table4
0000000000000400 s __GLOBAL__sub_I_another.cpp
U __Unwind_Resume
0000000000000638 b anotherString
00000000000000d0 T prjct::lbrAnother::doTheThing()
0000000000000000 T prjct::lbrAnother::doAnother()
U dpndnc::doThingy()
...
As you can see, it contains prjct::lbrAnother::doAnother()
and prjct::lbrAnother::doTheThing()
own functions, but also there is dpndnc::doThingy()
function from Thingy
.
Now let’s try to configure the Tool. It has the following in its CMakeLists.txt:
find_package(SomeLibrary CONFIG REQUIRED)
find_package(AnotherLibrary CONFIG REQUIRED)
target_link_libraries(${CMAKE_PROJECT_NAME}
PRIVATE
prjct::SomeLibrary
prjct::AnotherLibrary
)
so everything should be fine:
$ cd /path/to/cmake-target-link-libraries-example/tl
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/prjct/install" \
..
but it isn’t:
CMake Error at /path/to/cmake-target-link-libraries-example/prjct/install/share/AnotherLibrary/AnotherLibraryTargets.cmake:60 (set_target_properties):
The link interface of target "prjct::AnotherLibrary" contains:
dpndnc::Thingy
but the target was not found. Possible reasons include:
* There is a typo in the target name.
* A find_package call is missing for an IMPORTED target.
* An ALIAS target is missing.
That’s strange, but let’s add find_package()
:
find_package(Thingy CONFIG REQUIRED)
find_package(SomeLibrary CONFIG REQUIRED)
find_package(AnotherLibrary CONFIG REQUIRED)
and try again:
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/prjct/install" \
..
but that will fail too:
CMake Error at CMakeLists.txt:50 (find_package):
Could not find a package configuration file provided by "Thingy" with any
of the following names:
ThingyConfig.cmake
thingy-config.cmake
Add the installation prefix of "Thingy" to CMAKE_PREFIX_PATH or set
"Thingy_DIR" to a directory containing one of the above files. If "Thingy"
provides a separate development package or SDK, be sure it has been
installed.
Well, fuck. So it wants /path/to/cmake-target-link-libraries-example/dpndnc/install
to be also present in CMAKE_PREFIX_PATH
. But again, shouldn’t have all traces to Thingy
dependency “disappeared” after AnotherLibrary
was built, as it’s a STATIC
library and it links to Thingy
with PRIVATE
scope?
Turns out, they should have not. I already mentioned my ignorance, and that’s another evidence to it: I did not know that static libraries do not “bundle” their dependencies into their resulting binary. Moreover, static libraries are not really libraries, they are kind of archives of object files, which are what actually contains the symbols (I’d recommend you to read some more about this, as my understanding is still a bit vague). So AnotherLibrary
doesn’t not “contain” Thingy
functions, and so for our customer to be able to build his Tool application we need to deliver both our Project package and the Thingy
package (otherwise he will have to get it from somewhere else himself).
That is the main reason why I wanted to research this topic and make this experiment. Our project has a lot of 3rd-party dependencies, and I was naively hoping that if we just set PRIVATE
scope on linking to them from our SDK, then we won’t need to distribute these dependencies packages in addition to our main package. Well, tough titties, looks like we’ll have to anyway.
Okay then, let’s add the missing path to CMAKE_PREFIX_PATH
and finally build the application:
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/prjct/install;/path/to/cmake-target-link-libraries-example/dpndnc/install" \
..
$ cmake --build . --target install
While we are here, it is annoying (and error-prone) to add find_package()
for every single transitive dependency, so instead you (as maintainer of your project) should use find_dependency() macro in AnotherLibrary
’s CMake config like so:
include(CMakeFindDependencyMacro)
# ...keeping in mind, that this is not needed
# when AnotherLibrary is SHARED and links to Thingy with PRIVATE scope
# (there will be more details about that later)
find_dependency(Thingy CONFIG REQUIRED)
Having that (and having re-build/re-installed the Project), we can now delete find_package(Thingy CONFIG REQUIRED)
from the Tool project.
Now let’s inspect the resulting Tool binary:
$ nm --demangle ../install/bin/some-tool
0000000100003de0 s GCC_except_table0
0000000100003e78 s GCC_except_table0
0000000100003e8c s GCC_except_table0
0000000100003ea0 s GCC_except_table0
0000000100003e20 s GCC_except_table1
0000000100003e64 s GCC_except_table2
0000000100003c10 t __GLOBAL__sub_I_another.cpp
0000000100003ae0 t __GLOBAL__sub_I_some.cpp
0000000100003d30 t __GLOBAL__sub_I_thingy.cpp
U __Unwind_Resume
0000000100008000 b someString
0000000100008030 b thingyString
0000000100008018 b anotherString
0000000100003c00 T prjct::lbrAnother::doTheThing()
0000000100003b30 T prjct::lbrAnother::doAnother()
0000000100003a10 T prjct::lbrSome::doSome()
0000000100003c60 T dpndnc::doThingy()
...
It has all the symbols from the Project’s libraries: AnotherLibrary
and SomeLibrary
, and we can call these functions:
#include <SomeLibrary/some.h>
#include <AnotherLibrary/another.h>
std::cout << "Direct dependencies:" << std::endl;
prjct::lbrSome::doSome();
prjct::lbrAnother::doAnother();
std::cout << "Transitive dependency:" << std::endl;
std::cout << "via AnotherLibrary | ";
prjct::lbrAnother::doTheThing();
But nm
also says that the Tool binary has dpndnc::doThingy()
symbol, which comes from the Thingy
library. However, if we’ll try to call that function:
std::cout << "directly | ";
dpndnc::doThingy();
It will first fail with undeclared identifier:
/path/to/cmake-target-link-libraries-example/tl/src/main.cpp:27:5: error: use of undeclared identifier 'dpndnc'
dpndnc::doThingy();
^
1 error generated.
and if you’ll try to add #include <Thingy/thingy.h>
, then it will fail with missing header:
/path/to/cmake-target-link-libraries-example/tl/src/main.cpp:7:14: fatal error: 'Thingy/thingy.h' file not found
#include <Thingy/thingy.h>
^~~~~~~~~~~~~~~~~
1 error generated.
You can of course copy Thingy/thingy.h
from Thingy
installation folder so it becomes discoverable for the Tool project (for instance, in Project installation folder), and then you will in fact be able to call that function: the project will compile and will run just fine; but that is not how it was indented to work. The point of this stunt is to demonstrate that this is what the scopes are really about: discoverability of a library’s public headers. Or so I understood it.
If you are curious enough, you can also take a look an Ninja’s build commands in /path/to/cmake-target-link-libraries-example/tl/build/build.ninja
:
build CMakeFiles/some-tool.dir/src/main.cpp.o:
...
INCLUDES = -isystem /path/to/cmake-target-link-libraries-example/prjct/install/include
...
build some-tool:
...
LINK_LIBRARIES = /path/to/cmake-target-link-libraries-example/prjct/install/lib/libSomeLibrary.a /path/to/cmake-target-link-libraries-example/prjct/install/lib/libAnotherLibrary.a /path/to/cmake-target-link-libraries-example/dpndnc/install/lib/libThingy.a
...
So indeed, it links to all 3 of them: libSomeLibrary.a
, libAnotherLibrary.a
(these two are part of the Project) and libThingy.a
(this one is part of the Thingy
), even though the latter is a PRIVATE
dependency of the Project. Thus, the only(?) effect that PRIVATE
scope actually has here is that Thingy
’s public headers are not added to the Tool’s INCLUDES
; but as you saw a bit earlier, manually making these headers available lets the Tool discover dpndnc::doThingy()
symbol and still call that function with no problems, as the the library is linked to anyway. So it really is about the headers, isn’t it?
As a side note, that discovery made me suspect that we might be doing something stupid when we merge our SDK installation and our 3rd-party dependencies installations together into a single “bundle” with bin
/include
/lib
/share
folders structure (for the delivery convenience), because that roally fucks the PRIVATE
scope of our linking to them, as all the public headers become discoverable for consuming projects, don’t they. Goddamn.
INTERFACE
Let’s see what changes with INTERFACE
scope:
find_package(Thingy CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME}
INTERFACE
dpndnc::Thingy
)
Configure the project:
$ cd /path/to/cmake-target-link-libraries-example/prjct
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/dpndnc/install" \
..
If we now try to build the project without changing anything in its sources, it will fail:
/path/to/cmake-target-link-libraries-example/prjct/libraries/AnotherLibrary/src/another.cpp:4:14: fatal error: 'Thingy/thingy.h' file not found
#include <Thingy/thingy.h>
^~~~~~~~~~~~~~~~~
1 error generated.
That’s INTERFACE
scope in action: even though AnotherLibrary
finds Thingy
package and links to its library, that library’s functionality is not available in the project itself. But to be precise, it’s only headers that are not available, so if you’ll copy Thingy/thingy.h
into AnotherLibrary
’s include
or make it discoverable in some other way, then the project will build fine, the symbols inside libAnotherLibrary.a
will be the same as with PRIVATE scope, and then the Tool will also be able to call doTheThing()
. So once again, the only(?) effect the scopes have is the headers discoverability for targets.
But anyway, as we are not going to manually make Thingy
headers available for AnotherLibrary
against the INTERFACE
scope wishes, we’ll need to comment out (or wrap into #ifdef
) the following lines:
//#include <Thingy/thingy.h>
//void doTheThing()
//{
// dpndnc::doThingy();
//}
Then the project will build and install:
$ cmake --build . --target install
Here’s what is inside install/share/AnotherLibrary/AnotherLibraryTargets.cmake
this time:
# Create imported target prjct::AnotherLibrary
add_library(prjct::AnotherLibrary STATIC IMPORTED)
set_target_properties(prjct::AnotherLibrary PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include;${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "dpndnc::Thingy"
)
Now there is no LINK_ONLY generator expression, which apparently means that this time projects higher in the dependency chain not only will link to Thingy
library binary but will also get its public headers.
And here are the symbols that are available in the resulting libAnotherLibrary.a
:
$ nm --demangle ../install/lib/libAnotherLibrary.a
another.cpp.o:
000000000000037c s GCC_except_table0
0000000000000390 s GCC_except_table2
00000000000003d4 s GCC_except_table3
00000000000003f0 s __GLOBAL__sub_I_another.cpp
U __Unwind_Resume
00000000000005e0 b anotherString
0000000000000000 T prjct::lbrAnother::doAnother()
...
Quite naturally there is no prjct::lbrAnother::doTheThing()
anymore, as we commented it out, but there is also no dpndnc::doThingy()
, so AnotherLibrary
really is just passing Thingy
library linking interface without using the library itself.
If we now try to configure the Tool project, it will fail the same way (first not finding dpndnc::Thingy
target and then not finding ThingyConfig.cmake
), so let’s configure it with everything provided in the CMAKE_PREFIX_PATH
right away:
$ cd /path/to/cmake-target-link-libraries-example/tl
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/prjct/install;/path/to/cmake-target-link-libraries-example/dpndnc/install" \
..
$ cmake --build . --target install
What symbols does this variant have:
$ nm --demangle ../install/bin/some-tool
0000000100003df0 s GCC_except_table0
0000000100003e88 s GCC_except_table0
0000000100003e9c s GCC_except_table0
0000000100003eb0 s GCC_except_table0
0000000100003e30 s GCC_except_table1
0000000100003e74 s GCC_except_table2
0000000100003c20 t __GLOBAL__sub_I_another.cpp
0000000100003b00 t __GLOBAL__sub_I_some.cpp
0000000100003d40 t __GLOBAL__sub_I_thingy.cpp
U __Unwind_Resume
0000000100008000 b someString
0000000100008030 b thingyString
0000000100008018 b anotherString
0000000100003b50 T prjct::lbrAnother::doAnother()
0000000100003a30 T prjct::lbrSome::doSome()
0000000100003c70 T dpndnc::doThingy()
...
This time there is no prjct::lbrAnother::doTheThing()
, but there is still dpndnc::doThingy()
. What’s also different is that Thingy
’s public headers are discoverable now, which means that we have the following set of functions available for Tool to use:
#include <SomeLibrary/some.h>
#include <AnotherLibrary/another.h>
#include <Thingy/thingy.h>
std::cout << "Direct dependencies:" << std::endl;
prjct::lbrSome::doSome();
prjct::lbrAnother::doAnother();
std::cout << std::endl;
std::cout << "Transitive dependency:" << std::endl;
//
// trying to use that will fail to build with
// error: no member named 'doTheThing' in namespace 'prjct::lbrAnother'
//std::cout << "via AnotherLibrary | ";
//prjct::lbrAnother::doTheThing();
//
// but that one now works
std::cout << "directly | ";
dpndnc::doThingy();
If you are still curious, there is something different in /path/to/cmake-target-link-libraries-example/tl/build/build.ninja
too:
build CMakeFiles/some-tool.dir/src/main.cpp.o:
...
INCLUDES = -isystem /path/to/cmake-target-link-libraries-example/prjct/install/include -isystem /path/to/cmake-target-link-libraries-example/dpndnc/install/include
...
build some-tool:
...
LINK_LIBRARIES = /path/to/cmake-target-link-libraries-example/prjct/install/lib/libSomeLibrary.a /path/to/cmake-target-link-libraries-example/prjct/install/lib/libAnotherLibrary.a /path/to/cmake-target-link-libraries-example/dpndnc/install/lib/libThingy.a
...
The set of linking libraries is the same, but INCLUDES
now contains the path to Thingy
’s headers too.
PUBLIC
Finally, the PUBLIC
scope:
find_package(Thingy CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME}
PUBLIC
dpndnc::Thingy
)
Configure and build:
$ cd /path/to/cmake-target-link-libraries-example/prjct
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/dpndnc/install" \
..
$ cmake --build . --target install
Here everything works the same way as with PRIVATE scope, so AnotherLibrary
can call doThingy()
function from Thingy
library:
#include <Thingy/thingy.h>
void doTheThing()
{
dpndnc::doThingy();
}
But install/share/AnotherLibrary/AnotherLibraryTargets.cmake
is the same as with INTERFACE scope:
# Create imported target prjct::AnotherLibrary
add_library(prjct::AnotherLibrary STATIC IMPORTED)
set_target_properties(prjct::AnotherLibrary PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include;${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "dpndnc::Thingy"
)
And symbols inside libAnotherLibrary.a
are again the same as with PRIVATE scope:
$ nm --demangle ../install/lib/libAnotherLibrary.a
another.cpp.o:
000000000000038c s GCC_except_table0
00000000000003a0 s GCC_except_table3
00000000000003e4 s GCC_except_table4
0000000000000400 s __GLOBAL__sub_I_another.cpp
U __Unwind_Resume
0000000000000638 b anotherString
00000000000000d0 T prjct::lbrAnother::doTheThing()
0000000000000000 T prjct::lbrAnother::doAnother()
U dpndnc::doThingy()
...
Configuring the Tool still requires providing paths to both installation folders:
$ cd /path/to/cmake-target-link-libraries-example/tl
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/prjct/install;/path/to/cmake-target-link-libraries-example/dpndnc/install" \
..
$ cmake --build . --target install
And the Tool executable symbols will be the same as with PRIVATE too:
$ nm --demangle ../install/bin/some-tool
0000000100003dd0 s GCC_except_table0
0000000100003e68 s GCC_except_table0
0000000100003e7c s GCC_except_table0
0000000100003e90 s GCC_except_table0
0000000100003e10 s GCC_except_table1
0000000100003e54 s GCC_except_table2
0000000100003c00 t __GLOBAL__sub_I_another.cpp
0000000100003ad0 t __GLOBAL__sub_I_some.cpp
0000000100003d20 t __GLOBAL__sub_I_thingy.cpp
U __Unwind_Resume
0000000100008000 b someString
0000000100008030 b thingyString
0000000100008018 b anotherString
0000000100003bf0 T prjct::lbrAnother::doTheThing()
0000000100003b20 T prjct::lbrAnother::doAnother()
0000000100003a00 T prjct::lbrSome::doSome()
0000000100003c50 T dpndnc::doThingy()
...
All that means that the Tool can now use both doTheThing()
function from AnotherLibrary
and doThingy()
function from Thingy
:
#include <SomeLibrary/some.h>
#include <AnotherLibrary/another.h>
#include <Thingy/thingy.h>
std::cout << "Direct dependencies:" << std::endl;
prjct::lbrSome::doSome();
prjct::lbrAnother::doAnother();
std::cout << std::endl;
std::cout << "Transitive dependency:" << std::endl;
//
// now this one works
std::cout << "via AnotherLibrary | ";
prjct::lbrAnother::doTheThing();
//
// and this one works too
std::cout << "directly | ";
dpndnc::doThingy();
I remember you were curious about this kind of things, so here’s what’s inside /path/to/cmake-target-link-libraries-example/tl/build/build.ninja
:
build CMakeFiles/some-tool.dir/src/main.cpp.o:
...
INCLUDES = -isystem /path/to/cmake-target-link-libraries-example/prjct/install/include -isystem /path/to/cmake-target-link-libraries-example/dpndnc/install/include
...
build some-tool:
...
LINK_LIBRARIES = /path/to/cmake-target-link-libraries-example/prjct/install/lib/libSomeLibrary.a /path/to/cmake-target-link-libraries-example/prjct/install/lib/libAnotherLibrary.a /path/to/cmake-target-link-libraries-example/dpndnc/install/lib/libThingy.a
...
As you can see, it’s the same as with INTERFACE scope.
SHARED libraries
Let’s check if anything is different when Project libraries are built as SHARED
.
But first, a couple of words about DLLs - shared libraries on Windows. As you probably know, .dll
file is what your application would need to be available for its runtime, but in order to link with that library on building your application you would still need a .lib
file. That .lib
file isn’t produced “by default”, because you need to explicitly mark the library’s symbols for export/import (or add -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=1
to export everything, but that’s not recommended).
With SomeLibrary
as an example, to export its symbols for producing .lib
you’d need to add __declspec
keyword for every symbol in its public header. But as you need to do that only when the library is built as SHARED
, then you’d need to add a compile definition and check for it with #ifdef
.
Good news here is that you don’t need to set this definition yourself, because CMake already does that for SHARED
libraries on its own, in a form of -DSomeLibrary_EXPORTS
, where SomeLibrary
is the name of the target (you’ll be able to see that for yourself in the resulting build.ninja
file, on the DEFINES
lines). So you only need to do #ifdef SomeLibrary_EXPORTS
in your sources. But even that part can be taken care of by CMake with GenerateExportHeader (as described in this post), although this functionality I haven’t tested myself.
Now back to scopes.
PRIVATE
Having that:
find_package(Thingy CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME}
PRIVATE
dpndnc::Thingy
)
configure libraries as SHARED
and build:
$ cd /path/to/cmake-target-link-libraries-example/prjct
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/dpndnc/install" \
-DBUILD_SHARED_LIBS=1 \
..
$ cmake --build . --target install
And yes, there will be something new, here’s what’s inside install/share/AnotherLibrary/AnotherLibraryTargets.cmake
:
# Create imported target prjct::AnotherLibrary
add_library(prjct::AnotherLibrary SHARED IMPORTED)
set_target_properties(prjct::AnotherLibrary PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include;${_IMPORT_PREFIX}/include"
)
Nothing about linking to Thingy
!
Let’s look at the symbols:
$ nm --demangle ../install/lib/libAnotherLibrary.dylib
0000000000003ebc s GCC_except_table0
0000000000003f28 s GCC_except_table0
0000000000003ed0 s GCC_except_table3
0000000000003f14 s GCC_except_table4
0000000000003cf0 t __GLOBAL__sub_I_another.cpp
0000000000003e10 t __GLOBAL__sub_I_thingy.cpp
U __Unwind_Resume
0000000000008018 b thingyString
0000000000008000 b anotherString
0000000000003a30 T prjct::lbrAnother::doTheThing()
0000000000003960 T prjct::lbrAnother::doAnother()
0000000000003d40 T dpndnc::doThingy()
...
There are differences here too. Unlike PRIVATE scope with STATIC
variant, AnotherLibrary
’s binary now contains Thingy
stuff too: not just the function, as before, but also its string variable, which AnotherLibrary
binary did not contain in either of scopes with STATIC
variant.
And you guessed it right, this means that Tool no longer needs to find the Thingy
CMake package and does not need to link to Thingy
’s binary, so there is no need to provide path to Thingy
installation in CMAKE_INSTALL_PREFIX
:
$ cd /path/to/cmake-target-link-libraries-example/tl
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/prjct/install" \
..
$ cmake --build . --target install
It builds and gets the following symbols:
$ nm --demangle ../install/bin/some-tool
0000000100003e90 s GCC_except_table0
0000000100003ed0 s GCC_except_table1
0000000100003f14 s GCC_except_table2
U __Unwind_Resume
U prjct::lbrAnother::doTheThing()
U prjct::lbrAnother::doAnother()
U prjct::lbrSome::doSome()
...
Only the Project’s libraries symbols, no traces of Thingy
stuff.
Same story in the /path/to/cmake-target-link-libraries-example/tl/build/build.ninja
, no linking to Thingy
:
build CMakeFiles/some-tool.dir/src/main.cpp.o:
...
INCLUDES = -isystem /path/to/cmake-target-link-libraries-example/prjct/install/include
...
build some-tool:
...
LINK_LIBRARIES = -Wl,-rpath,/path/to/cmake-target-link-libraries-example/prjct/install/lib /path/to/cmake-target-link-libraries-example/prjct/install/lib/libSomeLibrary.dylib /path/to/cmake-target-link-libraries-example/prjct/install/lib/libAnotherLibrary.dylib
...
What’s new is that LINK_LIBRARIES
now also has -Wl,-rpath
.
As for the sources, needless to say that calling doThingy()
won’t work in this case, same as with PRIVATE scope in STATIC
variant:
#include <SomeLibrary/some.h>
#include <AnotherLibrary/another.h>
// not available, or rather not discoverable
//#include <Thingy/thingy.h>
std::cout << "Direct dependencies:" << std::endl;
prjct::lbrSome::doSome();
prjct::lbrAnother::doAnother();
std::cout << std::endl;
std::cout << "Transitive dependency:" << std::endl;
//
// that works
std::cout << "via AnotherLibrary | ";
prjct::lbrAnother::doTheThing();
//
// but that doesn't
//std::cout << "directly | ";
//dpndnc::doThingy();
A slightly interesting detail is that if you’ll make Thingy/thingy.h
header discoverable (for example, by copying it to include
inside the Project’s installation), then Tool will be able to call dpndnc::doThingy()
, just like as it was in the same situation with PRIVATE scope in STATIC
variant. But here in SHARED
variant it still won’t need the Thingy
’s binary for linking, as this symbol will be resolved from AnotherLibrary
binary. Cool, huh. Anyway, it’s just a side note; you should definitely not do such things (manipulating headers discovery against scope intentions).
Of course, since the Project libraries are SHARED
now, the executable will fail to run:
$ ../install/bin/some-tool
dyld[58538]: Library not loaded: @rpath/libSomeLibrary.dylib
Referenced from: <SOME-ID-HERE> /path/to/cmake-target-link-libraries-example/tl/install/bin/some-tool
Reason: tried: '/usr/local/lib/libSomeLibrary.dylib' (no such file), '/usr/lib/libSomeLibrary.dylib' (no such file, not in dyld cache)
Abort trap: 6
$ otool -L ../install/bin/some-tool
@rpath/libSomeLibrary.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/libAnotherLibrary.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1500.65.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
On Windows you’d need to put the libraries DLLs alongside the executable, while on Mac OS (and GNU/Linux) you also need to set DYLD_LIBRARY_PATH
(LD_LIBRARY_PATH
on GNU/Linux) environment variable:
$ cp ../../prjct/install/lib/*.dylib ../install/bin/
$ DYLD_LIBRARY_PATH="../install/bin" ../install/bin/some-tool
# or
$ cd ../install/bin
$ DYLD_LIBRARY_PATH="." ./some-tool
What’s interesting, although unrelated to scopes effects, is that if you try to install the Tool again, right after just installing it, the installation will have some error message while still returning 0
exit code:
$ cmake --build . --target install
[2/3] Install the project...
-- Install configuration: "Release"
-- Installing: /path/to/cmake-target-link-libraries-example/tl/install/bin/some-tool
$ echo $?
0
$ cmake --build . --target install
[0/1] Install the project...
-- Install configuration: "Release"
-- Up-to-date: /path/to/cmake-target-link-libraries-example/tl/install/bin/some-tool
error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/install_name_tool: no LC_RPATH load command with path: /path/to/cmake-target-link-libraries-example/prjct/install/lib found in: /path/to/cmake-target-link-libraries-example/tl/install/bin/some-tool (for architecture x86_64), required for specified option "-delete_rpath /path/to/cmake-target-link-libraries-example/prjct/install/lib"
$ echo $?
0
Removing ../install/bin/some-tool
file and trying again gets rid of the error message. I have no idea what that is, probably one of the Mac OS things.
Anyway, to summarize, the PRIVATE
scope in case of SHARED
libraries lets you not to deliver your 3rd-party dependencies to you customer, as he’ll be able to build his Tool without them. Great news, isn’t it. Too bad your customer will likely want to get exactly STATIC
variants of your Project libraries, not to mention that he’ll probably want to be able to build your Project from sources himself.
What if transitive dependency is also SHARED
If Thingy
library also was a SHARED
library, that wouldn’t affect the build and linking:
- there would be still no linking to it in
AnotherLibraryTargets.cmake
; - Tool configuration would still not require adding path to
Thingy
’s installation intoCMAKE_INSTALL_PREFIX
and would not requireThingy
’s binary to be available for linking.
But that would certainly affect the final application - the Tool - because it would then require Thingy
’s binary for its runtime (in addition to the Project’s libraries, and that obviously applies to other scopes too). So if your goal is to minimize the number of things you have to deliver, then you’d probably prefer your 3rd-party (transitive for your customer) dependencies to remain STATIC
(but that might complicate things if some of those are licensed under LGPL or similar).
INTERFACE
Having that:
find_package(Thingy CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME}
INTERFACE
dpndnc::Thingy
)
configure libraries as SHARED
and build:
$ cd /path/to/cmake-target-link-libraries-example/prjct
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/dpndnc/install" \
-DBUILD_SHARED_LIBS=1 \
..
$ cmake --build . --target install
Linking to Thingy
will be back in install/share/AnotherLibrary/AnotherLibraryTargets.cmake
:
# Create imported target prjct::AnotherLibrary
add_library(prjct::AnotherLibrary SHARED IMPORTED)
set_target_properties(prjct::AnotherLibrary PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include;${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "dpndnc::Thingy"
)
Symbols will be the same as with INTERFACE scope in STATIC
variant:
$ nm --demangle ../install/lib/libAnotherLibrary.dylib
0000000000003ef0 s GCC_except_table0
0000000000003f04 s GCC_except_table2
0000000000003f48 s GCC_except_table3
0000000000003e40 t __GLOBAL__sub_I_another.cpp
U __Unwind_Resume
0000000000008000 b anotherString
0000000000003ac0 T prjct::lbrAnother::doAnother()
...
And again, building the Tool will require providing paths to both Project and Thingy
installations:
$ cd /path/to/cmake-target-link-libraries-example/tl
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/prjct/install;/path/to/cmake-target-link-libraries-example/dpndnc/install" \
..
$ cmake --build . --target install
The Tool’s binary will get the following symbols:
$ nm --demangle ../install/bin/some-tool
0000000100003e64 s GCC_except_table0
0000000100003efc s GCC_except_table0
0000000100003ea4 s GCC_except_table1
0000000100003ee8 s GCC_except_table2
0000000100003db0 t __GLOBAL__sub_I_thingy.cpp
U __Unwind_Resume
0000000100008000 b thingyString
U prjct::lbrAnother::doAnother()
U prjct::lbrSome::doSome()
0000000100003ce0 T dpndnc::doThingy()
...
Unlike its STATIC
variant, this time there are no string variables from AnotherLibrary
and SomeLibrary
, but available functions are the same:
#include <SomeLibrary/some.h>
#include <AnotherLibrary/another.h>
#include <Thingy/thingy.h>
std::cout << "Direct dependencies:" << std::endl;
prjct::lbrSome::doSome();
prjct::lbrAnother::doAnother();
std::cout << std::endl;
std::cout << "Transitive dependency:" << std::endl;
//
// trying to use that one will fail to build with
// error: no member named 'doTheThing' in namespace 'prjct::lbrAnother'
//std::cout << "via AnotherLibrary | ";
//prjct::lbrAnother::doTheThing();
//
// that one works fine
std::cout << "directly | ";
dpndnc::doThingy();
The /path/to/cmake-target-link-libraries-example/tl/build/build.ninja
contents are almost the same as with INTERFACE scope in STATIC
variant, except for the -Wl,-rpath
again:
build CMakeFiles/some-tool.dir/src/main.cpp.o:
...
INCLUDES = -isystem /path/to/cmake-target-link-libraries-example/prjct/install/include -isystem /path/to/cmake-target-link-libraries-example/dpndnc/install/include
...
build some-tool:
...
LINK_LIBRARIES = -Wl,-rpath,/path/to/cmake-target-link-libraries-example/prjct/install/lib /path/to/cmake-target-link-libraries-example/prjct/install/lib/libSomeLibrary.dylib /path/to/cmake-target-link-libraries-example/prjct/install/lib/libAnotherLibrary.dylib /path/to/cmake-target-link-libraries-example/dpndnc/install/lib/libThingy.a
...
And the Tool executable will still require Project’s libraries to be available for runtime. But even though it requires Thingy
’s binary for linking, it is not needed for runtime:
$ otool -L ../install/bin/some-tool
@rpath/libSomeLibrary.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/libAnotherLibrary.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1500.65.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
$ cp ../../prjct/install/lib/*.dylib ../install/bin/
$ ls -L1 ../install/bin/
libAnotherLibrary.dylib
libSomeLibrary.dylib
some-tool
$ DYLD_LIBRARY_PATH="../install/bin" ../install/bin/some-tool
PUBLIC
Having that:
find_package(Thingy CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME}
PUBLIC
dpndnc::Thingy
)
configure libraries as SHARED
and build:
$ cd /path/to/cmake-target-link-libraries-example/prjct
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/dpndnc/install" \
-DBUILD_SHARED_LIBS=1 \
..
$ cmake --build . --target install
The linking to Thingy
is still present in install/share/AnotherLibrary/AnotherLibraryTargets.cmake
:
# Create imported target prjct::AnotherLibrary
add_library(prjct::AnotherLibrary SHARED IMPORTED)
set_target_properties(prjct::AnotherLibrary PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include;${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "dpndnc::Thingy"
)
Symbols are the same plus again Thingy
’s string variable:
$ nm --demangle ../install/lib/libAnotherLibrary.dylib
0000000000003ebc s GCC_except_table0
0000000000003f28 s GCC_except_table0
0000000000003ed0 s GCC_except_table3
0000000000003f14 s GCC_except_table4
0000000000003cf0 t __GLOBAL__sub_I_another.cpp
0000000000003e10 t __GLOBAL__sub_I_thingy.cpp
U __Unwind_Resume
0000000000008018 b thingyString
0000000000008000 b anotherString
0000000000003a30 T prjct::lbrAnother::doTheThing()
0000000000003960 T prjct::lbrAnother::doAnother()
0000000000003d40 T dpndnc::doThingy()
...
Building the Tool of course still requires both installation paths:
$ cd /path/to/cmake-target-link-libraries-example/tl
$ mkdir build && cd $_
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="../install" \
-DCMAKE_PREFIX_PATH="/path/to/cmake-target-link-libraries-example/prjct/install;/path/to/cmake-target-link-libraries-example/dpndnc/install" \
..
$ cmake --build . --target install
The symbols it gets are these:
$ nm --demangle ../install/bin/some-tool
0000000100003e84 s GCC_except_table0
0000000100003ec4 s GCC_except_table1
0000000100003f08 s GCC_except_table2
U __Unwind_Resume
U prjct::lbrAnother::doTheThing()
U prjct::lbrAnother::doAnother()
U prjct::lbrSome::doSome()
U dpndnc::doThingy()
...
And all the functions are available:
#include <SomeLibrary/some.h>
#include <AnotherLibrary/another.h>
#include <Thingy/thingy.h>
std::cout << "Direct dependencies:" << std::endl;
prjct::lbrSome::doSome();
prjct::lbrAnother::doAnother();
std::cout << std::endl;
std::cout << "Transitive dependency:" << std::endl;
//
// this one works
std::cout << "via AnotherLibrary | ";
prjct::lbrAnother::doTheThing();
//
// and this one works
std::cout << "directly | ";
dpndnc::doThingy();
Requirements for runtime libraries are the same too:
$ otool -L ../install/bin/some-tool
@rpath/libSomeLibrary.dylib (compatibility version 0.0.0, current version 0.0.0)
@rpath/libAnotherLibrary.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1500.65.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
$ cp ../../prjct/install/lib/*.dylib ../install/bin/
$ ls -L1 ../install/bin/
libAnotherLibrary.dylib
libSomeLibrary.dylib
some-tool
$ DYLD_LIBRARY_PATH="../install/bin" ../install/bin/some-tool
So when do I need to deliver 3rd-party dependencies to my customers
…so they could build their applications with your SDK/framework/library, which is their direct dependency.
Almost always, as it seems.
Assuming that we are comparing the scope of target_link_libraries()
from our library to 3rd-party dependencies, with both STATIC
and SHARED
variants of our library, a summarizing table would be this:
STATIC | SHARED | |
---|---|---|
PRIVATE | yes | no |
INTERFACE | yes | yes |
PUBLIC | yes | yes |
But I don’t want to
If you really don’t want to distribute all of your 3rd-party dependencies to customers (one of the reasons could be that they are not using CMake, so they’d need to somehow manage the transitive dependencies linking themselves), then there are still some options (none of which I’ve tried myself).
You could probably bundle/vendor 3rd-party dependencies sources into your SDK/framework/library project and make them OBJECT libraries. But we are all civilized people here, right, we want to use CMake packages and find_package(Thingy CONFIG REQUIRED)
, and probably with a package manager such as vcpkg, so no vendoring, forget that I even brought this up.
Another way to “hide” 3rd-party dependencies would probably be via making a frankenstein monster of a static mega-library, which would contain everything: object files of your libraries and object files of 3rd-party libraries. Basically, you would “unpack” all the static libraries that you got and “pack” their object files into one big fucking final static library, which is what you’d distribute to your customers. As with the previous option, this doesn’t look like something I would recommend, but if you still would like to do that, here are some links for further reading:
- Linking static libraries to other static libraries;
- How can I combine several C/C++ libraries into one;
- Using CMake to build a static library of static libraries;
- Bundling together static libraries with CMake, and as a result of that one, a feature request in CMake’s repository.
It also wouldn’t hurt to make sure that none of your 3rd-party dependencies license terms would mind against you performing these acrobatic exercises.
Social networks
Zuck: Just ask
Zuck: I have over 4,000 emails, pictures, addresses, SNS
smb: What? How'd you manage that one?
Zuck: People just submitted it.
Zuck: I don't know why.
Zuck: They "trust me"
Zuck: Dumb fucks