Skip to content

Latest commit

 

History

History
699 lines (452 loc) · 23.5 KB

03 CMake - Advanced Scripting and Modules.md

File metadata and controls

699 lines (452 loc) · 23.5 KB

CMake Crash Course - Advanced Scripting and Modules

Author: methylDragon
Contains a syntax reference for CMake. We'll be going through some more nifty stuff you can do with CMake, and some includable modules!!


Pre-Requisites

Assumed knowledge

  • Have a rudimentary understanding of C/C++
    • Since CMake is used to build C/C++ projects!
  • Understood the linkage and build concepts from this tutorial
  • The tutorial is written with Linux users in mind, specifically Ubuntu
    • But the scripting section should apply in general
  • Went through the previous sections of this tutorial

Table Of Contents

  1. Introduction
  2. CMake Advanced Scripting
    2.1 Configuring Files
    2.2 Reading Files
    2.3 Parse Arguments
    2.4 Calling Custom Terminal Commands
    2.5 Generating Files and Triggering Build Events
    2.6 Generator Expressions
    2.7 Try Compile and Try Run
    2.8 CMake Command Reference
  3. Useful CMake Modules
    3.1 Source Introspection: CheckFunctionExists
    3.2 Set Dependent Options: CMakeDependentOption
    3.3 Print Helpers: CMakePrintHelpers
    3.4 Check If Flags are Supported: CheckCXXCompilerFlag
    3.5 Detect Features as Options and Generate Backward Compatibility Implementations: WriteCompilerDetectionHeader
    3.6 Add and Print Feature Summaries: FeatureSummary

1. Introduction

While the previous two sections of the tutorial should more or less give you enough to build and configure projects all on your own, the rabbit hole of CMake goes really deep. So here's a cursory foray into some of the powerful tools and tricks available to you!

We'll be going through some advanced scripting commands, as well as go through some useful CMake modules that you might want to use.

A lot of this tutorial would not be possible without this tutorial, so I'd like to give credit where it is due. Thanks a lot for the great reference!

2. CMake Advanced Scripting

2.1 Configuring Files

go to top

Reference

The configure_file() command copies an input file into an output file, substituting variables (from CMake) referenced in the input into that output file.

In other words, it's a great way for you to to edit code dynamically at build time! And it's all possible using CMake!

Input files are normally specified as such using the .in extension.

So if you wanted to generate a file like rawr.cpp, the input file that you would create will be rawr.cpp.in!

# General call
configure_file(<input> <output>
               [COPYONLY] [ESCAPE_QUOTES] [@ONLY]
               [NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])

# Example usage
configure_file("${PROJECT_SOURCE_DIR}/src/rawr.cpp.in"
               "${PROJECT_SOURCE_DIR}/src/rawr.cpp"
               @ONLY
)

Arguments

Name Description
COPYONLY Disable variable substitution.
ESCAPE_QUOTES Any substituted quotes are C-style escaped. (With backslash.)
@ONLY Disables substitution of ${VAR} references. (Only allows @VAR@ specifications instead of both.)
NEWLINE_STYLE Adjust newline style. (Cannot be used with COPYONLY.)

Newline styles

'UNIX' or 'LF' for \n, 'DOS', 'WIN32' or 'CRLF' for \r\n.

Example Input File

#define MY_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MY_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MY_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MY_VERSION_TWEAK @PROJECT_VERSION_TWEAK@

// You can also have it replace ${PROJECT_VERSION_MAJOR}
// But due to how common that sort of syntax is in some types of files
// It's less recommended, and it's recommended to use @ONLY

