CMake Conditionals

[edited 17/04/14]

CMake has many wonderful features. However, CMake’s if() command handles variables in a way that can often trip people up. In particular, comparison operators like STREQUAL try to interpret their arguments as variable names, and only treat them as actual strings if there is no matching variable. Combined with the fact that basically any string you can write in a CMake file is a valid variable name, this can lead to some subtle bugs.

This script highlights all the potential pitfalls I could think of. The “FATAL_ERROR” branches are the ones that will not be taken, but many of them are ones you might expect to be taken if you either didn’t have a solid grasp of how if() parses its arguments or if you weren’t aware of what variables had been defined.

# Run with `cmake -P <script>`


# a string on its own (or with NOT, AND etc) is *always* expanded
# as though it were a variable, whether or not it is defined
if(unsetvar)
   message(FATAL_ERROR "unsetvar evaluates to TRUE")
else()
   message(STATUS "unsetvar evaluates to FALSE")
endif()

# !!! unsetvar is *not* expanded with STREQUAL, because it does not name a
#     variable!
if(unsetvar STREQUAL "")
   message(FATAL_ERROR "unsetvar equals the empty string")
elseif("unsetvar" STREQUAL "") # quoting does not matter
   message(FATAL_ERROR "quoted unsetvar equals the empty string")
elseif(unsetvar STREQUAL "somethingelse")
   message(FATAL_ERROR "unsetvar equals some other string")
elseif(unsetvar STREQUAL "unsetvar")
   message(STATUS "unsetvar does not equal the empty string")
else()
   message(FATAL_ERROR "unsetvar equals the empty string")
endif()

# note that this will fail to parse properly: we end up with
# if( STREQUAL ""), and STREQUAL wants two arguments
#if(${unsetvar} STREQUAL "")

# this works pretty much how you would expect, given what we said above
if("${unsetvar}" STREQUAL "unsetvar")
   message(FATAL_ERROR "expanded quoted unsetvar equals \"unsetvar\"")
elseif("${unsetvar}" STREQUAL "")
   # we end up with ("" STREQUAL "")
   message(STATUS "expanded quoted unsetvar equals the empty string")
else()
   message(FATAL_ERROR "expanded quoted unsetvar does not equal the empty string")
endif()



set(emptystringvar "")

# emptrystringvar is expanded to "", which evaluates to FALSE (see below
# for how variable values are evaluated)
if(emptystringvar)
   message(FATAL_ERROR "emptystringvar evaluates to TRUE")
else()
   message(STATUS "emptystringvar evaluates to FALSE")
endif()

# emptrystringvar is expanded to ""
if(emptystringvar STREQUAL "")
   message(STATUS "emptystringvar equals the empty string")
else()
   message(FATAL_ERROR "emptystringvar does not equal the empty string")
endif()

# as above: quoting does not matter
if("emptystringvar" STREQUAL "")
   message(STATUS "quoted emptystringvar equals the empty string")
else()
   message(FATAL_ERROR "quoted emptystringvar does not equal the empty string")
endif()

# as above: quoting does not matter
if(emptystringvar STREQUAL "emptystringvar")
   message(STATUS "emptystringvar equals quoted emptystringvar")
else()
   message(FATAL_ERROR "emptystringvar does not equal quoted emptystringvar")
endif()

# we end up with ("" STREQUAL "")
if("${emptystringvar}" STREQUAL "")
   message(STATUS "expanded quoted emptystringvar equals the empty string")
else()
   message(FATAL_ERROR "expanded quoted emptystringvar does not equal the empty string")
endif()

# !!! We end up with ("" STREQUAL "emptystringvar") which, as we know from
#     above, means emptystringvar is expanded to the empty string.
#     This is different from the unsetvar case.
if("${emptystringvar}" STREQUAL "emptystringvar")
   message(STATUS "expanded quoted emptystringvar equals the empty string")
else()
   message(FATAL_ERROR "expanded quoted emptystringvar does not equal the empty string")
endif()




set(emptystringvarvar "emptystringvar")

# emptystringvarvar is expanded to "emptystringvar", which evaluates to TRUE
if(emptystringvarvar)
   message(STATUS "emptystringvarvar evaluates to TRUE")
else()
   message(FATAL_ERROR "emptystringvarvar evaluates to FALSE")
endif()

# emptystringvarvar is expanded to "emptystringvar", which is not empty
if(emptystringvarvar STREQUAL "")
   message(FATAL_ERROR "emptystringvarvar equals the empty string")
