From 4046b4e31bee82429290f250b92b8e9e4604c896 Mon Sep 17 00:00:00 2001 From: Henry Fredrick Schreiner Date: Thu, 5 Apr 2018 12:30:54 +0200 Subject: [PATCH] Filled in final missing sections --- README.md | 2 +- SUMMARY.md | 6 +- chapters/basics/comms.md | 55 ++++++++++++++-- chapters/basics/functions.md | 121 ++++++++++++++++++++++++++++++++--- chapters/basics/programs.md | 46 +++++++++++-- 5 files changed, 206 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 38598eb..850f9e4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ And CMake 3.11 is significantly faster, as well! {% hint style='working' %} -This document is a work in progress. You can raise an issue or put in a merge request on [GitLab](https://gitlab.com/CLIUtils/modern-cmake). +This book is meant to be a living document. You can raise an issue or put in a merge request on [GitLab](https://gitlab.com/CLIUtils/modern-cmake). {% endhint %} In short, here are the most likely questions in your mind if you are considering Modern CMake: diff --git a/SUMMARY.md b/SUMMARY.md index d88f770..08f02a6 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -7,10 +7,10 @@ * [What's new in CMake](chapters/intro/newcmake.md) * [Introduction to the Basics](chapters/basics.md) * [Variables and the Cache](chapters/basics/variables.md) - * [Programming in CMake (X)](chapters/basics/functions.md) - * [Communicating with your code (X)](chapters/basics/comms.md) + * [Programming in CMake](chapters/basics/functions.md) + * [Communicating with your code](chapters/basics/comms.md) * [How to Structure Your Project](chapters/basics/structure.md) - * [Running Other Programs (X)](chapters/basics/programs.md) + * [Running Other Programs](chapters/basics/programs.md) * [Adding Features](chapters/features.md) * [C++11 and Beyond](chapters/features/cpp11.md) * [Small but common needs](chapters/features/small.md) diff --git a/chapters/basics/comms.md b/chapters/basics/comms.md index 6aeae3d..e504c29 100644 --- a/chapters/basics/comms.md +++ b/chapters/basics/comms.md @@ -1,9 +1,54 @@ # Communication with your code -## TODO: Configure File +## Configure File -## TODO: Reading files +CMake allows you to access CMake variables from your code using `configure_file`. This command copies a file (traditionally ending in `.in` from one place to another, substituting all CMake variables it finds. If you want to avoid replacing existing `${}` syntax in your input file, use the `@ONLY` keyword. There's also a `COPY_ONLY` keyword if you are just using this as a replacement for `file(COPY`. + +This functionality is used quite frequently; for example, on `Version.h.in`: + +#### Version.h.in + +```cpp +#pragma once + +#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@ +#define MY_VERSION "@PROJECT_VERSION@" +``` + +#### CMake lines: +```cmake +configure_file ( + "${PROJECT_SOURCE_DIR}/include/My/Version.h.in" + "${PROJECT_BINARY_DIR}/include/My/Version.h" +) +``` + +You should include the binary include directory as well when building your project. If you want to put any true/false variables in a header, CMake has C specific `#cmakedefine` and `#cmakedefine01` replacements to make appropriate define lines. + +You can also (and often do) use this to produce `.cmake` files, such as the configure files (see the section on configuring). + +## Reading files + +The other direction can be done too; you can read in something (like a version) from your source files. If you have a header only library that you'd like to make available with or without CMake, for example, then this would be the best way to handle a version. This would look something like this: + +```cmake +# 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}) +``` + +Above, `file(STRINGS file_name variable_name REGEX regex)` picks lines that match a regex; and the same regex is used to then pick out the parentheses capture group with the version part. Replace is used with back substitution to output only that one group. -{% hint style='working' %} -This document is a work in progress. You can raise an issue or put in a merge request on [GitLab](https://gitlab.com/CLIUtils/modern-cmake). -{% endhint %} diff --git a/chapters/basics/functions.md b/chapters/basics/functions.md index bb3339a..29e2754 100644 --- a/chapters/basics/functions.md +++ b/chapters/basics/functions.md @@ -1,16 +1,121 @@ # Programming in CMake -## TODO: Control flow +## Control flow -## TODO: Generator expressions +CMake has an [if statement], though over the years is has become rather complex. There are a series of all caps keywords you can use inside an if statement, and you can often refer to variables by either directly by name or using the `${}` syntax (the if statement historically predates variable expansion). An example if statement: -## TODO: Macros +```cmake +if(variable) + # If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number +else() + # If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND` +endif() +# If variable does not expand to one of the above, CMake will expand it then try again +``` -## TODO: Functions +Since this can be a little confusing if you explicitly put a variable expansion, like `${variable}`, due to the potential expansion of an expansion, a policy was added in CMake 3.1+ that keeps a quoted expansion from being expanded yet again. So, as long as the minimum version of CMake is 3.1+, you can do: -## TODO: Arguments +```cmake +if("${variable}") + # True if variable is not false-like +else() + # Note that undefined variables would be `""` thus false +endif() +``` + +There are a variety of keywords as well, such as: + +* Unary: `NOT`, `TARGET`, `EXISTS` (file), `DEFINED`, etc. +* Binary: `STREQUAL`, `AND`, `OR`, `MATCHES` (regular expression), `VERSION_LESS`, `VERSION_LESS_EQUAL` (CMake 3.7+), etc. +* Parentheses can be used to group -{% hint style='working' %} -This document is a work in progress. You can raise an issue or put in a merge request on [GitLab](https://gitlab.com/CLIUtils/modern-cmake). -{% endhint %} +## Generator expressions + +[Generator expressions] are really powerful, but a bit odd and specialized. Most CMake commands happen at configure time, include the if statements seen above. But what if you need logic to occur at build time or even install time? Generator expressions were added for this purpose.[^1] They are evaluated in target properties. + +The simplest generator expressions informational expressions, and are of the form `$`, and they evaluate to a piece of information relevant for the current configuration. The other form is `$`, where `KEYWORD` is a keyword that controls the evaluation, and value is the item to evaluate (an informational expression keyword is allowed here, too). If KEYWORD is a generator expression or variable that evaluates to 0 or 1, `value` is substituted +if 1 and not if 0. You can nest generator expressions, and you can use variables to make reading nested variables bearable. Some +expressions allow multiple values, separated by commas.[^2] + +If you want to put a compile flag only for the DEBUG configuration, for example, you could do: + +``` +target_compile_options(MyTarget PRIVATE "$<$:--my-flag>") +``` + +This is a newer, better way to add things than using specialized `*_DEBUG` variables, and generalized to all the things generator expressions support. Note that you should never, never use the configure time value for the current configuration, because multi-configuration generators like IDEs do not have a "current" configuration at configure time, only at build time through generator expressions and custom `*_` variables. + +Other common uses for generator expressions: + +* Limiting an item to a certain language only, such as CXX, to avoid it mixing with something like CUDA, or wrapping it so that it is different depending on target language. +* Accessing configuration dependent properties, like target file location. +* Giving a different location for build and install directories. + +That last one is very common. You'll see something like this in almost every package that supports installing: + +```cmake + target_include_directories( + MyTarget + PUBLIC + $ + $ + ) +``` + +[^1]: They act as if they are evaluated at build/install time, though actually they are evaluated for each build configuration. +[^2]: The CMake docs splits expressions into Informational, Logical, and Output. + +## Macros and Functions + +You can define your own CMake functions or macros easily. The only difference between a function and a macro is scope; macros don't have one. So, if you set a variable in a function and want it to be visible outside, you'll need `PARENT_SCOPE`. Nesting functions therefore is a bit tricky, since you'll have to explicitly set the variables you want visible to the outside world to `PARENT_SCOPE` in each function. But, functions don't "leak" all their variables like macros do. For the +following examples, I'll use functions. + +An example of a simple function is as follows: + +```cmake +function(SIMPLE REQUIRED_ARG) + message(STATUS "Simple arguments: ${REQUIRED_ARG}, followed by ${ARGV}") + set(${REQUIRED_ARG} "From SIMPLE" PARENT_SCOPE) +endfunction() + +simple(This) +message("Output: ${This}") +``` + +If you want positional arguments, they are listed explicitly, and all other arguments are collected in `ARGV`. You have to work around the fact that CMake does not have return values by setting variables. In the example above, you can explicitly give a variable name to set. + +## Arguments + +CMake has a named variable system that you've already seen in most of the build in CMake functions. You can use it with the [`cmake_parse_arguments` function][cmake_parse_arguments]. If you want to support a version of CMake less than 3.5, you'll want to also include the CMakeParseArguments module, which is where it used to live before becoming a built in command. Here is an example of how to use it: + +```cmake +function(COMPLEX) +cmake_parse_arguments( + COMPLEX_PREFIX + "SINGLE;ANOTHER" + "ONE_VALUE;ALSO_ONE_VALUE" + "MULTI_VALUES" +) + +endfunction() + +complex(SINGLE ONE_VALUE value MULTI_VALUES some other values) +``` + +Inside the function after this call, you'll find: + +```cmake +COMPLEX_PREFIX_SINGLE = TRUE +COMPLEX_PREFIX_ANOTHER = FALSE +COMPLEX_PREFIX_ONE_VALUE = "value" +COMPLEX_PREFIX_ALSO_ONE_VALUE = +COMPLEX_PREFIX_MULTI_VALUES = "some;other;values" +``` + +If you look at the official page, you'll see a slightly different method using set to avoid explicitly writing the semicolons in the list; feel free to use the structure you like best. You can mix it with the positional arguments listed above; any remaining arguments (therefore optional positional arguments) are in `COMPLEX_PREFIX_UNPARSED_ARGUMENTS`. + +[if statement]: https://cmake.org/cmake/help/latest/command/if.html +[Generator expressions]: https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html +[cmake_parse_arguments]: https://cmake.org/cmake/help/latest/command/cmake_parse_arguments.html + diff --git a/chapters/basics/programs.md b/chapters/basics/programs.md index f9f93c5..23b46ec 100644 --- a/chapters/basics/programs.md +++ b/chapters/basics/programs.md @@ -1,13 +1,45 @@ # Running other programs -## TODO: Running a command at configure time +## Running a command at configure time -## TODO: Running a command at build time +Running a command at configure time is relatively easy. Use [`execute_process`][execute_process] to run a process and access the results. It is generally a good idea to avoid hard coding a program path into your CMake; you can use `${CMAKE_COMMAND}`, `find_package(Git)`, or `find_program` to get access to a command to run. Use `RESULT_VARIABLE` to check the return code and `OUTPUT_VARIABLE` to get the output. -## TODO: Included common utilities +Here is an example that updates all git submodules: -(Like `cmake -E`) +```cmake +find_package(Git QUIET) + +if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") + execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT) + if(NOT GIT_SUBMOD_RESULT EQUAL "0") + message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") + endif() +endif() +``` + +## Running a command at build time + +Build time commands are a bit tricker. The main complication comes from the target system; when do you want your command to run? Does it produce an output that another target needs? With this in mind, here is an example that calls a Python script to generate a header file: + +```cmake +find_package(PythonInterp REQUIRED) +add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp" + COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/GenerateHeader.py" --argument + DEPENDS some_target) + +add_custom_target(generate_header ALL + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp") + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp DESTINATION include) +``` + +Here, the generation happens after `some_target` is complete, and happens when you run make without a target (`ALL`). If you make this a dependency of another target with `add_dependencies`, you could avoid the `ALL` keyword. Or, you could require that a user explicitly builds the `generate_header` target when making. + +## Included common utilities + +A useful tool in writing CMake builds that work cross-platform is `cmake -E ` (seen in CMake files as `${CMAKE_COMMAND} -E`). This mode allows CMake to do a variety of things without calling system tools explicitly, like `copy`, `make_directory`, and `remove`. It is mostly used for the build time commands. Note that the very useful `create_symlink` mode only works on Unix systems. [See the docs](https://cmake.org/cmake/help/latest/manual/cmake.1.html#command-line-tool-mode). + +[execute_process]: https://cmake.org/cmake/help/latest/command/execute_process.html -{% hint style='working' %} -This document is a work in progress. You can raise an issue or put in a merge request on [GitLab](https://gitlab.com/CLIUtils/modern-cmake). -{% endhint %}