if, And Conditions

if Syntax

  • Much like in other languages

    set(some_variable "value1")
    
    if ("${some_variable}" STREQUAL "value1")
      message("case 1")
    elseif ("${some_variable}" STREQUAL "value2")
      message("case 2")
    else()
      message("default")
    endif()
    
    $ cmake -P if-good.cmake
    case 1
    
  • Much complexity in the evaluation of the condition

  • The following holds true for if(), elseif(), and while()

Predefined Conditions: String Comparison

  • Lexicographic comparison, e.g. STRLESS

    set(some_name "Faschingbauer")
    
    if ("${some_name}" STRLESS "Zeilinger")
      message("yes")
    endif()
    
    $ cmake -P if-strless.cmake
    yes
    
  • Numerical comparison, e.g. LESS

    set(some_number 7)                      # <--- unquoted is ok for numbers :-)
    
    if ("${some_number}" LESS 8)
      message("yes")
    endif()
    
    $ cmake -P if-less.cmake
    yes
    

Pitfalls: Variables Are Strings, And CMake Does Not Care

  • Comparing apples and oranges: no runtime error

  • CMake just says “no”

set(some_number "Faschingbauer")

if ("${some_number}" LESS 8)
  message("yes")
else()
  message("no")
endif()

if ("${some_number}" GREATER 8)
  message("yes")
else()
  message("no")
endif()

if ("${some_number}" EQUAL 8)
  message("yes")
else()
  message("no")
endif()
$ cmake -P if-less-mismatch.cmake
no
no
no

Danger

Be careful! Test!!

Predefined Conditions: File System

  • File existence, etc.

    set(filename "/etc/passwd")
    
    if (EXISTS "${filename}")
      message("${filename} exists")
      if (NOT IS_DIRECTORY "${filename}")
        message("${filename} is not a directory")
      endif()
    endif()
    
    $ cmake -P if-file-exists-and-is-directory.cmake
    /etc/passwd exists
    /etc/passwd is not a directory
    
  • Path comparison

    set(filename "/etc/passwd")
    
    if (IS_ABSOLUTE "${filename}")
      message("${filename} is an absolute path")
    endif()
    
    cmake_policy(SET CMP0139 NEW)           # <--- argh!
    
    set(alternative_filename "/etc/blah/../passwd")
    if ("${alternative_filename}" PATH_EQUAL "${filename}")
      message("${alternative_filename} and ${filename} are the same path")
    endif()
    
    $ cmake -P if-path-comparison.cmake
    /etc/passwd is an absolute path
    

Policies?

Concrete problem: in the above code snippet, CMake version 3.27.7 (cmake --version) failed to recognise the PATH_EQUAL operator, with a message saying something about policy CMP0139.

$ cmake --help-policy CMP0139
CMP0139
-------

.. versionadded:: 3.24

The ``if()`` command supports path comparisons using ``PATH_EQUAL``
operator.

The ``OLD`` behavior for this policy is to ignore the ``PATH_EQUAL`` operator.
The ``NEW`` behavior is to interpret the ``PATH_EQUAL`` operator.

This policy was introduced in CMake version 3.24.
CMake version 3.27.7 warns when the policy is not set and uses
``OLD`` behavior.  Use the ``cmake_policy()`` command to set
it to ``OLD`` or ``NEW`` explicitly.

.. note::
  The ``OLD`` behavior of a policy is
  ``deprecated by definition``
  and may be removed in a future version of CMake.

Solution: set the damn policy

cmake_policy(SET CMP0139 NEW)           # <--- argh!

Note

  • Try not to set policies in any of the 100 CMakeLists.txt files in your project

  • Reserve the toplevel CMakeLists.txt for such administrative tasks

Miscellaneous Checks (Some Rather Advanced)

  • Is a variable defined? Note: a variable may be defined but empty

    set(some_variable "value")
    
    if (DEFINED some_variable)
      message("some_variable is a defined variable")
    endif()
    if (NOT DEFINED another_variable)
      message("another_variable is not a defined variable")
    endif()
    
    $ cmake -P if-defined.cmake
    some_variable is a defined variable
    another_variable is not a defined variable
    
  • Does a target exist?

    set(libname "a-library-that-is-optionally-built")
    
    if (NOT TARGET "${libname}")
      message("${libname} is not a known target")
    endif()
    
    $ cmake -P if-target.cmake
    a-library-that-is-optionally-built is not a known target
    

    Note

    • Targets are architecturally relevant

    • Properties are probably a better way to communicate optionality.

