In my introduction to Autotools, I demonstrated how to manage building and packaging code with GNU Autotools. It's a robust and common platform that integrates easily into many packaging systems, including RPM, APT, pkgsrc, and more. Its syntax and structure can be confusing, but luckily there are alternatives, including the open source CMake framework.
CMake is a cross-platform suite for building, testing, and packaging software. It uses simple and clearly documented syntax, so it's easy to start using even if you've never used a build system before.
Install CMake
CMake may already be installed on your Linux system. If not, you can install it with your distribution's package manager:
$ sudo dnf install cmake
On Debian or similar:
$ sudo apt install cmake
For Mac, you can use MacPorts or Homebrew:
$ sudo port install cmake
On Windows, you can use Chocolatey or download a binary directly from the CMake website.
CMake at work
For developers or users who want to build software from source code, CMake is a quick and easy way to compile and install it. CMake works in stages:
- First, during the
cmake
step, CMake scans the host system (the computer it's being run on) to discover the default settings. Default settings include where support libraries are located and where new software should be placed on the system. - Next, you use your system's
make
command (usually GNU Make on Linux, NetBSD Make on NetBSD, and so on) to build the application, usually by converting human-readable source code into machine language. - Finally, during the
make install
step, the built files are copied to the appropriate locations (as detected during thecmake
stage) on your computer.
It seems simple, and it is when you use CMake.
CMake portability
CMake is designed with portability in mind. While it can't make your project work across all POSIX platforms (that's up to you, as the coder), it can ensure that the files you've marked for installation get installed to the most sensible locations on a known platform. And because of tools like CMake, it's trivial for power users to customize and override any non-optimal value according to their system's needs.
With CMake, all you have to know is which files need to be installed to what general location. It takes care of everything else. No more custom install scripts that break on any untested operating system.
Packaging
Like Autotools, CMake is well-supported. Hand a project with CMake over to a distro packager, whether they're packaging an RPM or a DEB or a TGZ (or anything else), and their job is simple and direct. Packaging tools know CMake, so it's likely there will not be any patching, hacking, or adjustments required. In many cases, incorporating a CMake project into a pipeline can be automated.
How to use CMake
To start using CMake with your project, you need only to create a CMakeLists.txt
file in your project directory. First, declare the minimum required version of CMake and a project title and version. CMake strives to retain compatibility for as long as possible, but the more you use it and follow its development, the better you'll know what features you rely upon.
cmake_minimum_required(VERSION 3.10)
project(Hello VERSION 1.0)
As you may already be detecting, the syntax of CMake is a command followed by parameters in parentheses. The capitalized VERSION
strings aren't arbitrary or just for style; they're valid parameters for the project
command.
Before continuing, generate a sample hello world
application in C or C++. For simplicity, I wrote six lines of C code and saved it as hello.c
(to match the executable I list in CMakeLists.txt
):
#include <stdio.h>
int main() {
printf("Hello open source\n");
return 0;
}
Make no mistake, though, CMake is useful beyond just C and C++. It can handle arbitrary files and has lots of commands available to it, so it can help you maintain projects in many different forms.
The CMake website documents all valid built-in commands and their available parameters, so it's easy to uncover the functions you need, no matter what you're trying to do. This is a simple example, though, so the next command you need is essential—you must define for CMake what code you're building:
add_executable(Hello hello.c)
This sets the name of your compiled binary to Hello
, so functionally, it's the same as running gcc
with -o Hello
in your terminal.
In a complex project, you likely have libraries as well as executables. You can add libraries with the add_library
command.
After you've set what files you want built and marked for installation, you must tell CMake where the finished product should end up once a user installs your application.
In this simple example, only one thing is marked for installation, so you only have to add one install
line to your CMakeLists
. The install
command accepts a few parameters, but in this case, all that's necessary is the TARGETS
parameter followed by the name of the file to install:
install(TARGETS Hello)
Adding files to a CMake project
A software project rarely just delivers code to its users. There's usually some additional data, such as manual or info pages, example projects, or configuration files. You can include arbitrary data in a CMake project using a similar workflow to when you include compiled files: first, add the file to CMakeLists.txt
and then describe how it is to be installed.
For example, to include a directory called assets
with your sample application, you use the file
command, followed by the COPY
and DESTINATION
parameters to tell CMake to copy your additional files into your distributable package:
file(COPY assets DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
The ${CMAKE_CURRENT_BINARY_DIR}
is a special built-in CMake variable representing the path to the directory currently being processed by CMake. In other words, your arbitrary data gets copied to the build directory (this becomes even clearer after you run cmake
, so watch for this to come up again later).
Because data directories tend to be crowded places (take a look in /usr/share
if you don't believe me), it's to everyone's benefit for you to create a subdirectory for your own project, preferably with versioning. You can do this by specifying a new directory within CMAKE_CURRENT_BINARY_DIR
using your chosen project name followed by a special variable named for your project and the VERSION
you set for it in your project declaration:
file(COPY assets DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Hello-${Hello_VERSION}")
Defining install locations
You've defined the file for the build process, so now you must tell CMake where to put it during the install process. Like your main executable, this uses the install
command:
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Hello-${Hello_VERSION}" TYPE DATA)
There are some new parameters here. The DIRECTORY
parameter identifies the data source as a directory (rather than FILE
or SCRIPT
, for instance). You're using the same variables as you used when copying the data files into the build location. Additionally, either a TYPE
or a DESTINATION
must be provided for install
(not both). The TYPE
argument specifies a generic file type, which is placed into a location appropriate to the target system. On Linux, a TYPE DATA
directory usually gets placed into /usr/local/share
or /usr/share
, unless the user or packager has defined a different data location.
That's one of the powerful things about a good build system like CMake. You don't have to worry about exactly where files end up because you know that the user can alert CMake of their preferred defaults and that CMake will build the code to make that work.
Running CMake
CMake has several interfaces. You can use it from your terminal as a command or an interactive application, or you can use its graphical user interface (GUI) front end. I tend to use the terminal command, but I enjoy the other user experiences just as much (they definitely beat scrubbing through Makefiles in search of obscure variables to redefine).
The first step, familiar to anyone who's built their fair share of open source C++ projects, is to create a build
directory, change to it, and then run the cmake ..
command. I'm a lazy typist, so I name my build directory b
, but you can use whatever makes the most sense to you:
$ mkdir b
$ cd b
$ cmake ..
-- The C compiler identification is GNU 11.1.1
-- The CXX compiler identification is GNU 11.1.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /var/home/seth/demo-hello/b
$
This is, more or less, the equivalent of ./configure
in the classic ./configure; make; make install
incantation. A look into your build directory reveals that CMake has generated several new files to help your project come together. There's some CMake data, a regular Makefile (that's 247 lines of code for free, but quite a lot more for complex projects), and the Hello-1.0
data directory containing the arbitrary non-compiled data distributed with this example application:
$ ls
CMakeCache.txt
CMakeFiles
Makefile
Hello-1.0
cmake_install.cmake
Next, you can build. You can do this with CMake using the --build
option, using the current build directory as the source directory:
$ cmake --build .
Scanning dependencies of target Hello
[ 50%] Building C object CMakeFiles/Hello.dir/hello.c.o
[100%] Linking C executable Hello
[100%] Built target Hello
Or you can run the make
command. This reads the Makefile
generated by CMake. In this example, the default action for Make is to compile its target, hello.c
:
$ make
Scanning dependencies of target Hello
[ 50%] Building C object CMakeFiles/Hello.dir/hello.c.o
[100%] Linking C executable Hello
[100%] Built target Hello
$
As you might expect, the Hello
binary executable now exists in your current build directory. Because it's a simple self-contained application, you can run it for testing purposes:
$ ./Hello
Hello open source
$
Finally, you can install with the --install
option. Because I don't want my simple "hello world" application to actually get installed on my system, I set the --prefix
option to redirect CMake's target from the root directory (/
) to a subdirectory in /tmp
:
$ cmake --install . --prefix /tmp/hello/
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/local/bin/Hello
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file1
Alternately, you can run make install
to invoke the install actions of the Makefile. Again, to avoid a demo application to be installed on my system, I set the DESTDIR
variable in this example to redirect the install target to a subdirectory in /tmp
:
$ mkdir /tmp/dist-hello
$ make install DESTDIR=/tmp/dist-hello
[100%] Built target Hello
Install the project...
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/local/bin/Hello
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file1
The output confirms its actions, and the application is installed.
Quick customization
CMake's install prefix (the CMAKE_INSTALL_PREFIX
variable) defaults to /usr/local
, but any CMake variable can be customized when you run cmake
with the -D
option:
$ cmake -DCMAKE_INSTALL_PREFIX=/usr ..
$ make install DESTDIR=/tmp/dist-hello
$ make install DESTDIR=/tmp/dist-hello
[100%] Built target Hello
Install the project...
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/bin/Hello
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0/assets/file1
Any variable used by CMake can be customized in this way.
Interactive CMake
CMake's interactive mode is a friendly and useful method to configure an installation environment. It's a lot to ask your users to know all the possible CMake variables your project uses, so the CMake interactive interface is an easy way for them to discover customization options without looking at Makefiles and CMakeLists.
To invoke an interactive CMake session, use the ccmake
command. There's not much to see for this simple example project, but a big project like the digital audio workstation Rosegarden makes the user interface invaluable.
More CMake
There's much, much more to CMake. As a developer, I enjoy CMake for its simple syntax and extensive documentation, extensibility, and expediency. As a user, I appreciate CMake for its friendly and helpful error messages and user interfaces. If you're not using a build system for your project, take a look at CMake. You, and anyone trying to package your application later, won't be sorry.
3 Comments