blob: e5dc68dbf4d3f21f47f100808d8f2e0557932a93 [file] [log] [blame] [edit]
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>