| 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> |