This should output something like this (in this case we're on version 1.0.0), assuming the variable was defined in the CMakeLists.txt that called configure_file() with @ONLY engaged.

#define MY_VERSION_MAJOR 1
#define MY_VERSION_MINOR 0
#define MY_VERSION_PATCH 0
#define MY_VERSION_TWEAK 0

// You can also have it replace ${PROJECT_VERSION_MAJOR}
// But due to how common that sort of syntax is in some types of files
// It's less recommended, and it's recommended to use @ONLY

Neat!

2.2 Reading Files

go to top

Reference

Use the file() command to manipulate files! You can technically write to them as well, but here we just use them to read.

You can even apply regex!

Source

# Assuming the canonical version is listed in a single line
# This would be in several parts if picking up from MAJOR, MINOR, etc.
set(VERSION_REGEX "#define MY_VERSION[ \t]+\"(.+)\"")

# Read in the line containing the version
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/include/My/Version.hpp"
    VERSION_STRING REGEX ${VERSION_REGEX})

# Pick out just the version
string(REGEX REPLACE ${VERSION_REGEX} "\\1" VERSION_STRING "${VERSION_STRING}")

# Automatically getting PROJECT_VERSION_MAJOR, My_VERSION_MAJOR, etc.
project(My LANGUAGES CXX VERSION ${VERSION_STRING})

2.3 Parse Arguments

go to top

Create named arguments for functions and macros in CMake!

Note: This feature only works from CMake 3.5 onwards. If you want to use it in a CMake version lower than that, include the CMakeParseArguments module.

function(my_func)
  cmake_parse_arguments(
    NAMED_ARGUMENT_PREFIX
    "OPTION_1;OPTION_2" # These are option variables
    "SINGLE_VAL_1;SINGLE_VAL_2" # These are single non-list variables
    "LIST_VAR_1;LIST_VAR_2"
    ${ARGN} # Then pass in ${ARGN}, which contains all arguments
  )
endfunction()

# So now when you call this
my_func(OPTION_1 SINGLE_VAL_1 rawr LIST_VAR_1 raa rer)

# Inside the function, you should see these variables
# NAMED_ARGUMENT_PREFIX_OPTION_1: True
# NAMED_ARGUMENT_PREFIX_OPTION_2: False
# NAMED_ARGUMENT_PREFIX_SINGLE_VAL_1: rawr
# NAMED_ARGUMENT_PREFIX_SINGLE_VAL_2: <UNDEFINED>
# NAMED_ARGUMENT_PREFIX_LIST_VAR_1: "raa;rer"
# NAMED_ARGUMENT_PREFIX_LIST_VAR_2: <UNDEFINED>

Pretty neat!

2.4 Calling Custom Terminal Commands

go to top

Command Reference

You can use add_custom_target() to create a build pipeline target that executes a certain command. Adding a custom target like this does not produce any output, so it will always run if it is called.

Confusingly enough, the command to run commands is not add_custom_command(), but add_custom_target() instead!

Minimal Call

# General Call
add_custom_target(Name [ALL] [command1 [args1...]]
                  [COMMAND command2 [args2...] ...]
                  [DEPENDS depend depend depend ... ]
                  [BYPRODUCTS [files...]]
                  [WORKING_DIRECTORY dir]
                  [COMMENT comment]
                  [JOB_POOL job_pool]
                  [VERBATIM] [USES_TERMINAL]
                  [COMMAND_EXPAND_LISTS]
                  [SOURCES src1 [src2...]])
                  
# Minimally Practical Call
# This just runs 'echo "RAWR"' on calling make
add_custom_target(target_name ALL echo "RAWR")

If we didn't include the ALL argument, we would have to call make target_name to invoke the custom target, which will then run the command.

This is because the ALL argument adds the custom target to the default build target.

Multiple Calls

We can also specify multiple commands to run! And they will correspondingly run in order, though not necessarily composed into a stateful shell or batch script. If you want that to happen, it is probably better to run a file that is generated (perhaps using configure_file().)

1566144489572

cmake_minimum_required(VERSION 3.10)

add_custom_target(target_name ALL echo "RAWR"
                  COMMAND echo "RAWR_2"
                  COMMAND echo "RAWR_3"
                  COMMAND pwd)

