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 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
|
Step 3: Configuration and Cache Variables
=========================================
CMake projects often have some project-specific configuration variables which
users and packagers are interested in. CMake has many ways that an invoking
user or process can communicate these configuration choices, but the most
fundamental of them are :option:`-D <cmake -D>` flags.
In this step we'll explore the ins and out of how to provide project
configuration options from within a CML, and how to invoke CMake to take
advantage of configuration options provided by both CMake and individual
projects.
Background
^^^^^^^^^^
If we had a CMake project for compression software which supported multiple
compression algorithms, we might want to let the packager of the project decide
which algorithms to enable when they build our software. We can do so by
consuming variables set via :option:`-D <cmake -D>` flags.
.. code-block:: cmake
if(COMPRESSION_SOFTWARE_USE_ZLIB)
message("I will use Zlib!")
# ...
endif()
if(COMPRESSION_SOFTWARE_USE_ZSTD)
message("I will use Zstd!")
# ...
endif()
.. code-block:: console
$ cmake -B build \
-DCOMPRESSION_SOFTWARE_USE_ZLIB=ON \
-DCOMPRESSION_SOFTWARE_USE_ZSTD=OFF
...
I will use Zlib!
Of course, we will want to provide reasonable defaults for these configuration
choices, and a way to communicate the purpose of a given option. This function
is provided by the :command:`option` command.
.. code-block:: cmake
option(COMPRESSION_SOFTWARE_USE_ZLIB "Support Zlib compression" ON)
option(COMPRESSION_SOFTWARE_USE_ZSTD "Support Zstd compression" ON)
if(COMPRESSION_SOFTWARE_USE_ZLIB)
# Same as before
# ...
.. code-block:: console
$ cmake -B build \
-DCOMPRESSION_SOFTWARE_USE_ZLIB=OFF
...
I will use Zstd!
The names created by :option:`-D <cmake -D>` flags and :command:`option` are
not normal variables, they are **cache** variables. Cache variables are globally
visible variables which are *sticky*, their value is difficult to change after
it is initially set. In fact they are so sticky that, in project mode, CMake
will save and restore cache variables across multiple configurations. If a
cache variable is set once, it will remain until another :option:`-D <cmake -D>`
flag preempts the saved variable.
.. note::
CMake itself has dozens of normal and cache variables used for configuration.
These are documented at :manual:`cmake-variables(7)` and operate in the same
manner as project-provided variables for configuration.
:command:`set` can also be used to manipulate cache variables, but will not
change a variable which has already been created.
.. code-block:: cmake
set(StickyCacheVariable "I will not change" CACHE STRING "")
set(StickyCacheVariable "Overwrite StickyCache" CACHE STRING "")
message("StickyCacheVariable: ${StickyCacheVariable}")
.. code-block:: console
$ cmake -P StickyCacheVariable.cmake
StickyCacheVariable: I will not change
Because :option:`-D <cmake -D>` flags are processed before any other commands,
they take precedence for setting the value of a cache variable.
.. code-block:: console
$ cmake \
-DStickyCacheVariable="Commandline always wins" \
-P StickyCacheVariable.cmake
StickyCacheVariable: Commandline always wins
While cache variables cannot ordinarily be changed, they can be *shadowed* by
normal variables. We can observe this by :command:`set`'ing a variable to have
the same name as a cache variable, and then using :command:`unset` to remove
the normal variable.
.. code-block:: cmake
set(ShadowVariable "In the shadows" CACHE STRING "")
set(ShadowVariable "Hiding the cache variable")
message("ShadowVariable: ${ShadowVariable}")
unset(ShadowVariable)
message("ShadowVariable: ${ShadowVariable}")
.. code-block:: console
$ cmake -P ShadowVariable.cmake
ShadowVariable: Hiding the cache variable
ShadowVariable: In the shadows
Exercise 1 - Using Options
^^^^^^^^^^^^^^^^^^^^^^^^^^
We can imagine a scenario where consumers really want our ``MathFunctions``
library, and the ``Tutorial`` utility is a "take it or leave it" add-on. In
that case, we might want to add an option to allow consumers to disable
building our ``Tutorial`` binary, building only the ``MathFunctions`` library.
With our knowledge of options, conditionals, and cache variables we have all
the pieces we need to make this configuration available.
Goal
----
Add an option named ``TUTORIAL_BUILD_UTILITIES`` to control if the ``Tutorial``
binary is configured and built.
.. note::
CMake allows us to determine which targets are built after configuration. Our
users could ask for the ``MathFunctions`` library alone without ``Tutorial``.
CMake also has mechanisms to exclude targets from ``ALL``, the default target
which builds all the other available targets.
However, options which completely exclude targets from the configuration are
convenient and popular, especially if configuring those targets involves
heavy-weight steps which might take some time.
It also simplifies :command:`install()` logic, which we'll discuss in later
steps, if targets the packager is uninterested in are completely excluded.
Helpful Resources
-----------------
* :command:`option`
* :command:`if`
Files to Edit
-------------
* ``CMakeLists.txt``
Getting Started
---------------
The ``Help/guide/tutorial/Step3`` folder contains the complete, recommended
solution to ``Step1`` and the relevant ``TODOs`` for this step. Take a minute
to review and refamiliarize yourself with the ``Tutorial`` project.
When you feel you have an understanding of the current code, start with
``TODO 1`` and complete through ``TODO 2``.
Build and Run
-------------
We can now reconfigure our project. However, this time we want to control the
configuration via :option:`-D <cmake -D>` flags. We again start by navigating
to ``Help/guide/tutorial/Step3`` and invoking CMake, but this time with our
configuration options.
.. code-block:: console
cmake -B build -DTUTORIAL_BUILD_UTILITIES=OFF
We can now build as usual.
.. code-block:: console
cmake --build build
After the build we should observe no Tutorial executable is produced. Because
cache variables are sticky even a reconfigure shouldn't change this, despite
the default-``ON`` option.
.. code-block:: console
cmake -B build
cmake --build build
Will not produce the Tutorial executable, the cache variables are "locked in".
To change this we have two options. First, we can edit the file which stores
the cache variables between CMake configuration runs, the "CMake Cache". This
file is ``build/CMakeCache.txt``, in it we can find the option cache variable.
.. code-block:: text
//Build the Tutorial executable
TUTORIAL_BUILD_UTILITIES:BOOL=OFF
We can change this from ``OFF`` to ``ON``, rerun the build, and we will get
our ``Tutorial`` executable.
.. note::
``CMakeCache.txt`` entries are of the form ``<Name>:<Type>=<Value>``, however
the "type" is only a hint. All objects in CMake are strings, regardless of
what the cache says.
Alternatively, we can change the value of the cache variable on the command
line, because the command line runs before ``CMakeCache.txt`` is loaded its
value take precedence over those in the cache file.
.. code-block:: console
cmake -B build -DTUTORIAL_BUILD_UTILITIES=ON
cmake --build build
Doing so we observe the value in ``CMakeCache.txt`` has flipped from ``OFF``
to ``ON``, and that the ``Tutorial`` executable is built.
Solution
--------
First we create our :command:`option` to provide our cache variable with a
reasonable default value.
.. raw:: html
<details><summary>TODO 1: Click to show/hide answer</summary>
.. literalinclude:: Step4/CMakeLists.txt
:caption: TODO 1: CMakeLists.txt
:name: CMakeLists.txt-option-TUTORIAL_BUILD_UTILITIES
:language: cmake
:start-at: option(TUTORIAL_BUILD_UTILITIES
:end-at: option(TUTORIAL_BUILD_UTILITIES
.. raw:: html
</details>
Then we can check the cache variable to conditionally enable the ``Tutorial``
executable (by way of adding its subdirectory).
.. raw:: html
<details><summary>TODO 2: Click to show/hide answer</summary>
.. literalinclude:: Step4/CMakeLists.txt
:caption: TODO 2: CMakeLists.txt
:name: CMakeLists.txt-if-TUTORIAL_BUILD_UTILITIES
:language: cmake
:start-at: if(TUTORIAL_BUILD_UTILITIES)
:end-at: endif()
.. raw:: html
</details>
Exercise 2 - ``CMAKE`` Variables
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
CMake has several important normal and cache variables provided to allow
packagers to control the build. Decisions such as compilers, default flags,
search locations for packages, and much more are all controlled by CMake's
own configuration variables.
Among the most important are language standards. As the language standard can
have significant impact on the ABI presented by a given package. For example,
it's quite common for libraries to use standard C++ templates on later
standards, and provide polyfills on earlier standards. If a library is consumed
under different standards then ABI incompatibilities between the standard
templates and the polyfills can result in incomprehensible errors and runtime
crashes.
Ensuring all of our targets are built under the same language standard is
achieved with the :variable:`CMAKE_<LANG>_STANDARD` cache variables. For C++,
this is ``CMAKE_CXX_STANDARD``.
.. note::
Because these variables are so important, it is equally important that
developers not override or shadow them in their CMLs. Shadowing
:variable:`CMAKE_<LANG>_STANDARD` in a CML because the library wants C++20,
when the packager has decided to build the rest of their libraries and
applications with C++23, can lead to the aforementioned terrible,
incomprehensible errors.
Do not :command:`set` ``CMAKE_`` globals without very strong reasons for
doing so. We'll discuss better methods for targets to communicate
requirements like definitions and minimum standards in later steps.
In this exercise, we'll introduce some C++20 code into our library and
executable and build them with C++20 by setting the appropriate cache variable.
Goal
----
Use ``std::format`` to format printed strings instead of stream operators. To
ensure availability of ``std::format``, configure CMake to use the C++20
standard for C++ targets.
Helpful Resources
-----------------
* :option:`cmake -D`
* :variable:`CMAKE_<LANG>_STANDARD`
* :variable:`CMAKE_CXX_STANDARD`
* :prop_tgt:`CXX_STANDARD`
* `cppreference \<format\> <https://en.cppreference.com/w/cpp/utility/format/format.html>`_
Files to Edit
-------------
* ``Tutorial/Tutorial.cxx``
* ``MathFunctions/MathFunctions.cxx``
Getting Started
---------------
Continue to edit files from ``Step3``. Complete ``TODO 3`` through ``TODO 7``.
We'll be modifying our prints to use ``std::format`` instead of stream
operators.
Ensure your cache variables are set such that the Tutorial executable will be
built, using any of the methods discussed in the previous exercise.
Build and Run
-------------
We need to reconfigure our project with the new standard, we can do this
using the same method as our ``TUTORIAL_BUILD_UTILITIES`` cache variable.
.. code-block:: console
cmake -B build -DCMAKE_CXX_STANDARD=20
.. note::
Configuration variables are, by convention, prefixed with the provider of the
variable. CMake configuration variables are prefixed with ``CMAKE_``, while
projects should prefix their variables with ``<PROJECT>_``.
The tutorial configuration variables follow this convention, and are prefixed
with ``TUTORIAL_``.
Now that we've configured with C++20, we can build as usual.
.. code-block:: console
cmake --build build
Solution
--------
We need to include ``<format>`` and then use it.
.. raw:: html
<details><summary>TODO 3-5: Click to show/hide answer</summary>
.. literalinclude:: Step4/Tutorial/Tutorial.cxx
:caption: TODO 3: Tutorial/Tutorial.cxx
:name: Tutorial/Tutorial.cxx-include-format
:language: c++
:start-at: #include <format>
:end-at: #include <string>
.. literalinclude:: Step4/Tutorial/Tutorial.cxx
:caption: TODO 4: Tutorial/Tutorial.cxx
:name: Tutorial/Tutorial.cxx-format1
:language: c++
:start-at: if (argc < 2) {
:end-at: return 1;
:append: }
:dedent: 2
.. literalinclude:: Step4/Tutorial/Tutorial.cxx
:caption: TODO 5: Tutorial/Tutorial.cxx
:name: Tutorial/Tutorial.cxx-format3
:language: c++
:start-at: // calculate square root
:end-at: outputValue);
:dedent: 2
.. raw:: html
</details>
And again for the ``MathFunctions`` library.
.. raw:: html
<details><summary>TODO 6-7: Click to show/hide answer</summary>
.. literalinclude:: Step4/MathFunctions/MathFunctions.cxx
:caption: TODO 6: MathFunctions.cxx
:name: MathFunctions/MathFunctions.cxx-include-format
:language: c++
:start-at: #include <format>
:end-at: #include <iostream>
.. literalinclude:: Step4/MathFunctions/MathFunctions.cxx
:caption: TODO 7: MathFunctions.cxx
:name: MathFunctions/MathFunctions.cxx-format
:language: c++
:start-at: double delta
:end-at: std::format
:dedent: 4
.. raw:: html
</details>
Exercise 3 - CMakePresets.json
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Managing these configuration values can quickly become overwhelming. In CI
systems it is appropriate to record these as part of a given CI step. For
example in a Github Actions CI step we might see something akin to the
following:
.. code-block:: yaml
- name: Configure and Build
run: |
cmake \
-B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_STANDARD=20 \
-DCMAKE_CXX_EXTENSIONS=ON \
-DTUTORIAL_BUILD_UTILITIES=OFF \
# Possibly many more options
# ...
cmake --build build
When developing code locally, typing all these options even once might be error
prone. If a fresh configuration is needed for any reason, doing so multiple
times could be exhausting.
There are many and varied solutions to this problem, and your choice is
ultimately up to your preferences as a developer. CLI-oriented developers
commonly use task runners to invoke CMake with their desired options for a
project. Most IDEs also have a custom mechanism for controlling CMake
configuration.
It would be impossible to fully enumerate every possible configuration workflow
here. Instead we will explore CMake's built-in solution, known as
:manual:`CMake Presets <cmake-presets(7)>`. Presets give us a format to name
and express collections of CMake configuration options.
.. note::
Presets are capable of expressing entire CMake workflows, from
configuration, through building, all the way to installing the software
package.
They are far more flexible than can we have room for here. We'll limit
ourselves to using them for configuration.
CMake Presets come in two standard files, ``CMakePresets.json``, which is
intended to be a part of the project and tracked in source control; and
``CMakeUserPresets.json``, which is intended for local user configuration
and should not be tracked in source control.
The simplest preset which would be of use to a developer does nothing more
than configure variables.
.. code-block:: json
{
"version": 4,
"configurePresets": [
{
"name": "example-preset",
"cacheVariables": {
"EXAMPLE_FOO": "Bar",
"EXAMPLE_QUX": "Baz"
}
}
]
}
When invoking CMake, where previously we would have done:
.. code-block:: console
cmake -B build -DEXAMPLE_FOO=Bar -DEXAMPLE_QUX=Baz
We can now use the preset:
.. code-block:: console
cmake -B build --preset example-preset
CMake will search for files named ``CMakePresets.json`` and
``CMakeUserPresets.json``, and load the named configuration from them if
available.
.. note::
Command line flags can be mixed with presets. Command line flags have
precedence over values found in a preset.
Presets also support limited macros, variables that can be brace-expanded
inside the preset. The only one of interest to us is the ``${sourceDir}`` macro,
which expands to the root directory of the project. We can use this to set our
build directory, skipping the :option:`-B <cmake -B>` flag when configuring
the project.
.. code-block:: json
{
"name": "example-preset",
"binaryDir": "${sourceDir}/build"
}
Goal
----
Configure and build the tutorial using a CMake Preset instead of command line
flags.
Helpful Resources
-----------------
* :manual:`cmake-presets(7)`
Files to Edit
-------------
* ``CMakePresets.json``
Getting Started
---------------
Continue to edit files from ``Step3``. Complete ``TODO 8`` and ``TODO 9``.
.. note::
``TODOs`` inside ``CMakePresets.json`` need to be *replaced*. There should
be no ``TODO`` keys left inside the file when you have completed the exercise.
You can verify the preset is working correctly by deleting the existing build
folder before you configure, this will ensure you're not reusing the existing
CMake Cache for configuration.
.. note::
On CMake 3.24 and newer, the same effect can be achieved by configuring with
:option:`cmake --fresh`.
All future configuration changes will be via the ``CMakePresets.json`` file.
Build and Run
-------------
We can now use the preset file to manage our configuration.
.. code-block:: console
cmake --preset tutorial
Presets are capable of running the build step for us, but for this tutorial
we'll continue to run the build ourselves.
.. code-block:: console
cmake --build build
Solution
--------
There are two changes we need to make, first we want to set the build
directory (also called the "binary directory") to the ``build`` subdirectory
of our project folder, and second we need to set the ``CMAKE_CXX_STANDARD`` to
``20``.
.. raw:: html
<details><summary>TODO 8-9: Click to show/hide answer</summary>
.. code-block:: json
:caption: TODO 8-9: CMakePresets.json
:name: CMakePresets.json-initial
{
"version": 4,
"configurePresets": [
{
"name": "tutorial",
"displayName": "Tutorial Preset",
"description": "Preset to use with the tutorial",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_CXX_STANDARD": "20"
}
}
]
}
.. raw:: html
</details>
|