CMake Tips & Tricks: Drop Down List

In a recent CMake project I was setting up, I wanted users to be able to choose one of several possible libraries at project generation, to make performance comparisons easy on multiple platforms.  This is easy enough to do with a configuration parameter, but since the libraries available were a limited set offering the available options seemed better.  I discovered that in the CMake GUI it is possible to have a drop down menu of options for a given property, and it’s actually quite easy.  The only thing to keep in mind is that this approach doesn’t enforce anything; the user could still enter other options.  Since this is only used by developers to generate projects, I didn’t particularly care.  They break it, they bought it, as it were.

First, we use a cache variable and enumerate the options for our drop-down list:

SET(LIBRARY_TO_USE "Option1" CACHE STRING "library selected at CMake configure time")

SET_PROPERTY(CACHE LIBRARY_TO_USE PROPERTY STRINGS Option1 Option2 Option3) 

After that it’s just a matter of changing the things that should change when this option changes.  There are a couple possible approaches here, though none of them completely satisfy my aesthetic sense.

1.       The first is simply to call everything on every invocation, but that defeats the purpose of the caching in the first place. 

2.      I can make sure this .cmake file is included at the top of the project CMakeLists.txt, so it is called before anything else that might include this library.  In that case I can check the LIBRARY_FOUND variable, which is set the first time any of these libraries are loaded during a build.  The upside of this is that if multiple files include this .cmake file it will only reload everything once per project generation.  The downside is that it relies on not having someone load the library before this file is included, and that was a deal breaker; I don’t want to rely on implicit assumptions.  Also, it still reloads the cache once per build. On the up side, if I want to vary non-cache values this allows me to group all the change logic in one place.

3.       The final option is explicitly checking to see if the variable has changed by caching the last value inside of the has-changed if statement. This requires using a second cached variable to hold state and initializing it if it is undefined. Additionally, this variable should never be changed by a user, so I use MARK_AS_ADVANCED to hide it from the GUI.

I used option three, which looks like:

IF(NOT(DEFINED LIBRARY_LAST))

     SET(LIBRARY_LAST "NotAnOption" CACHE STRING "last library loaded")

     MARK_AS_ADVANCED (FORCE LIBRARY_LAST)

ENDIF()

IF(NOT (${LIBRARY_TO_USE} MATCHES ${LIBRARY_LAST}))

     UNSET(LIBRARY_INCLUDES CACHE)

     SET(LIBRARY_LAST ${LIBRARY_TO_USE} CACHE STRING "Updating Library Project Configuration Option" FORCE)

ENDIF()

The important part of this is “UNSET”.  Any cached variables that are set in the Find.cmake file will need to be explicitly cleared in order for them to be actually updated.  The rest of it is simply determining whether or not the parameter changed.

Finally, we need to change parameters on the basis of what option is selected.  If only cache variables change, we can include this in the “if changed” loop, but I was using non-cached variables accessed by the Find.cmake files, so I set these each time.  It would be cleaner to separate these into their own CMake files with a regular naming scheme, but since I was only setting one parameter I didn’t bother.  This looked like:

IF(${LIBRARY_TO_USE} MATCHES "Option1")

     SET(LIBRARY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/Vendor/Option1”)

ENDIF()

    

IF(${LIBRARY_TO_USE} MATCHES "Option2")

     SET(LIBRARY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/Vendor/Option2")

ENDIF()

Etc.  The path naming conventions were not actually that regular either, or wouldn’t have needed the switch statement.

All of these went into a SetLibraryOptions.cmake file, and I added INCLUDE(SetLibraryOptions.cmake) to the root level CMakeLists.txt file.  When I included this library in a future target, I used the regular package syntax with ${LIBRARY_TO_USE} as the package name.  This is why it was so useful to have a drop down menu here: each package name must exactly match the format of the Find.cmake file.  T

Now, when I use the library in another package the include will look something like:

IF (NOT LIBRARY_FOUND)

     FIND_PACKAGE(${LIBRARY_TO_USE} REQUIRED)

     IF(NOT LIBRARY_FOUND)

           MESSAGE(FATAL_ERROR “failed to find “ ${LIBRARY_TO_USE})

     ENDIF()

ENDIF()

And that’s it; when the user selects a different library all of the projects will be regenerated with the new option.  The final result looks like: