The buildsystem was always my softspot when writing code, my approach was always to google how to do it and copy paste code and try to get it to run. This is an attempt to take a step back and actually try to learn how cmake work, so that I can actually understand what's going on. So I will continue to add in random methods of linking in librarys to try them out, just to learn how to organize bigger projects with several types of external components and will use this as a reference for the future.
CmakeHowTo
|-- CMakeList.txt
|
Components
|
|-- App
| |-- CmakeList.txt
| |-- main.cpp
|
|-- FunctionLibb
| |-- CmakeList.txt
| |-- Functions.cpp
| |-- Functions.h
| |
| |-- test
| |-- test_ValidateLibraryFunctions.cpp
|
|-- YamlLibb
|-- CmakeList.txt
|-- Yaml.cpp
|-- Yaml.h
So the idea is to have all the different components split up into their own parts, and the root
CMakeList.txt
includes all components, the components are then themself responsible of linking up against it's dependencies.
The specific use cases are described below for my own convenience, or else I will forget about it.
Order of component include matters
Since I want to use the functionlibb in the app, I need to in the root add the FunctionLibb before I add App, such as:
add_subdirectory(Components/FunctionLibb)
add_subdirectory(Components/app)
Use FunctionLibb in App
The simple use case of linking a component together with another is done as such:
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} PUBLIC FunctionLibb)
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_BINARY_DIR}
${FunctionLibb_INCLUDES}
)
Where the ${FunctionLibb_INCLUDES}
variable is something that is defined and cached in the FunctionLibb components CMakeList.txt
by doing the following:
set(${PROJECT_NAME}_INCLUDES ${PROJECT_SOURCE_DIR} CACHE INTERNAL "extra_includes")
I've found that it's an okay way of communicating the components includes.
And building the library is as easy as:
add_library(${PROJECT_NAME} SHARED Functions.cpp)
Add a external library from github
The external library I wanted to add was googletest, to do that I used the CMake module ExternalProject
, but then it was as easy as:
include(ExternalProject)
ExternalProject_Add(googletest
GIT_REPOSITORY git://github.com/google/googletest
GIT_TAG main
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${EXTERNAL_INSTALL_LOCATION}
)
and after calling add_dependencies(RunTests googletest)
, the cmake system will try to download the specified tag and build it in the cmake build folders when the RunTests
project is built - Super simple and neat!
Add Google test to CMake
When building google tests, there are a few parts that needs to be done.
- Create a executable to run the tests.
- Add googletest as a dependency for the executable.
- Link together executable, googletest and the library that you want to test.
- Add the gtest framework boilerplate code to the
CMakeList.txt
file.
In my case I chose to place the tests in a test
folder to make it more easily overviewable - And this all could look something like:
enable_testing()
add_executable(RunTests test/test_ValidateLibraryFunctions.cpp)
add_dependencies(RunTests googletest)
target_link_libraries(RunTests ${PROJECT_NAME} gtest gtest_main)
target_include_directories(RunTests PRIVATE
${EXTERNAL_INSTALL_LOCATION}/include
.
)
include(GoogleTest)
gtest_discover_tests(RunTests)
Dynamic vs Static library
The two main library types (that I know of?) are:
- Dynamic library, called
libgtest.dylib
on mac,libgtest.dll
on windows andlibgtest.so
on UNIX. - Static library, called
libgtest.a
on mac / UNIX andlibtest.lib
on windows.
Where static libraries are a little bit easier to use as they compile everything together to a big binary file with no external links. For dynamic libraries, you also need the libraries external references as it will not be compiled into the same binary.
This reduces overall size and increases flexibility of the dynamic library, but makes it a bit more annoying to work with. In cmake it is very easy to build your library as a dynamic (shared library), just do the following:
add_library(${PROJECT_NAME} SHARED Functions.cpp)
And for building the a static library change the SHARED
keyword to STATIC
such as:
add_library(${PROJECT_NAME} SHARED Functions.cpp````)
And when linking in the shared vs static library into your app you do exactly the same thing for both of them:
add_executable(${PROJECT_NAME} main.cpp)
add_dependencies(${PROJECT_NAME}
YamlLibb
FunctionLibb
)
target_link_libraries(${PROJECT_NAME} PRIVATE
FunctionLibb
YamlLibb
)
target_include_directories(${PROJECT_NAME} PRIVATE
${FunctionLibb_INCLUDES}
${YamlLibb_INCLUDES}
)
With the caveat that the shared library (FunctionLibb
is shared and YamlLibb
is static) also would need it's external dependencies also linked in, but in this example FunctionLibb
does not have any external dependencies.
Target "yaml-cpp" of type UTILITY may not be linked into another target
When I was linking in the Yaml-cpp library I kept running into
CMake Error at Components/YamlLibb/CMakeLists.txt:24 (target_link_libraries): Target "yaml-cpp" of type UTILITY may not be linked into another target. One may link only to INTERFACE, OBJECT, STATIC or SHARED libraries, or to executables with the ENABLE_EXPORTS property set
And when searching around for it, the explanation I found was that cmake couldn't resolve the path to the yaml-cpp lib So instead of doing:
target_link_libraries(${PROJECT_NAME} PRIVATE
yaml-cpp
)
I had to explicitly point out to cmake where the static library was like so:
target_link_libraries(${PROJECT_NAME} PRIVATE
${EXTERNAL_INSTALL_LOCATION}/lib/libyaml-cpp.a
)
But then it worked like a charm!
Make IDE detect externally downloaded libraries
It is very nice if you can setup your IDE to work with all external libraries, so your intellisense works correctly and your includes work.
How I made this work in this project was to use the include_directories(...)
directive in cmake.
So in my root's cmake i do the following, to setup the path where I put all my external libraries and then tell the IDE where to find all includes:
set(EXTERNAL_INSTALL_LOCATION ${CMAKE_BINARY_DIR}/external)
include_directories(${EXTERNAL_INSTALL_LOCATION}/include)
So with this small maneuver I was able to get the IDE understand about my includes.