elseif("emptystringvarvar" STREQUAL "") # remember: quoting is irrelevant here
   message(FATAL_ERROR "quoted emptystringvarvar equals the empty string")
elseif(emptystringvarvar STREQUAL "emptystringvar")
   # !!! remember: emptystringvar will be expanded to the empty string!
   message(FATAL_ERROR "emptystringvarvar equals \"emptystringvar\"")
elseif(emptystringvarvar STREQUAL "emptystringvarvar")
   # the variable is expanded on both sides
   message(STATUS "emptystringvarvar does not equal the empty string")
else()
   message(FATAL_ERROR "emptystringvarvar did not equal anything we threw at it")
endif()

# !!! we end up with ("emptystringvar" STREQUAL ""), which - if you look above
#     - evaluates to TRUE)
if("${emptystringvarvar}" STREQUAL "")
   message(STATUS "expanded quoted emptystringvarvar equals the empty string")
else()
   message(FATAL_ERROR "expanded quoted emptystringvarvar does not equal the empty string")
endif()




set(stringvar "foo")

# stringvar is expanded to "foo", which evaluates to TRUE
if(stringvar)
   message(STATUS "stringvar evaluates to TRUE")
else()
   message(FATAL_ERROR "stringvar evaluates to FALSE")
endif()

if(stringvar STREQUAL "")
   message(FATAL_ERROR "stringvar equals the empty string")
elseif("stringvar" STREQUAL "")
   message(FATAL_ERROR "quoted stringvar equals the empty string")
elseif("stringvar" STREQUAL "foo")
   # !!! stringvar is expanded to "foo", and "foo" is not expanded as it does
   #     not name a variable
   message(STATUS "\"stringvar\" equals \"foo\"")
else()
   message(FATAL_ERROR "stringvar did not equal anything we threw at it")
endif()
# but this also works:
if(stringvar STREQUAL "stringvar")
   # stringvar is expanded to "foo" on *both* sides
   message(STATUS "stringvar equals \"stringvar\"")
else()
   message(FATAL_ERROR "stringvar does not equal \"stringvar\"")
endif()

if("${stringvar}" STREQUAL "")
   # we end up with ("foo" STREQUAL ""), and foo is not the name of a variable,
   # and so is not expanded
   message(FATAL_ERROR "expanded quoted stringvar equals the empty string")
elseif("${stringvar}" STREQUAL "foo")
   # we end up with ("foo" STREQUAL "foo"), and foo is not the name of a
   # variable, and so is not expanded
   message(STATUS "expanded quoted stringvar equals \"foo\"")
else()
   message(FATAL_ERROR "expanded quoted stringvar does not equal \"foo\"")
endif()



set(stringvarvar "stringvar")

if(stringvarvar)
   # this branch (stringvarvar is expanded to "stringvar", which evaluates to TRUE)
   message(STATUS "stringvarvar evaluates to TRUE")
else()
   message(FATAL_ERROR "stringvarvar evaluates to FALSE")
endif()

if(stringvarvar STREQUAL "")
   # stringvarvar is expanded to "stringvar", which is not the empty string
   message(FATAL_ERROR "stringvarvar equals the empty string")
elseif("stringvarvar" STREQUAL "")
   # quoting does not matter
   message(FATAL_ERROR "quoted stringvarvar equals the empty string")
elseif("stringvarvar" STREQUAL "foo")
   # stringvarvar is expanded to "stringvar", which is not "foo"
   message(FATAL_ERROR "quoted stringvarvar equals \"foo\"")
elseif("stringvarvar" STREQUAL "stringvar")
   # !!! stringvarvar is expanded to "stringvar", but "stringvar" on the other
   #     side is expanded to "foo"
   message(FATAL_ERROR "expanded quoted stringvarvar equals the empty string")
elseif("stringvarvar" STREQUAL "stringvarvar")
   # of course, if you put the same thing on both sides, it always works
   message(STATUS "stringvarvar equals stringvarvar")
else()
   message(FATAL_ERROR "stringvarvar did not equal anything we threw at it")
endif()

if("${stringvarvar}" STREQUAL "")
   # we end up with ("stringvar" STREQUAL ""), and stringvar is then expanded
   # to "foo"
   message(FATAL_ERROR "expanded quoted stringvarvar equals the empty string")
