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