Calls as Dependencies

Alternatively, you may choose to omit the ALL argument and use add_dependencies() to cause commands to run before the compilation of another target, as a sort of custom preprocessing step.

To be safe, please only specify one add_dependency() per custom target specified this way!

add_custom_target(custom_target_name echo "RAWR")
add_dependencies(needy_target_name custom_target_name)

So with this, custom_target_name will be 'built' (that is, have its command run), before needy_target_name is built!

2.5 Generating Files and Triggering Build Events

go to top

Command Reference

Again, confusingly enough, add_custom_command() is used only for running commands that either generate files, or trigger as a result of another target.

Note that this is especially confusing because the latter functionality (triggering as a result of another target) serves almost the same functionality as the pattern in add_custom_target(), where declaring the custom target as a dependency for another target causes the custom target to run.

Using add_custom_command() for this latter functionality is more powerful though, since for add_custom_target(), we are constrained to causing the target to run before the other target is built, whereas for add_custom_command(), we are able to specify when we actually want the commands to run (pre-build, pre-link, and post-build.)

General Calls

# General Call for Generating Files
add_custom_command(OUTPUT output1 [output2 ...]
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [MAIN_DEPENDENCY depend]
                   [DEPENDS [depends...]]
                   [BYPRODUCTS [files...]]
                   [IMPLICIT_DEPENDS <lang1> depend1
                                    [<lang2> depend2] ...]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment]
                   [DEPFILE depfile]
                   [JOB_POOL job_pool]
                   [VERBATIM] [APPEND] [USES_TERMINAL]
                   [COMMAND_EXPAND_LISTS])
                   
# General Call for Build Events
add_custom_command(TARGET <target>
                   PRE_BUILD | PRE_LINK | POST_BUILD
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [BYPRODUCTS [files...]]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment]
                   [VERBATIM] [USES_TERMINAL])

Generating Files

We can specify a custom command to generate a file, and then specify a target that depends on the output of that command in order to ensure that the command runs.

add_custom_command(OUTPUT some_output.h
  COMMAND touch some_output.h # Run this command to produce some_output.h
  WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/include" # Specify working directory
  COMMENT "I WILL ECHO THIS BEFORE THIS CUSTOM COMMAND IS RUN"
  VERBATIM # All arguments are escaped properly
)
add_custom_target(command_target_name DEPENDS some_output.h)

So in effect, calling make command_target_name will attempt to build the target, but since the target depends on the OUTPUT of the specified add_custom_command(), the command will be run!

Note that once the dependency is built, the command will no longer be run. And of course, if no targets are called that depend on the output of the custom command, the command will not be run.

Of course, if you want to write a script in another language (like Python) to produce the output file, you can! In fact, it's probably recommended.

Just remember to call it in the command argument, and to also ensure you specify any dependencies.

Build Events

Alternatively, add_custom_command() has an alternative call signature that allows for build events to trigger during the build process of a target.

1566146742207

add_custom_target(associated_target)
add_custom_command(TARGET associated_target PRE_BUILD COMMAND echo "Rawr")

Now, running make associated_target (after you run cmake . to generate the build pipeline) should result in the command running, and then associated_target getting 'built'.

You can also specify PRE_LINK or POST_BUILD to change the exact timing of the invocation of the command during the building of any specific target.

2.6 Generator Expressions

go to top

Full List of Generator Expressions

Generator expressions are expressions that are evaluated at build time or install, not config time like the if() statement is.