elseif("${stringvarvar}" STREQUAL "foo")
   # !!! we end up with ("stringvar" STREQUAL "foo"), and stringvar is then
   #     expanded to "foo", but "foo" is not expanded
   message(STATUS "expanded quoted stringvarvar equals the empty string")
else()
   message(FATAL_ERROR "expanded stringvarvar did not equal anything we threw at it")
endif()
# this also works, because we end up with ("stringvar" STREQUAL "stringvar"),
# and both sides are expanded to "foo"
if("${stringvarvar}" STREQUAL "stringvar")
   message(STATUS "expanded stringvarvar equals \"stringvar\"")
else()
   message(FATAL_ERROR "expanded stringvarvar did not equal \"stringvar\"")
endif()


# OK, let's look more closely at what happens when you do
# if(variablename)

set(falsevar FALSE)
set(falsestringvar "FALSE")
set(zerostringvar "0")
set(notfoundvar "foo-NOTFOUND")

# variables are always expanded, so it doesn't matter if the variable is
# defined or not
if(unsetvar)
   # undefined is like an empty string, which is FALSE
   message(FATAL_ERROR "undefined variable evaluates to TRUE")
elseif(emptystringvar)
   # empty strings are false
   message(FATAL_ERROR "variable set to the empty string evaluates to TRUE")
elseif(falsevar)
   # obviously, FALSE is false
   message(FATAL_ERROR "variable set to FALSE evaluates to TRUE")
elseif(falsestringvar)
   # but the string "false" (case-insensitive) also works
   message(FATAL_ERROR "variable set to \"false\" evaluates to TRUE")
elseif(zerostringvar)
   # same with 0 (doesn't matter whether it was quoted in the set() call)
   message(FATAL_ERROR "variable set to \"0\" evaluates to TRUE")
elseif(notfoundvar)
   # a bit unexpected, but this is a language for build scripts
   message(FATAL_ERROR "variable set to \"foo-NOTFOUND\" evaluates to TRUE")
else()
   message(STATUS "all tested variables evaluated to FALSE")
endif()




# now, who would ever do a thing like this?
set(FALSE TRUE)
set(truestringvar "TRUE")

# This is the default if you don't set cmake_minimum_required to at least 2.6.4
# See `cmake --help-policy CMP0012`
cmake_policy(SET CMP0012 "OLD")

if(FALSE)
   # FALSE expands to TRUE
   message(STATUS "FALSE evaluates to TRUE")
else()
   message(FATAL_ERROR "FALSE evaluates to FALSE")
endif()
if(${falsestringvar}) # !!!
   # ${falsestringvar} is FALSE, which expands to TRUE
   message(STATUS "\${falsestringvar} evaluates to TRUE")
else()
   message(FATAL_ERROR "\${falsestringvar} evaluates to FALSE")
endif()
if(TRUE)
   # TRUE expands to nothing
   message(FATAL_ERROR "TRUE evaluates to TRUE")
elseif(${truestringvar})
   # ${truestringvar} is TRUE, which expands to nothing
   message(FATAL_ERROR "\${truestringvar} evaluates to TRUE")
else()
   message(STATUS "TRUE evaluates to FALSE")
endif()

# This is the default if you set cmake_minimum_required to at least 2.6.4
# See `cmake --help-policy CMP0012`
cmake_policy(SET CMP0012 "NEW")

if(FALSE)
   message(FATAL_ERROR "FALSE evaluates to TRUE")
elseif(${falsestringvar})
   message(FATAL_ERROR "\${falsestringvar} evaluates to TRUE")
else()
   message(STATUS "FALSE evaluates to FALSE")
endif()
if(TRUE)
   message(STATUS "TRUE evaluates to TRUE")
else()
   message(FATAL_ERROR "TRUE evaluates to FALSE")
endif()
if(${truestringvar})
   message(STATUS "\${truestringvar} evaluates to TRUE")
else()
   message(FATAL_ERROR "TRUE evaluates to FALSE")
endif()
About these ads

Tags: , , ,

3 Responses to “CMake Conditionals”

  1. Uniq Says:

    I don’t get your point. Once you understood how CMake evaluates variables and especially string variables, everything is fine. Ok, you don’t like. But this post is was to verbose to express that.

    • randomguy3 Says:

      Hmm… I think I worded that badly. I wasn’t trying to express my dislike of the if() syntax so much as point out subtle bugs it can introduce. I’ve reworded the introductory text.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: