CMake and Libraries, part 1
I must tell you the truth, not all applications out there are as simple as a hello world; yes, I know, shocking right?! So far we have been doing simple CMake files to produce a simple application with a few files. Today let’s try to cover a little more complicated scenario with an application depending and producing libraries; it will be fun!
The project
Let’s start with our project structure:
.
├── CMakeLists.txt
├── hello_app
│ ├── CMakeLists.txt
│ └── src
├── libgreeter
│ ├── CMakeLists.txt
│ ├── includes
│ └── src
└── output
We will put our library libgreeter
(the greatest hello library out there!), its source in the src
directory and its header files in the includes
directory. Our executable application will live in the hello_app
directory and its source in the src
folder, hello_app depends on libgreeter (it will be statically linked) and we will compile a static and shared library for libgreeter. Finally, we will place the output of everything in the output
directory and out of source build in the build
folder. Notice we have a root CMakeLists.txt
file and a corresponding file for the library and the “main” program. This is to make our build process much more modular and maintainable, yes, we can make all the build with just one CMake list file but I wouldn’t recommend that (me and almost everybody else in the internet).
The code for greeter.h
header library in the libgreeter/includes
directory is pretty simple:
#ifndef GREETER_H
#define GREETER_H
void say_hello_to (char *name);
#endif
Its implementation lives in libgreeter/src/greeter.c
:
#include <stdio.h>
#include "greeter.h"
void say_hello_to (char *name) {
printf("hello %s\n", name);
}
Can we put the header and the source in the same directory for the greeter library? Yes, you can! However, because we are expecting our amazing library to be used by other programs or libraries we decide to make those header public and place them apart. This is a very common layout for libraries and projects in CMake.
The CMakeLists.txt
file for our greeter library (not the root list file) is the typical library list file:
cmake_minimum_required (VERSION 3.6)
project (libgreeter VERSION 1.0)
include_directories (${CMAKE_CURRENT_SOURCE_DIR}/includes)
set (GREETER_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)
set (GREETER_SOURCE ${GREETER_SOURCE_DIR}/greeter.c)
add_library (greeter SHARED ${GREETER_SOURCE})
add_library (greeter_static STATIC ${GREETER_SOURCE})
Notice the usage of CMAKE_CURRENT_SOURCE_DIR
to point to the current directory where this CMakeLists.txt file is located; if we don’t do that it will point to the directory of the root project.
Let’s move to the root CMakeLists.txt
; it is a little different to what we have written so far:
cmake_minimum_required (VERSION 3.6)
project (hello VERSION 1.0)
set (LIBRARY_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/output)
add_subdirectory (libgreeter)
The add_subdirectory
line tells CMake to include that directory project as part of the build, like a cascade project. I like that.
We are nearly done, let’s test if we are building the library at least. In the root project directory do the classic out of source build:
mkdir build; cd build
cmake ..
cmake --build . --clean-first
Done, you should see a dynamic and static library binary in the output
directory.
The application
Now it is time to move to our main application, this is how our simple hello_app/src/main.c
looks like:
#include "greeter.h"
int main() {
say_hello_to ("cristian");
}
Now it is time to define hello_app/CMakeLists.txt
file:
cmake_minimum_required (VERSION 3.6)
project (hello_app VERSION 1.0)
set (HELLO_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)
set (HELLO_SOURCES ${HELLO_SOURCE_DIR}/main.c)
add_executable (hello_app ${HELLO_SOURCES})
target_link_libraries (hello_app greeter)
Let’s change the root CMakeLists.txt
file, now it will look like this:
cmake_minimum_required (VERSION 3.6)
project (hello VERSION 1.0)
set (LIBRARY_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/output)
set (EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/output)
add_subdirectory (greeter)
add_subdirectory (hello_app)
If you try to run this as is it won’t work. We are including greeter.h
but that file it is not in our source tree. We could probably add an include_directories
pointing to the directory where the greeter.h
is located but that would be a killer in our pursuit of modularization. The secret is adding one line to our libgreeter/CMakeLists.txt
file, it should now look like this:
cmake_minimum_required (VERSION 3.6)
project (libgreeter VERSION 1.0)
include_directories (${CMAKE_CURRENT_SOURCE_DIR}/includes)
set (GREETER_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)
set (GREETER_SOURCE ${GREETER_SOURCE_DIR}/greeter.c)
add_library (greeter SHARED ${GREETER_SOURCE})
add_library (greeter_static STATIC ${GREETER_SOURCE})
target_include_directories (greeter PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includes)
See the last line? It basically tells to CMake “hey, this directory I am putting here is a public include headers directory, pass that info to everybody else in the project”.
Now try to build the whole project again… You will see the executable hello_app
, a static library and a dynamic library for the greeter project as well.
Next
What if the library is not made in CMake? What if the library is a third party library already in the system? What if it’s neither…? Well, I will try those ideas later in another blog post(s); wish me luck!
As usual, you can find the source code for this in a GitHub Gist.