But generally, generator expressions are expressions that, if the condition for the specific generator expression is 'correct', will evaluate to a certain value, or nothing/something else if the condition is 'wrong'. (I'm not using true and false here because not all generator expressions work that way. And actually, there are some expressions that will evaluate to 0 or 1, or affect the input in some way (like turning all characters in a string uppercase.))

  • The gist is that all expressions will generate something (even if that something is an empty string), hence the name!
  • More importantly, you can nest them! This lets you do some really cool stuff!

Let's go through a simple example, using some commonly used expressions.

# But first, let's look at a really basic pair of generator expressions

$<0:...>
# Empty string (ignores ...)

$<1:...>
# Content of ...

# So,
$<1: "AAA"> # Will become "AAA" at compile time

So, imagine what would happen if you did this?

$<$<CXX_COMPILER_ID:GNU>:-Wno-psabi>

That expression will be replaced with -Wno-psabi if your system's C++ compiler ID is GNU (that is, if you are using gcc as your compiler!) If you aren't it'll evaluate to nothing!

This is really powerful because you can use generator expressions to optionally add flags, or options at compile time to fine-tune your build configuration based off of your the current configuration!

You could technically do this with variables and conditionals too, but that happens at configuration time, and if you're checking for many different conditions, can get quite unwieldy.

Note: Because generator expressions are evaluated at build time, using them with message() won't work, since message() resolves at compile time.

Echo Snippet

If you want to see how a generator expression behaves, you can use this code snippet to give it a try.

cmake_minimum_required(VERSION 3.10)

set(LIST_VAR <EXPRESSION_HERE>)

# Example
set(LIST_VAR $<UPPER_CASE:"rawr">)

# Add a custom make target called print, which will echo the generator expression
add_custom_target(print
        ${CMAKE_COMMAND} -E echo ${LIST_VAR}
)

Run it with

cmake .
make print

You should get an echo of the generator expression's output. In this case, RAWR.

1565847804604

2.7 Try Compile and Try Run

go to top

You can attempt to try to compile a target, and try to run some source files at configure time.

Try Compile

Reference

The result variable will contain TRUE or FALSE if the compilation succeeded or not.

try_compile(<result_var> <bindir> SOURCES try_this_source.cpp)

You can also try with a full project!

try_compile(<result_var> <bindir> <srcdir> <project_name>)

Try Run

Reference

You will try to both compile, and then run!

try_run(<run_result_var> <compile_result_var> <bindir> <srcfile>)

2.8 CMake Command Reference

go to top

Here's a list of all CMake commands in case you want to see what I haven't covered here.

3. Useful CMake Modules

There are so many modules! Here's a selection of some of them, courtesy of this tutorial and this tutorial.

3.1 Source Introspection: CheckFunctionExists

go to top

Reference

CMake can check for function definitions!

You can then use the knowledge of whether certain functions are defined to alter input files that CMake can configure, which then correspondingly can be used in source code to alter behaviour!

Here's an example.

CMakeLists.txt

# Include the module
include (CheckFunctionExists)

# Check if log() and exp() exists
# Depending on whether they do, set the corresponding variable
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)

Config.h.in

Your input pre-configured file that will eventually be imported into your source file

// This will get replaced with #define HAVE_LOG or nothing depending on
// whether CMake found the log() function or not
#cmakedefine HAVE_LOG

// This will get replaced with #define HAVE_EXP or nothing depending on
// whether CMake found the exp() function or not
#cmakedefine HAVE_EXP

source.cpp

#include "Config.h"

#if defined (HAVE_LOG) && defined (HAVE_EXP)
  // Do some stuff using log() and exp()
#else
  // Use some other implementation that doesn't use log() and exp()
#endif

Pretty nifty!

3.2 Set Dependent Options: CMakeDependentOption

go to top

Reference

Set the default state of an option given the state of other options.

include(CMakeDependentOption)

# Like ternary operators!
# Set BUILD_TESTS ON if VAL1 and VAL2 are ON, else OFF
cmake_dependent_option(BUILD_TESTS "Build your tests" ON "VAL1;VAL2" OFF)

Which is actually a shorthand for

if(VAL1 AND VAL2)
    set(BUILD_TESTS_DEFAULT ON)
else()
    set(BUILD_TESTS_DEFAULT OFF)
endif()

option(BUILD_TESTS "Build your tests" ${BUILD_TESTS_DEFAULT})

