Working with CMake

From Inkscape Wiki
Jump to navigation Jump to search

This is a developer-oriented introduction to CMake and how to modify the CMake build scripts.

We assume you already know the basics of how to use cmake to configure and build Inkscape (if not, see Inkscape's README).


Functions and Variables

cmake has its own language and syntax, but it's pretty simple. Essentially, everything is a function call.

   message("Hello world!")

Even just setting variables is a function call:

   set(MYVAR "foobar")

Function calls have parenthesis but arguments aren't separated with commas, just whitespace. It's fine to add newlines between arguments inside the parens. CMake isn't strict about casing for function names, so SET() and set() are equivalent, but it is strict about variable name casing. We'll adopt the convention of keeping function names lower case, and variable names upper case.

Strings don't always have to be quoted, although it's also a good convention to follow. So, these are all functionally equivalent in this case:

   SET(MYVAR "foobar")
   set(MYVAR
       foobar)
   SET(MYVAR foobar)

Variables are referenced using a dollar sign and curly brackets:

   set(MYVAR "foobar")
   message("${MYVAR}")


Your First CMake Script

You now know enough to create a trivial cmake program.

Create an empty directory and open a file named 'CMakeLists.txt' in your favorite text editor.

   $ mkdir cmake-tutorial
   $ cd cmake-tutorial
   $ gedit CMakeLists.txt

Type the following lines into this CMakeLists.txt file:

   set(MYVAR "foobar")
   message("${MYVAR}")

Save the text file and exit your editor. The folder cmake-tutorial is your source folder from cmakes perspective and you need a build directory to run cmake without not mess up the source folder with build artefacts:

   $ cd ..
   $ mkdir cmake-tutorial-build

Now run cmake in the build directory, specifying the path to the source directory:

   $ cd cmake-tutorial-build
   $ cmake ../cmake-tutorial
   foobar
   -- Configuring done
   -- Generating done
   -- Build files have been written to: /tmp/cmake-tutorial

CMake will automatically recognize "CMakeLists.txt" as its config file. CMake assumes that the current directory is where we want all our stuff built to. The '.' argument to the cmake command tells CMake to look in the current directory for source files.


Print full command line used when building

When you produce Makefiles using cmake, and make fails, you can get verbose output e.g. the full gcc command line used for compilation, by adding VERBOSE=1 to make:

   $ make VERBOSE=1

Clean up cmake cache

If you end up in a dead-end with things not building anymore, you can clean the cmake cached files by issuing

   $ make clean-cmake-files

.. followed by regenerating the Makefiles:

   $ cmake ../inkscape

Note that this will require a full-rebuild of the whole of Inkscape

Pre-defined Variables

As you can imagine, CMake provides a broad set of pre-defined variables that relate to build systems.

It has a bunch to help determine what platform the build is building for:

   message("UNIX:  ${UNIX}")
   message("APPLE: ${APPLE}")
   message("WIN32: ${WIN32}")
   message("MSVC:  ${MSVC}")

Prints for me:

   UNIX:  1
   APPLE: 
   WIN32: 
   MSVC:  

CMake also has a slew of variables to keep track of directories that things should go to:

   PROJECT_SOURCE_DIR
   CMAKE_SOURCE_DIR
   CMAKE_CURRENT_SOURCE_DIR
   PROJECT_BINARY_DIR
   CMAKE_BINARY_DIR
   CMAKE_CURRENT_BINARY_DIR
   CMAKE_INSTALL_PREFIX

Let's take a closer look at these:

   message("PROJECT_NAME              ${PROJECT_NAME}")
   message("PROJECT_SOURCE_DIR        ${PROJECT_SOURCE_DIR}")
   message("CMAKE_SOURCE_DIR          ${CMAKE_SOURCE_DIR}")
   message("CMAKE_CURRENT_SOURCE_DIR  ${CMAKE_CURRENT_SOURCE_DIR}")
   message("PROJECT_BINARY_DIR        ${PROJECT_BINARY_DIR}")
   message("CMAKE_BINARY_DIR          ${CMAKE_BINARY_DIR}")
   message("CMAKE_CURRENT_BINARY_DIR  ${CMAKE_CURRENT_BINARY_DIR}")
   message("CMAKE_INSTALL_PREFIX      ${CMAKE_INSTALL_PREFIX}")

For me this prints out:

   PROJECT_NAME              Project
   PROJECT_SOURCE_DIR        /tmp/cmake-tutorial
   CMAKE_SOURCE_DIR          /tmp/cmake-tutorial
   CMAKE_CURRENT_SOURCE_DIR  /tmp/cmake-tutorial
   PROJECT_BINARY_DIR        /tmp/cmake-tutorial
   CMAKE_BINARY_DIR          /tmp/cmake-tutorial
   CMAKE_CURRENT_BINARY_DIR  /tmp/cmake-tutorial
   CMAKE_INSTALL_PREFIX      /usr/local

The 'source' vs. 'binary' variables are the same if the user is doing an in-tree build but differ if out-of-tree. Generally, if you do out-of-tree builds yourself, you can be reasonably confident your code will work either way.

The 'current' variables come into play when you have a directory hierarchy with CMakeList.txt files at various levels. Variables like CMAKE_CURRENT_SOURCE_DIR refer to the currently processing child location in the tree, whereas CMAKE_SOURCE_DIR will always reference the root of the source tree.

Also, be aware that the user is able to set their own installation prefix, so be careful about assumptions about where things will be installed.

Let's try building our example in a more complex way to see how these variables change:

   $ mkdir build install
   $ cd build && cmake .. -DCMAKE_INSTALL_PREFIX=../install
   ...
   PROJECT_NAME              Project
   PROJECT_SOURCE_DIR        /tmp/cmake-tutorial
   CMAKE_SOURCE_DIR          /tmp/cmake-tutorial
   CMAKE_CURRENT_SOURCE_DIR  /tmp/cmake-tutorial
   PROJECT_BINARY_DIR        /tmp/cmake-tutorial/build
   CMAKE_BINARY_DIR          /tmp/cmake-tutorial/build
   CMAKE_CURRENT_BINARY_DIR  /tmp/cmake-tutorial/build
   CMAKE_INSTALL_PREFIX      /tmp/cmake-tutorial/install
   ...


Conditionals

'If' statements are pretty fundamental in all languages, but particularly so for configuring software. Here's how to do an if statement in CMake:

   set(MYVAR "foobar")
   if(MYVAR STREQUAL "foobar")
       set(FOOBAR "yes")
   endif()
   if(FOOBAR)
       message("Hello world!")
   elseif(NOT DEFINED FOOBAR)
       message("Your FOOBAR is fubar.")
   else()
       message("Goodbye, cruel world...")
   endif()

Couple things to note - there's none of the usual ==, <, etc. boolean operators. Instead use EQUAL and LESS for numerical comparisons, STREQUAL, and STRLESS for strings. Other frequently used tests include EXISTS, DEFINED, AND, OR, and NOT. MATCHES provides a handy way to do regular expression testing on strings. VERSION_EQUAL, VERSION_LESS, and VERSION_GREATER are helpful for testing release version strings. There's a bunch of tests relating to checking status on files. Also notice in the second if statement, that FOOBAR's value of "yes" is automatically recognized as a true value. "no" is a false value.

For a complete list of available unary and binary tests and what CMake recognizes as true and false values, see https://cmake.org/cmake/help/v3.0/command/if.html.


Strings

Basic text manipulation in CMake can be done via the string() routine. The first argument to the string() function is the operation to be performed - CONCAT, SUBSTRING, LENGTH, REGEX, COMPARE, MD5, etc.

   cmake_minimum_required(VERSION 2.8)
   set(A "foobar")
   string(SUBSTRING "${A}" 3 3 B)
   string(TOUPPER "${B}" C)
   message("${C}")

Prints

   BAR

Did you notice in this last example how we had a cmake_minimum_required() call?

CMake has grown in functionality since it was originally first introduced, and the cmake_minimum_required() let's us specify what level of compatibility our script expects. After all, the version of cmake installed on different users' systems is going to vary considerably so this is a very basic thing to check.

I don't know if there is a website somewhere that indicates what cmake functionality is available in which cmake versions. However, cmake seems to be good about warning us when we use something that requires a specific version of cmake, and warns us in this example if we don't specify what we require. Obviously we don't want to set the version *too* recent or else users will be unable to run cmake to build Inkscape!


Lists

CMake strives to make handling lists of strings straightforward and easy. Basically, it handles lists as semi-colon delimited strings, and automatically handles converting back and forth:

   set(FOOLIST a b c d e)
   message("${FOOLIST}")
   message(${FOOLIST})

Prints out

   a;b;c;d;e
   abcde

The list() function provides operations such as APPEND, REVERSE, SORT, REMOVE_AT, REMOVE_ITEM, REMOVE_DUPLICATES, GET, etc. similar to string.

Iterating through the items can be done with a for loop:

   cmake_minimum_required(VERSION 2.8)
   set(FOOLIST a b c d e)
   list(REMOVE_AT FOOLIST 1)
   list(REMOVE_AT FOOLIST 2)
   foreach(v ${FOOLIST})
       message(${v})
       list(APPEND LETTERS ${v})
   endforeach()
   message(${LETTERS})


Prints out

   a
   c
   e
   ace


Files

Configuring a software project often involves fiddling with files. Configuration files may need settings changed in them. Generated code files need generated. Other files may need stuff substituted in them or read out of them. CMake has a wealth of functionality to help here.

The most basic would be reading the contents of a file into a variable consisting of a list of strings:

   file(STRINGS /usr/share/inkscape/palettes/inkscape.gpl INKSCAPE_PALETTE)
   list(GET INKSCAPE_PALETTE 0 1 PALETTE_NAME)
   message("${PALETTE_NAME}")

Prints out:

   GIMP Palette;Name: Inkscape default

Like list() and string(), the first argument to file() is the operation to perform. Other operations are WRITE, GLOB, REMOVE, TO_CMAKE_PATH, TIMESTAMP, COPY, and a bunch more. This routine is pretty powerful, as you can see from the file() documentation.


Building and Installing

Now that we've done a quick look at CMake's basic syntax, you should be comfortable with tinkering with Inkscape's CMakeLists.txt and other CMake config files. Now on to the nitty gritty of how to actually build code files into executables and get them installed on the system.

There are plenty of detailed tutorials out there for how to do this from scratch, which I'll suggest perusing if you're creating a cmake project from scratch. But if you're working on Inkscape's build system you really just need to know the key bits and pieces to look for.

First, you'll need a list of source code files. CMake's file() function has a GLOB operation that enables it to automatically find source files, but this is generally considered bad practice, so we'll typically list the source code files explicitly somewhere.

   set(inkscape_SRC
       attributes.h
       attributes.cpp
       ...
       version.cpp
       version.h
   )

These source files could be directly associated with an executable, but in Inkscape we compile most of the source code into an object library, using the add_library() function:

   add_library(inkscape_base OBJECT ${inkscape_SRC})

This way we can create both the inkscape and inkview executables using the same core codebase, with CMake's add_executable():

   add_executable(inkscape main.cpp $<TARGET_OBJECTS:inkscape_base>)
   add_executable(inkview inkview.cpp $<TARGET_OBJECTS:inkscape_base>)

We also need to link external libraries into the mix, using target_link_libraries():

   set(INKSCAPE_TARGET_LIBS inkscape nrtype_LIB croco_LIB avoid_LIB 2geom_LIB ...)
   target_link_libraries(inkscape ${INKSCAPE_TARGET_LIBS})
   target_link_libraries(inkview ${INKSCAPE_TARGET_LIBS})

Finally, we install the two executables via something like:

   install(
       PROGRAMS      ${EXECUTABLE_OUTPUT_PATH}/inkscape
                     ${EXECUTABLE_OUTPUT_PATH}/inkscape
       DESTINATION   ${CMAKE_INSTALL_PREFIX}/bin
   )

Inkscape's Build File Structure

To keep things manageable we split code out of CMakeLists.txt into more specialized files, whose logic all feeds back up to the top CMakeLists.txt. There are a few different ways this is done:

Our codebase is split into subdirectories containing various libraries and other collections of C++ files. We use CMake's add_subdirectory() to register each of these subdirs. CMake then recursively navigates into them, looks for a sub-CMakeLists.txt and then processes it.

The include() function is used to insert other CMake files as snippets into the current file. This can be handy for building up a collection of custom functions and macros. In Inkscape we typically place these into the CMakeScripts/ subdirectory and name the files with the *.cmake extension.

find_package() is critical for detecting and handling external dependencies, and deserves special mention. The first argument to this routine is the name of a dependency, which should correspond to a File<Dependency>.cmake file in the CMakeScripts/Modules/ directory.

So, to add a new dependency library Foo to Inkscape, you first must find or write a FindFoo.cmake file and put it at CMakeScripts/Modules/FindFoo.cmake. Then you use it like this:

   find_package(Foobar)
   if(FOOBAR_FOUND)
       set(WITH_FOOBAR ON)
       list(APPEND INKSCAPE_INCS_SYS ${FOOBAR_INCLUDE_DIRS})
       list(APPEND INKSCAPE_LIBS ${FOOBAR_LIBRARIES})
       add_definitions(${FOOBAR_DEFINITIONS})
   else()
       set(WITH_FOOBAR OFF)
   endif()

The exact logic will vary from dependency to dependency; review the Find<Dependency>.cmake file to see what variables it exports. For dependencies that are required, you use a REQUIRED parameter and can skip the check if its found (since cmake will bail if it isn't).

   find_package(Baz REQUIRED)
   list(APPEND INKSCAPE_INCS_SYS ${BAZ_INCLUDE_DIRS})
   list(APPEND INKSCAPE_LIBS ${BAZ_LIBRARIES})
   add_definitions(${BAZ_DEFINITIONS})


Other Notable CMake Functions

Let's take a quick overview of a few of the other common function calls that you'll run across in Inkscape's CMake files.

configure_file() is a convenient way to process template files and substitute parameters in them with the values of variables within the cmake script. A common use would be to substitute the current release version number into C files or documentation, or to fill in configuration values into a config.h.in template.

option() lets you establish cmake options the user can use to control the build. This is typically used to allow the user to control whether to compile in various sub-modules or to specify alternate libraries.

add_custom_target() is used to create additional make targets, like 'make dist', 'make uninstall', 'make check', and so on. You can provide it with a separate CMake file to be executed, or simply list a sequence of COMMANDS for it to invoke.

add_definitions