File: CMake%20Language%20Fundamentals.rst

package info (click to toggle)
cmake 4.2.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 152,344 kB
  • sloc: ansic: 403,894; cpp: 303,807; sh: 4,097; python: 3,582; yacc: 3,106; lex: 1,279; f90: 538; asm: 471; lisp: 375; cs: 270; java: 266; fortran: 239; objc: 215; perl: 213; xml: 198; makefile: 108; javascript: 83; pascal: 63; tcl: 55; php: 25; ruby: 22
file content (583 lines) | stat: -rw-r--r-- 18,611 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
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>