Condition Evaluation (Where The Mess Begins): What’s Boolean?

So far, we only used strings in the examples

Attention

  • Rule: use strings explicitly as much as you can in your CMake code

    • This keeps you mentally sane

    • This will help colleagues understand your code

  • Rule: set policy CMP0012 to NEW

  • if (and while, for that matter) exists in CMake for a long time

  • ⟶ long before ${NAME} has been considered the way to go for variable references

  • confusion

  • if understands “literals”/”constants” ⟶ there is no such definition in the language, it just happened

Dereferencing Unquoted Variable Names

set(some_name "Faschingbauer")

if (some_name STRLESS "Zeilinger")      # <--- better: "${some_name}" or ${some_name}
  message("yes")
endif()
$ cmake -P if-unquoted.cmake
yes

“Boolean” Variables: Weirdly Interpreted Strings

  • First: there is no boolean type

  • ⟶ only strings that are interpreted somehow

    • 1, ON, YES, TRUE, Y (case-insensitively) are interpreted as boolean true

    • 0, OFF, NO, FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in the suffix -NOTFOUND (case-insensitively) are interpreted as boolean false.

  • please listen, and suffer

  • A naive approach would be to pretend to not know all that …

    set(boolean_variable "TRUE")
    
    if ("${boolean_variable}" STREQUAL "TRUE")
      message("true")
    endif()
    
    $ cmake -P if-bool-naive.cmake
    true
    
  • But this is not how CMake wants it

    set(boolean_variable 1)
    
    if (boolean_variable)                   # <--- unquoted variables 
                                            # <--- preferred for bool
      message("boolean_variable (${boolean_variable}) evaluated as boolean true")
    endif()
    
    set(boolean_variable 0)
    
    if (NOT boolean_variable)               # <--- unquoted variables 
                                            # <--- preferred for bool
      message("boolean_variable (${boolean_variable}) evaluated as boolean false")
    endif()
    
    $ cmake -P if-bool-cmake.cmake
    boolean_variable (1) evaluated as boolean true
    boolean_variable (0) evaluated as boolean false
    

Attention

All would be well if that was all. But we have no such luck.

Pitfall: Evaluation Until There Is No More

  • When CMake evaluates (“interpolates”, “expands”) strings, it does so until there’s nothing left.

  • … which can lead to surprising results

  • The following evaluates to the plain string "boolean_variable", which is turn is recognized by CMake as a variable reference

set(boolean_variable 0)
set(another_variable "boolean_variable")  # <--- name of boolean_variable

if ("${another_variable}")                # <--- evaluates to unquoted boolean_variable
                                          #      which evaluates to 0
  message("quoted: true")
else()
  message("quoted: false")
endif()
$ cmake -P if-boolshit-expand-loop.cmake
CMake Warning (dev) at if-boolshit-expand-loop.cmake:4 (if):
  Policy CMP0054 is not set: Only interpret if() arguments as variables or
  keywords when unquoted.  Run "cmake --help-policy CMP0054" for policy
  details.  Use the cmake_policy command to set the policy and suppress this
  warning.

  Quoted variables like "boolean_variable" will no longer be dereferenced
  when the policy is set to NEW.  Since the policy is not set the OLD
  behavior will be used.
This warning is for project developers.  Use -Wno-dev to suppress it.

quoted: false

Pitfall: Evaluation Of Unquoted Arguments

  • Undefined variables evaluate to boolean false. Just like before, quoting evaluates until there is no more

    • "${foo}" evaluates to "bar"

    • … which is then taken by if as an unquoted reference to variable bar

    • bar is not defined which evaluates to boolean false

    set(foo "bar")
    if ("${foo}")                           # <--- unquoted bar -> undefined
      message("true")
    else()
      message("false")
    endif()
    
    $ cmake -P if-boolshit-par-excellence-ok.cmake
    false
    
  • Biggest crap ever seen: unquoted variable references are special

    set(foo "bar")
    if (foo)
      message("true")
    else()
      message("false")
    endif()
    
    $ cmake -P if-boolshit-par-excellence.cmake
    true