Chapter 4
Building in multiple directories
This chapter has two main objectives:
- Demonstrate how to organize the generated documents by CMake for your project using directories.
- Show how to build a project which consists of multiple directories.
Multiple Directory Example
In the last three chapters, the focus has been in basic syntax and CACHE vaiables, which are the basic requirements in order to create a simple project. As the project grows and our CMakeLists.txt file becomes more complex, the number of generated files used by CMake to build our project grows as well, which creates a problem of administration.
Thankfully, organizing our code and CMakeLists file in order to fix the uncontrolled growth of generated file is really easy.
Lets take our Hello World example from the previous chapters and reorganize it and we'll rebuild it using four new directories: src where our code is going to be located, includes which is a directory inside src and constains the header files, build which is the directory where we want all our build files to be stored and bin where we want the final binary to be generated.
First let's create the new directories for our source code and move the files to their corresponding position
$ mkdir src
$ mkdir src/includes
$ mv Hello.cpp src/
$ mv HelloTest.cpp src/
$ mv Hello.h src/includes/
Which should give us an structure similar to the following one
$ ls
CMakeLists.txt CMakeValuesCache.txt src/
$ ls -R
.:
CMakeLists.txt CMakeValuesCache.txt src/
./src:
Hello.cpp HelloTest.cpp includes/
./src/includes:
Hello.h
We have to modify our source code to reflect the new structure of our source code
//
// Hello.cpp
//
#include "includes/Hello.h"
#include<iostream>
int main(int argc, char** argv) {
std::cout << helloWorld() << std::endl;
return 0;
}
//
// HelloTest.cpp
//
#include "includes/Hello.h"
#include <assert.h>
#include <string.h>
int main(int argc, char** argv) {
assert(std::string("Hello World").compare(helloWorld()) == 0);
return 0;
}
The next step is to modify our CMakeLists.txt files to reflect the changes of the directory. Bellow is listed the file before we start with our modifications:
# CMakeLists.txt
# We are using version 3.0.2 of CMake for this example
cmake_minimum_required( VERSION 3.0.2 )
# Workspace is HelloWorld, at the end you should see a
# HelloWorld directory witht the project on it
project( HelloWorld )
# Set the directory where the bin will be built.
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/bin)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
# The CACHE variable *HelloWorld_SOURCE_DIR* stores the absolute path of
# the directory where CMakeLists is located
set( HELLO_HEADERS ${HelloWorld_SOURCE_DIR}/src/includes/Hello.h )
set( HELLO_SRCS ${HelloWorld_SOURCE_DIR}/src/Hello.cpp )
# Link the sources with the final binary. The name of the binary
# is going to be "hello"
add_executable( hello ${HELLO_SRCS} ${HELLO_HEADERS} )
# If the flag DEBUG_MODE is set, build the test file
if( DEBUG_MODE )
set( HELLO_TEST_SRCS HelloTest.cpp )
add_executable( hello_test ${HELLO_TEST_SRCS} ${HELLO_HEADERS} )
endif()
The variable CMAKE_BINARY_DIR is used by CMake to know the directory to the top level of the build tree and the variable EXECUTABLE_OUTPUT_PATH for the directory where the final binary will be built. In this case we are stating it in the bin directory.
Probably you are reading this file and thinking "I could just add the new paths to HELLO_HEADERS and HELLO_SRC and the code should work", and interestingly you'll be correct, but there is a little problem with the solution and is that you'll have to change the value of those two variables every small change in the structure of your code. Instead we can use the CACHE values and start generating the new values from them.
The new CACHE value which will use is
There are two, as version 3.0.2, two different ways of indicating cmake where to generate the files it needs, the first one, which is deprecated and not officially in the documentation anymore, is using the flag -B followed by the name of the directory. If cmake doesn't find the directory this is created before the files are generated. The command structure is:
$ cmake CMakeLists.txt -B<directory to build>
In this case if we want to write inside the directory build the command would be
$ cmake CMakeLists.txt -Bbuild
What the flag -B does it to rewrite the CACHE variable
The second option consists of rewriting explicitly the variables creating a directory called "build" and called cmake from that directory. This method is generally the most common option documented in projects using CMake. The following steps will be used to create our new project
$ mkdir build
$ mkdir bin
$ cd build
$ cmake ../
$ cmake ../
-- The C compiler identification is GNU 4.9.2
-- The CXX compiler identification is GNU 4.9.2
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Configuring done
-- Generating done
-- Build files have been written to: <Path where the build files were generated>
Notice that the first cmake command only needs the directory where the CMakeLists.txt by convention cmake searches this file in the directory sent as parameter.
$ cmake --build .
Linking CXX executable ../bin/hello
[100%] Built target hello
The new executable will be found inside the directory /bin in the root directory. This approach of organizing the new generated files has multiple advantage compared with our previous examples:
- If something was configured wrong deleting the build and bin directories is enough instead of looking for each file
- It's easier to just list a single directory for tools to ignore instead of a list of files
- Keeps a separation between our build process and our development process which eliminates problems of overloading the source directory with new files.
In future chapters more complex methods to assign the direcrtory structure usning Macros. For the mayority of projects this method is enough as it's quite scalable.