Recall from class that CMake is a tool that enables you to configure and construct a build process for a project. This exercise will provide you first hand experience in using basic features of CMake that you may find useful (required) in order to complete your term project. You will be constructing and modifying the CMake configuration files of a small project in order to produce the final desired results. You should feel free to look at outside sources such as the CMake Documentation for help, as long as you do not copy or share your work with another student. To access help for any command from the command line, you may use
cmake --help-command <command> | less
for example, you may print the documentation for the find_library
command via
cmake --help-command find_library | less
Note:
CMakeCache.txt
in the root directory of the project
in order to continue with the exercise.libncurses5-dev
package is installed.To start, create a new directory called cmakeExercise/
. This will be the root
directory of your project for the exercise. Spelling matters, as exercises will
be graded automatically.
A project built using CMake is configured and controlled through lists of
CMake commands in CMakeLists.txt
files.
To simply compile a program using CMake, you need to define a CMakeLists.txt
file containing basic information about the project in the root directory of
the project.
Our initial project contains the three files below:
#include <iostream>
#include <vector>
#include "SortIntegers.h"
int
main() {
std::vector<int> integers = {1, 13, 89, 2, 55, 21, 8, 5, 3, 1, 34};
sortIntegers(integers);
for (auto integer : integers) {
std::cout << integer << "\n";
}
return 0;
}
#ifndef SORT_INTEGERS_H
#define SORT_INTEGERS_H
void sortIntegers(std::vector<int> &numbers);
#endif
#include <algorithm>
#include <vector>
void
sortIntegers(std::vector<int> &numbers) {
std::sort(std::begin(numbers), std::end(numbers));
}
The most basic CMakeLists.txt
for a project usually includes three commands:
cmake_minimum_required
, project
, and add_executable
. Recall that
add_executable
creates a target to build1. Note that all work
we do in this class uses C++14,
which is not the default for the compiler installed in the labs. Thus, you also
need to set the version of the language used to compile the target using:
set_property(TARGET <target name> PROPERTY CXX_STANDARD 14)
Use the documentation for these commands to create a CMakeLists.txt
script
that defines a project called sorter
and compiles the above two files into a
single program called sorter
. Recall that you will want to use an
out-of-source build process to make sure that your script works. You will lose
points for any build artifacts found in your source directories. You can also
choose to use clang
and clang++
as the compilers for your project by using
the CMAKE_C_COMPILER
and CMAKE_CCC_COMPILER
variables:
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ /path/to/project/
If you were to run the program under a debugger, you might notice that it does
not have any debugging symbols. This certainly makes debugging a challenge!
CMake allows you to build a program in different modes or
types.
Building in Debug
mode enables debugging, while building in Release
mode
does not enable debugging symbols and might, for instance, do extra
optimization. The default mode is essentially Release
mode. In order to
select Debug
mode, you must set the CMAKE_BUILD_TYPE
variable when running
CMake. Once again, you do this on the command line:
cmake -DCMAKE_BUILD_TYPE=Debug /path/to/project/
When compiling with gcc
or clang
, this will automatically add the -g
option to the standard CFLAGS
variable used when invoking the compiler.
As much as possible, we would like to organize our source directory structure in order to reflect the different components of our project. We also want to decompose the project into potentially reusable components as much as possible. Finally, we want the directory structure of the separate build directory to also reflect the structure of the different components we are building.
In order to break our source program structure into different components, we will perform two tasks simultaneously. From the perspective of design, we will break the program up into (1) a library that encapsulates related features and (2) an executable program that uses the functionality of the library to achieve a specific goal. From the perspective of file organization, we will separate the files for individual libraries and executables into their own subdirectories.
Inside the project root directory, create the following directories:
include/
- contains the header files declaring the functions in the librarieslib/
- contains subdirectories for each library to buildlib/sortIntegers/
- contains a library for sorting vectors of integerstools/
- contains subdirectories for each executable to buildtools/fibSorter/
- contains an executable that sorts and prints the first
several scrambled Fibonacci numbers.Following the above descriptions (1) move main.cpp
into tools/fibSorter/
,
(2) move SortIntegers.cpp
into lib/sortIntegers/
, and (3) move
SortIntegers.h
into include/
. Now you must change the CMakeLists.txt
in
the project's root directory in order to use the new directory structure and
design. The first task is to tell CMake where to look for header files. This
is done using the include_directories
command:
include_directories(${CMAKE_SOURCE_DIR}/include)
Next, commands need to be added to build the sortInteger
library and the fibSorter
application. Use the add_subdirectory
command
to include targets in the libs/
and tools/
subdirectories. Inside these
subdirectories, you will need additional CMakeLists.txt
files that tell CMake
to descend further into the lib/sortIntegers/
and tools/fibSorter/
directories.
Finally, create a CMakeLists.txt
file in
lib/sortIntegers/
that builds a library called sortIntegers
using the
add_library
command. Similarly, create a CMakeLists.txt
file in
tools/fibSorter/
that builds a program called fibSorter
using the
add_executable
command along with the target_link_libraries
command.
Don't forget that both of these targets use C++14.
Now try building your project to make sure that it compiles. Look at the
directory structure in the build directory. Where is the fibSorter
program?
Try running it to make sure that it works. Where is the sortIntegers
library?
The build directory would be more organized if all programs were built in a
single bin/
subdirectory and all libraries were built in a 'lib/'
subdirectory. This is again done by setting specific CMake variables in the top
level CMakeLists.txt
:
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bin")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib")
You should be aware that the order of operations matters. Ask yourself,
"Where in the CMakeLists.txt
file do these lines belong?"
Note that the sortIntegers
library we built is called an archive or a static
library. If we instead wanted to build a
shared library,
we would instead set the CMAKE_ARCHIVE_OUTPUT_DIRECTORY
variable.
From within your build directory, run make clean
to remove your previously
compiled files and then run make
in order to check that the system correctly
compiles with your modifications2.
Now that you know how to build and organize your own projects in isolation, you can start to incorporate other external projects and libraries into your own project. For instance, if you want to use an external library for managing networking or cryptographic protocols, you can include CMake commands to find such libraries and link against them automatically.
External libraries and development APIs are included in a project using the
find_library
and find_package
commands. In many cases, such libraries and development packages can be found
and used by your CMake project even if it may reside in different locations
for different build environments or even different operating systems. Many
libraries are even specially detected by CMake in order to make development
easier. CMake
can also automatically build a completely separate
external project
and allow you to use it as a part of your build process.
For this next step, you will create a new tool/program that uses the ncurses library for presenting nice text based user interfaces. You will use CMake to recognize (1) whether or not the build environment contains the necessary dependencies and (2) determine which libraries need to be linked against in order to build a program that uses ncurses functionality.
Start by creating a new subdirectory called tools/cursesexample/
. Inside this
directory create the following source file:
#include <ncurses.h>
int
main() {
initscr();
if(has_colors() == FALSE) {
endwin();
printf("Your terminal does not support color\n");
return 1;
}
start_color();
init_pair(1, COLOR_RED, COLOR_BLACK);
attron(COLOR_PAIR(1));
attron(A_BOLD);
printw("Hello World !!!");
attroff(A_BOLD);
attroff(COLOR_PAIR(1));
refresh();
getch();
endwin();
return 0;
}
Now add the necessary changes to the various CMakeLists.txt
files in order to
tell CMake to build a program called cursesexample
using this source file.
Try to build to project. You should now see that it fails because the project
uses ncurses, but we haven't told it that we need to link against the curses
library. In order to do so, in the CMakeLists.txt
that configures the build
of cursesexample
, we need to use the find_package
command to find the
required ncurses libraries. You should use find_package
to search for the
Curses
package as a required dependency of the project. This uses CMake's
built in support for recognizing the curses-like libraries. Notice from the
given documentation
that using ncurses requires the command
SET(CURSES_USE_NCURSES TRUE)
before calling find_package
. Also notice that find_package
results in
variables being set that need to be used in order to make the project build
correctly. CURSES_LIBRARIES
is given the value of the necessary library names
for linking a project that uses ncurses. You can use this variable as
${CURSES_LIBRARIES}
to pass its values as arguments to another command.
The CURSES_INCLUDE_DIR
is given the path to the directory containing the
header files for ncurses. Use the commands you have already seen in order to
make the project compile using these variables.
Notice, if we wanted to, we could make the project compile by explicitly
passing different project commands the names of the libraries we want to link
against and the paths to the headers. However, if we did so, changing those
values in the future would be more cumbersome. In addition, using find_package
as we do in this case means that if curses is not present in the build
environment, we will get a clearly explained error as soon as CMake runs,
instead of a potentially confusing compilation error much later. I may check for
this when grading.
Once you can build and run cursesexample
, move on to the next step.
Building the project is important, but you also want to be able to install it
once the build process is complete. Installing all of the desired files once
they are built can be done using the install
command. install
copies the
selected TARGETS
and FILES
into paths rooted at the directory pointed to by
the CMAKE_INSTALL_PREFIX
variable. For instance, if you wanted to install
the project to ~/testing/
, you would set the install prefix when invoking
CMake using a command like
cmake -DCMAKE_INSTALL_PREFIX=~/testing/ -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ /path/to/project/
Notice that this is different than the invocation we used before. The argument
-DCMAKE_INSTALL_PREFIX=~/testing/
determines where the project will be
installed. Afterward, you can install the project by running make
and then
make install
.
There may be many different things that we want to install. For instance, we
presently build a library and a program, both of which can be installed. In
order to use the library, however, the header file for sortIntegers
must also
be installed. If we wanted to install documentation for the user, that would
also be done using the install
command.
install
allows you to specify a subdirectory inside the install prefix where
each installed file will be placed. For a Linux system there are general
patterns/rules to follow:
bin
subdirectorylib
subdirectoryinclude
subdirectoryshare/doc/<program name>
subdirectoryAdd rules to your CMakeLists.txt
files to install the header, the library,
and the programs to the appropriate directories.
If you are interested in creating a more professional installer, e.g. a Debian package or a graphical installer for Windows or OS X, you can do so using CPack and its CMake commands.
You should now have some experience with the fundamentals of CMake as discussed
in class. As a part of the project, you will also start to use the
add_custom_command
and
add_custom_target
commands along with find_package
in order to define a build process for the
project documentation using Sphinx as well as to
coordinate unit testing.
If you have written things successfully, you should be able to run the following from the project directory without errors.
mkdir ../tmpbuild
cd ../tmpbuild
cmake -DCMAKE_INSTALL_PREFIX=../tmpinstall/ ../cmakeExercise
make
make install
echo '#include <iostream>
#include <vector>
#include "SortIntegers.h"
int main() {
std::vector<int> integers = {6, 3, 1, 15, 11, 16};
sortIntegers(integers);
for (auto integer : integers) { std::cout << integer << "\n"; }
return 0;
}' > test.cpp
clang++ test.cpp -std=c++14 -I../tmpinstall/include -L../tmpinstall/lib/ -lsortIntegers
./a.out
../tmpinstall/bin/fibSorter
../tmpinstall/bin/cursesexample
One you are comfortable with your exercise, create a .tgz
file of the entire
cmakeExercise
directory and submit it via
CourSys.
As discussed in class, using CMake provides many useful features. For instance,
you are not required to use make
in order to build your project. You can have
CMake instead generate, e.g., an Eclipse project, a Visual Studio project,
an XCode project, and
more.
When building large projects, I like to use
Ninja because it is faster than many other
build systems and thus allows me to wait less for compilations to complete.
To build a project using Ninja, you can use
cmake -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=on /path/to/project/
ninja
ninja install
In addition, the JSON compilation databases that CMake can export allow you to
more easily use advanced analysis tools like
(clang-tidy
)[http://clang.llvm.org/extra/clang-tidy/].
As demonstrated during class, such tools can provide great aid in both
uncovering bugs and in identifying potential stylistic or design issues in
a project. To use clang-tidy
, you must pass it a generated compilation
database and tell it what checks you want to run as well as which files you
wish to analyze.
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=on /path/to/project/
cd /path/to/project/
clang-tidy -p /path/to/build/compile_commands.json -checks='*' lib/*/*.cpp tools/*/*.cpp
There are many more features available in CMake. You should feel free to explore the official CMake tutorial and other documentation online and see what else is available that may be helpful in your project.
A library is another type of target. ↩
There are other ways that a project can be organized. One approach is to break a project into individual subprojects, each in its own directory. CMake provides additional commands that can allow projects to use/communicate with each other. You can find more information on advanced uses of CMake here. ↩