mirror of
https://github.com/Kitware/CMake.git
synced 2025-10-21 14:40:48 +08:00

This is a full re-write of the CMake Tutorial for CMake 3.23, both the functionality it provides, as well as the modern workflows that developers use when interfacing with CMake. Issue: #22663, #23086, #23799, #26053, #26105, #26153, #26914
584 lines
18 KiB
ReStructuredText
584 lines
18 KiB
ReStructuredText
Step 2: CMake Language Fundamentals
|
|
===================================
|
|
|
|
In the previous step we rushed through and handwaved several aspects of the
|
|
CMake language which is used within ``CMakeLists.txt`` in order to get useful,
|
|
building programs as soon as possible. However, in the wild we encounter
|
|
a great deal more complexity than simply describing lists of source and
|
|
header files.
|
|
|
|
To deal with this complexity CMake provides a Turing-complete domain-specific
|
|
language for describing the process of building software. Understanding the
|
|
fundamentals of this language will be necessary as we write more complex
|
|
CMLs and other CMake files. The language is formally known as
|
|
":manual:`CMake Language <cmake-language(7)>`", or more colloquially as CMakeLang.
|
|
|
|
.. note::
|
|
The CMake Language is not well suited to describing things which are not
|
|
related to building software. While it has some features for general purpose
|
|
use, developers should use caution when solving problems not directly related
|
|
to their build in CMake Language.
|
|
|
|
Oftentimes the correct answer is to write a tool in a general purpose
|
|
programming language which solves the problem, and teach CMake how to invoke
|
|
that tool as part of the build process. Code generation, cryptographic
|
|
signature utilities, and even ray-tracers have been written in CMake Language,
|
|
but this is not a recommended practice.
|
|
|
|
Because we want to fully explore the language features, this step is an
|
|
exception to the tutorial sequencing. It neither builds on ``Step1``, nor is the
|
|
starting point for ``Step3``. This will be a sandbox to explore language
|
|
features without building any software. We'll pick back up with the Tutorial
|
|
program in ``Step3``.
|
|
|
|
.. note::
|
|
This tutorial endeavors to demonstrate best practices and solutions to real
|
|
problems. However, for this one step we're going to be re-implementing some
|
|
built-in CMake functions. In "real life", do not write your own
|
|
:command:`list(APPEND)`.
|
|
|
|
Background
|
|
^^^^^^^^^^
|
|
|
|
The only fundamental types in CMakeLang are strings and lists. Every object in
|
|
CMake is a string, and lists are themselves strings which contain semicolons
|
|
as separators. Any command which appears to operate on something other than a
|
|
string, whether they be booleans, numbers, JSON objects, or otherwise, is in
|
|
fact consuming a string, doing some internal conversion logic (in a language
|
|
other than CMakeLang), and then converting back to a string for any potential
|
|
output.
|
|
|
|
We can create a variable, which is to say a name for a string, using the
|
|
:command:`set` command.
|
|
|
|
.. code-block:: cmake
|
|
|
|
set(var "World!")
|
|
|
|
A variable's value can be accessed using brace expansion, for example if we want
|
|
to use the :command:`message` command to print the string named by ``var``.
|
|
|
|
.. code-block:: cmake
|
|
|
|
set(var "World!")
|
|
message("Hello ${var}")
|
|
|
|
.. code-block:: console
|
|
|
|
$ cmake -P CMakeLists.txt
|
|
Hello World!
|
|
|
|
.. note::
|
|
:option:`cmake -P` is called "script mode", it informs CMake this file is not
|
|
intended to have a :command:`project` command. We're not building any
|
|
software, instead using CMake only as a command interpreter.
|
|
|
|
Because CMakeLang has only strings, conditionals are entirely by convention of
|
|
which strings are considered true and which are considered false. These are
|
|
*supposed* to be intuitive, "True", "On", "Yes", and (strings representing)
|
|
non-zero numbers are truthy, while "False" "Off", "No", "0", "Ignore",
|
|
"NotFound", and the empty string are all considered false.
|
|
|
|
However, some of the rules are more complex than that, so taking some time
|
|
to consult the :command:`if` documentation on expressions is worthwhile. It's
|
|
recommended to stick to a single pair for a given context, such as
|
|
"True"/"False" or "On"/"Off".
|
|
|
|
As mentioned, lists are strings containing semicolons. The :command:`list`
|
|
command is useful for manipulating these, and many structures within CMake
|
|
expect to operate with this convention. As an example, we can use the
|
|
:command:`foreach` command to iterate over a list.
|
|
|
|
.. code-block:: cmake
|
|
|
|
set(stooges "Moe;Larry")
|
|
list(APPEND stooges "Curly")
|
|
|
|
message("Stooges contains: ${stooges}")
|
|
|
|
foreach(stooge IN LISTS stooges)
|
|
message("Hello, ${stooge}")
|
|
endforeach()
|
|
|
|
.. code-block:: console
|
|
|
|
$ cmake -P CMakeLists.txt
|
|
Stooges contains: Moe;Larry;Curly
|
|
Hello, Moe
|
|
Hello, Larry
|
|
Hello, Curly
|
|
|
|
Exercise 1 - Macros, Functions, and Lists
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
CMake allows us to craft our own functions and macros. This can be very helpful
|
|
when constructing lots of similar targets, like tests, for which we will want
|
|
to call similar sets of commands over and over again. We do so with
|
|
:command:`function` and :command:`macro`.
|
|
|
|
.. code-block:: cmake
|
|
|
|
macro(MyMacro MacroArgument)
|
|
message("${MacroArgument}\n\t\tFrom Macro")
|
|
endmacro()
|
|
|
|
function(MyFunc FuncArgument)
|
|
MyMacro("${FuncArgument}\n\tFrom Function")
|
|
endfunction()
|
|
|
|
MyFunc("From TopLevel")
|
|
|
|
.. code-block:: console
|
|
|
|
$ cmake -P CMakeLists.txt
|
|
From TopLevel
|
|
From Function
|
|
From Macro
|
|
|
|
Like with many languages, the difference between functions and macros is one
|
|
of scope. In CMakeLang, both :command:`function` and :command:`macro` can "see"
|
|
all the variables created in all the frames above them. However, a
|
|
:command:`macro` acts semantically like a text replacement, similar to C/C++
|
|
macros, so any side effects the macro creates are visible in their calling
|
|
context. If we create or change a variable in a macro, the caller will see the
|
|
change.
|
|
|
|
:command:`function` creates its own variable scope, so side effects are not
|
|
visible to the caller. In order to propagate changes to the parent which called
|
|
the function, we must use ``set(<var> <value> PARENT_SCOPE)``, which works the
|
|
same as :command:`set` but for variables belonging to the caller's context.
|
|
|
|
.. note::
|
|
In CMake 3.25, the :command:`return(PROPAGATE)` option was added, which
|
|
works the same as :command:`set(PARENT_SCOPE)` but provides slightly better
|
|
ergonomics.
|
|
|
|
While not necessary for this exercise, it bears mentioning that :command:`macro`
|
|
and :command:`function` both support variadic arguments via the ``ARGV``
|
|
variable, a list containing all arguments passed to the command, and the
|
|
``ARGN`` variable, containing all arguments past the last expected argument.
|
|
|
|
We're not going to build any targets in this exercise, so instead we'll
|
|
construct our own version of :command:`list(APPEND)`, which adds a value to a
|
|
list.
|
|
|
|
Goal
|
|
----
|
|
|
|
Implement a macro and a function which append a value to a list, without using
|
|
the :command:`list(APPEND)` command.
|
|
|
|
The desired usage of these commands is as follows:
|
|
|
|
.. code-block:: cmake
|
|
|
|
set(Letters "Alpha;Beta")
|
|
MacroAppend(Letters "Gamma")
|
|
message("Letters contains: ${Letters}")
|
|
|
|
.. code-block:: console
|
|
|
|
$ cmake -P Exercise1.cmake
|
|
Letters contains: Alpha;Beta;Gamma
|
|
|
|
.. note::
|
|
The extension for these exercises is ``.cmake``, that's the standard extension
|
|
for CMakeLang files when not contained in a ``CMakeLists.txt``
|
|
|
|
Helpful Resources
|
|
-----------------
|
|
|
|
* :command:`macro`
|
|
* :command:`function`
|
|
* :command:`set`
|
|
* :command:`if`
|
|
|
|
Files to Edit
|
|
-------------
|
|
|
|
* ``Exercise1.cmake``
|
|
|
|
Getting Started
|
|
----------------
|
|
|
|
The source code for ``Exercise1.cmake`` is provided in the
|
|
``Help/guide/tutorial/Step2`` directory. It contains tests to verify the
|
|
append behavior described above.
|
|
|
|
.. note::
|
|
You're not expected to handle the case of an empty or undefined list to
|
|
append to. However, as a bonus, the case is tested if you want to try out
|
|
your understanding of CMakeLang conditionals.
|
|
|
|
Complete ``TODO 1`` and ``TODO 2``.
|
|
|
|
Build and Run
|
|
-------------
|
|
|
|
We're going to use script mode to run these exercises. First navigate to the
|
|
``Help/guide/tutorial/Step2`` folder then you can run the code with:
|
|
|
|
.. code-block:: console
|
|
|
|
cmake -P Exercise1.cmake
|
|
|
|
The script will report if the commands were implemented correctly.
|
|
|
|
Solution
|
|
--------
|
|
|
|
This problem relies on an understanding of the mechanisms of CMake variables.
|
|
CMake variables are names for strings; or put another way, a CMake variable
|
|
is itself a string which can brace expand into a different string.
|
|
|
|
This leads to a common pattern in CMake code where functions and macros aren't
|
|
passed values, but rather, they are passed the names of variables which contain
|
|
those values. Thus ``ListVar`` does not contain the *value* of the list we need
|
|
to append to, it contains the *name* of a list, which contains the value we
|
|
need to append to.
|
|
|
|
When expanding the variable with ``${ListVar}``, we will get the name of the
|
|
list. If we expand that name with ``${${ListVar}}``, we will get the values
|
|
the list contains.
|
|
|
|
To implement ``MacroAppend``, we need only combine this understanding of
|
|
``ListVar`` with our knowledge of the :command:`set` command.
|
|
|
|
.. raw:: html
|
|
|
|
<details><summary>TODO 1: Click to show/hide answer</summary>
|
|
|
|
.. code-block:: cmake
|
|
:caption: TODO 1: Exercise1.cmake
|
|
:name: Exercise1.cmake-MacroAppend
|
|
|
|
macro(MacroAppend ListVar Value)
|
|
set(${ListVar} "${${ListVar}};${Value}")
|
|
endmacro()
|
|
|
|
.. raw:: html
|
|
|
|
</details>
|
|
|
|
We don't need to worry about scope here, because a macro operates in the same
|
|
scope as its parent.
|
|
|
|
``FuncAppend`` is almost identical, in fact it could be implemented in the
|
|
same one liner but with an added ``PARENT_SCOPE``, but the instructions ask
|
|
us to implement it in terms of ``MacroAppend``.
|
|
|
|
.. raw:: html
|
|
|
|
<details><summary>TODO 2: Click to show/hide answer</summary>
|
|
|
|
.. code-block:: cmake
|
|
:caption: TODO 2: Exercise1.cmake
|
|
:name: Exercise1.cmake-FuncAppend
|
|
|
|
function(FuncAppend ListVar Value)
|
|
MacroAppend(${ListVar} ${Value})
|
|
set(${ListVar} "${${ListVar}}" PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
.. raw:: html
|
|
|
|
</details>
|
|
|
|
``MacroAppend`` transforms ``ListVar`` for us, but it won't propagate the result
|
|
to the parent scope. Because this is a function, we need to do so ourselves
|
|
with :command:`set(PARENT_SCOPE)`.
|
|
|
|
Exercise 2 - Conditionals and Loops
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The two most common flow control elements in any structured programming
|
|
language are conditionals and their close sibling loops. CMakeLang is no
|
|
different. As previously mentioned, the truthiness of a given CMake string is a
|
|
convention established by the :command:`if` command.
|
|
|
|
When given a string, :command:`if` will first check if it is one of the known
|
|
constant values previously discussed. If the string isn't one of those values
|
|
the command assumes it is a variable, and checks the brace-expanded contents of
|
|
that variable to determine the result of the conditional.
|
|
|
|
.. code-block:: cmake
|
|
|
|
if(True)
|
|
message("Constant Value: True")
|
|
else()
|
|
message("Constant Value: False")
|
|
endif()
|
|
|
|
if(ConditionalValue)
|
|
message("Undefined Variable: True")
|
|
else()
|
|
message("Undefined Variable: False")
|
|
endif()
|
|
|
|
set(ConditionalValue True)
|
|
|
|
if(ConditionalValue)
|
|
message("Defined Variable: True")
|
|
else()
|
|
message("Defined Variable: False")
|
|
endif()
|
|
|
|
.. code-block:: console
|
|
|
|
$ cmake -P ConditionalValue.cmake
|
|
Constant Value: True
|
|
Undefined Variable: False
|
|
Defined Variable: True
|
|
|
|
.. note::
|
|
This is a good a time as any to discuss quoting in CMake. All objects in
|
|
CMake are strings, thus the double quote, ``"``, is often unnecessary.
|
|
CMake knows the object is a string, everything is a string.
|
|
|
|
However, it is needed in some contexts. Strings containing whitespace require
|
|
double quotes, else they are treated like lists; CMake will concatenate the
|
|
elements together with semicolons. The reverse is also true, when
|
|
brace-expanding lists it is necessary to do so inside quotes if we want to
|
|
*preserve* the semicolons. Otherwise CMake will expand the list items into
|
|
space-separate strings.
|
|
|
|
A handful of commands, such as :command:`if`, recognize the difference
|
|
between quoted and unquoted strings. :command:`if` will only check that the
|
|
given string represents a variable when the string is unquoted.
|
|
|
|
Finally, :command:`if` provides several useful comparison modes such as
|
|
``STREQUAL`` for string matching, ``DEFINED`` for checking the existence of
|
|
a variable, and ``MATCHES`` for regular expression checks. It also supports the
|
|
typical logical operators, ``NOT``, ``AND``, and ``OR``.
|
|
|
|
In addition to conditionals CMake provides two loop structures,
|
|
:command:`while`, which follows the same rules as :command:`if` for checking a
|
|
loop variable, and the more useful :command:`foreach`, which iterates over lists
|
|
of strings and was demonstrated in the `Background`_ section.
|
|
|
|
For this exercise, we're going to use loops and conditionals to solve some
|
|
simple problems. We'll be using the aforementioned ``ARGN`` variable from
|
|
:command:`function` as the list to operate on.
|
|
|
|
Goal
|
|
----
|
|
|
|
Loop over a list, and return all the strings containing the string ``Foo``.
|
|
|
|
.. note::
|
|
Those who read the command documentation will be aware that this is
|
|
:command:`list(FILTER)`, resist the temptation to use it.
|
|
|
|
Helpful Resources
|
|
-----------------
|
|
|
|
* :command:`function`
|
|
* :command:`foreach`
|
|
* :command:`if`
|
|
* :command:`list`
|
|
|
|
Files to Edit
|
|
-------------
|
|
|
|
* ``Exercise2.cmake``
|
|
|
|
Getting Started
|
|
----------------
|
|
|
|
The source code for ``Exercise2.cmake`` is provided in the ``Help/guide/tutorial/Step2``
|
|
directory. It contains tests to verify the append behavior described above.
|
|
|
|
.. note::
|
|
You should use the :command:`list(APPEND)` command this time to collect your
|
|
final result into a list. The input can be consumed from the ``ARGN`` variable
|
|
of the provided function.
|
|
|
|
Complete ``TODO 3``.
|
|
|
|
Build and Run
|
|
-------------
|
|
|
|
Navigate to the ``Help/guide/tutorial/Step2`` folder then you can run the code with:
|
|
|
|
.. code-block:: console
|
|
|
|
cmake -P Exercise2.cmake
|
|
|
|
The script will report if the ``FilterFoo`` function was implemented correctly.
|
|
|
|
Solution
|
|
--------
|
|
|
|
We need to do three things, loop over the ``ARGN`` list, check if a given
|
|
item in that list matches ``"Foo"``, and if so append it to the ``OutVar``
|
|
list.
|
|
|
|
While there are a couple ways we could invoke :command:`foreach`, the
|
|
recommended way is to allow the command to do the variable expansion for us
|
|
via ``IN LISTS`` to access the ``ARGN`` list items.
|
|
|
|
The :command:`if` comparison we need is ``MATCHES`` which will check if
|
|
``"FOO"`` exists in the item. All that remains is to append the item to the
|
|
``OutVar`` list. The trickiest part is remembering that ``OutVar`` *names* a
|
|
list, it is not the list itself, so we need to access it via ``${OutVar}``.
|
|
|
|
.. raw:: html
|
|
|
|
<details><summary>TODO 3: Click to show/hide answer</summary>
|
|
|
|
.. code-block:: cmake
|
|
:caption: TODO 3: Exercise2.cmake
|
|
:name: Exercise2.cmake-FilterFoo
|
|
|
|
function(FilterFoo OutVar)
|
|
|
|
foreach(item IN LISTS ARGN)
|
|
if(item MATCHES Foo)
|
|
list(APPEND ${OutVar} ${item})
|
|
endif()
|
|
endforeach()
|
|
|
|
set(${OutVar} ${${OutVar}} PARENT_SCOPE)
|
|
endfunction()
|
|
|
|
.. raw:: html
|
|
|
|
</details>
|
|
|
|
Exercise 3 - Organizing with Include
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
We have already discussed how to incorporate subdirectories containing their
|
|
own CMLs with :command:`add_subdirectory`. In later steps we will explore
|
|
the various way CMake code can be packaged and shared across projects.
|
|
|
|
However for small CMake functions and utilities, it is often beneficial for them
|
|
to live in their own ``.cmake`` files outside the project CMLs and separate
|
|
from the rest of the build system. This allows for separation of concerns,
|
|
removing the project-specific elements from the utilities we are using to
|
|
describe them.
|
|
|
|
To incorporate these separate ``.cmake`` files into our project, we use the
|
|
:command:`include` command. This command immediately begins interpreting the
|
|
contents of the :command:`include`'d file in the scope of the parent CML. It
|
|
is as if the entire file were being called as a macro.
|
|
|
|
Traditionally, these kinds of ``.cmake`` files live in a folder named "cmake"
|
|
inside the project root. For this exercise, we'll use the ``Step2`` folder instead.
|
|
|
|
Goal
|
|
----
|
|
|
|
Use the functions from Exercises 1 and 2 to build and filter our own list of items.
|
|
|
|
Helpful Resources
|
|
-----------------
|
|
|
|
* :command:`include`
|
|
|
|
Files to Edit
|
|
-------------
|
|
|
|
* ``Exercise3.cmake``
|
|
|
|
Getting Started
|
|
----------------
|
|
|
|
The source code for ``Exercise3.cmake`` is provided in the ``Help/guide/tutorial/Step2``
|
|
directory. It contains tests to verify the correct usage of our functions
|
|
from the previous two exercises.
|
|
|
|
.. note::
|
|
Actually it reuses tests from Exercise2.cmake, reusable code is good for
|
|
everyone.
|
|
|
|
Complete ``TODO 4`` through ``TODO 7``.
|
|
|
|
Build and Run
|
|
-------------
|
|
|
|
Navigate to the ``Help/guide/tutorial/Step2`` folder then you can run the code with:
|
|
|
|
.. code-block:: console
|
|
|
|
cmake -P Exercise3.cmake
|
|
|
|
The script will report if the functions were invoked and composed correctly.
|
|
|
|
Solution
|
|
--------
|
|
|
|
The :command:`include` command will interpret the included file completely,
|
|
including the tests from the first two exercises. We don't want to run these
|
|
tests again. Thanks to some forethought, these files check a variable called
|
|
``SKIP_TESTS`` prior to running their tests, setting this to ``True`` will
|
|
get us the behavior we want.
|
|
|
|
.. raw:: html
|
|
|
|
<details><summary>TODO 4: Click to show/hide answer</summary>
|
|
|
|
.. code-block:: cmake
|
|
:caption: TODO 4: Exercise3.cmake
|
|
:name: Exercise3.cmake-SKIP_TESTS
|
|
|
|
set(SKIP_TESTS True)
|
|
|
|
.. raw:: html
|
|
|
|
</details>
|
|
|
|
Now we're ready to :command:`include` the previous exercises to grab their
|
|
functions.
|
|
|
|
.. raw:: html
|
|
|
|
<details><summary>TODO 5: Click to show/hide answer</summary>
|
|
|
|
.. code-block:: cmake
|
|
:caption: TODO 5: Exercise3.cmake
|
|
:name: Exercise3.cmake-include
|
|
|
|
include(Exercise1.cmake)
|
|
include(Exercise2.cmake)
|
|
|
|
.. raw:: html
|
|
|
|
</details>
|
|
|
|
Now that ``FuncAppend`` is available to us, we can use it to append new elements
|
|
to the ``InList``.
|
|
|
|
.. raw:: html
|
|
|
|
<details><summary>TODO 6: Click to show/hide answer</summary>
|
|
|
|
.. code-block:: cmake
|
|
:caption: TODO 6: Exercise3.cmake
|
|
:name: Exercise3.cmake-FuncAppend
|
|
|
|
FuncAppend(InList FooBaz)
|
|
FuncAppend(InList QuxBaz)
|
|
|
|
.. raw:: html
|
|
|
|
</details>
|
|
|
|
Finally, we can use ``FilterFoo`` to filter the full list. The tricky part to
|
|
remember here is that our ``FilterFoo`` wants to operate on list values via
|
|
``ARGN``, so we need to expand the ``InList`` when we call ``FilterFoo``.
|
|
|
|
.. raw:: html
|
|
|
|
<details><summary>TODO 7: Click to show/hide answer</summary>
|
|
|
|
.. code-block:: cmake
|
|
:caption: TODO 7: Exercise3.cmake
|
|
:name: Exercise3.cmake-FilterFoo
|
|
|
|
FilterFoo(OutList ${InList})
|
|
|
|
.. raw:: html
|
|
|
|
</details>
|