if(NOT BUILD_TESTS_DEFAULT)
    mark_as_advanced(BUILD_TESTS)
endif()

3.3 Print Helpers: CMakePrintHelpers

go to top

Reference

include(CMakePrintHelpers)

# Print properties of targets
cmake_print_properties([TARGETS target1 ..  targetN]
                      [SOURCES source1 .. sourceN]
                      [DIRECTORIES dir1 .. dirN]
                      [TESTS test1 .. testN]
                      [CACHE_ENTRIES entry1 .. entryN]
                      PROPERTIES prop1 .. propN )

# Print variable names and values
cmake_print_variables(var1 var2)

3.4 Check If Flags are Supported: CheckCXXCompilerFlag

go to top

Reference

Check if a build flag is supported, and store the result of that check in an output variable. (This means that you can ultimately pass it to your code using configure_file()!)

include(CheckCXXCompilerFlag)
check_cxx_compiler_flag(-someflag OUTPUT_VARIABLE)

3.5 Detect Features as Options and Generate Backward Compatibility Implementations: WriteCompilerDetectionHeader

go to top

Reference

This one is a little bit weird. So we'll have to go through some examples.

You will need to specify what compilers you want to check compatibility for.

We'll talk through this topic using the material from here.

Suppose we had this in our CMakeLists.txt file,

include(WriteCompilerDetectionHeader)
write_compiler_detection_header(
    FILE foo_compiler_detection.h
    PREFIX FOO
    COMPILERS GNU MSVC
    FEATURES cxx_constexpr cxx_nullptr
)

A header file will be generated, with FOO_COMPILER_CXX_CONSTEXPR defined if constexpr is available.

We can then write some code that includes this header file, and write alternative implementations if a particular feature is not available to the compiler.

#include "foo_compiler_detection.h"

#if FOO_COMPILER_CXX_CONSTEXPR

// implementation with constexpr available
constexpr int bar = 0;

#else

// implementation with constexpr not available
const int bar = 0;

#endif

Additionally, FOO_CONSTEPR and FOO_NULLPTR will be defined and expand to the corresponding feature keyword, or either a compatibility implementation, or nothing, depending on whether there is one available to CMake or not.

#include "foo_compiler_detection.h"

FOO_CONSTEXPR int bar = 0;

void baz(int* p = FOO_NULLPTR);

So in this case, if the compiler does not support constexpr, FOO_CONSTEXPR will resolve to nothing instead, since it has no backwards compatible implementations available to CMake. But luckily, FOO_NULLPTR will resolve to NULL in the absence of any nullptr feature.

3.6 Add and Print Feature Summaries: FeatureSummary

go to top

Reference

You may add package information, as well as mark options as part of the feature summary.

In this case, we're adding descriptions to packages we include in our project.

1566149145967

cmake_minimum_required(VERSION 3.10)
include(FeatureSummary)
project(my_project)

# Add information about the use of specific packages in your project
find_package(OpenMP)
set_package_properties(OpenMP PROPERTIES
    URL "http://www.openmp.org"
    DESCRIPTION "Parallel compiler directives"
    PURPOSE "This is what it does in my package")
    
# Add information about a feature with a given name
# In this case, the feature WITH_OPENMP will be listed under ENABLED_FEATURES
# if OpenMP_CXX_FOUND is TRUE, and in DISABLED_FEATURES if not
add_feature_info(WITH_OPENMP OpenMP_CXX_FOUND "OpenMP  (Thread safe FCNs only)")

# Then we can print feature info!
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
    feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES PACKAGES_FOUND)
    
    # Or even log it to a file!
    feature_summary(FILENAME ${CMAKE_CURRENT_BINARY_DIR}/features.log WHAT ALL)
endif()

Nifty!

                            .     .
                         .  |\-^-/|  .    
                        /| } O.=.O { |\


Yeah! Buy the DRAGON a COFFEE!