CMake Correctly pass configuration options to subdirectory

Viewed 44

I want to pass configuration options (some STRINGs and BOOLs) to a subdirectory with its own CMake project. What is the correct way to do this since all three methods I have found, have major flaws?

  1. set(CACHE): You need to use set(CACHE) in the parent project as well, since it cache variables and normal variables don't interact in any logical manner (and it changes depending on the policy). Also you have to set the variable description in the the parent project, which also makes no sense.
  2. set(): Not an option since you cannot set any defaults except with a lot of if-statements.
  3. option(): Useless since you can only use BOOLs with it.

Is there something that I am missing? How do big projects handle this kind of problem?

Thanks in advance :)

3 Answers

Cache variables and normal variables interact logically as of CMake 3.21 with CMP0126 enabled.

In your project, just write:

cmake_minimum_required(VERSION 3.21)
project(example)

...

# include the next line if subdir is 3rd party
set(CMAKE_POLICY_DEFAULT_CMP0126 ON)

set(subdir_SOME_OPTION "value")
add_subdirectory(subdir)

The variable CMAKE_POLICY_DEFAULT_CMP0126 will enable sane cache interactions with normal variables, even when the subproject's cmake_minimum_required version is set below 3.21. In short, with CMP0126 normal variables always "win" over cache variables.

If you want subdir_SOME_OPTION to remain user-overridable, then you will need to make it into a cache variable with the same type and docstring as the subproject. A bit annoying, yes, but not too much of a burden. Alternatively, you can re-export the option with your own cache variable like example_SOME_OPTION and then set(subdir_SOME_OPTION "${example_SOME_OPTION}") if you have some use for this level of indirection.

That would look like this:

cmake_minimum_required(VERSION 3.21)
project(example)

set(CMAKE_POLICY_DEFAULT_CMP0126 ON)

set(example_SOME_OPTION "default-value"
    CACHE STRING "documentation")

set(subdir_SOME_OPTION "${example_SOME_OPTION}")
add_subdirectory(subdir)

What is the correct way

add_subdirectory inherits all variables. Just set() the variable.

Is there something that I am missing?

option() and set(.... CACHE) are for configuration options and/or cache variables. A variable doesn't have to be in cache to be visible in a subdirectory. Just set() it normally. Set a variable to be in cache, if you have a specific reason for it, like you want the variable to be preserved between configuration runs, or it should show up in ccmake and gui apps. That's what CACHE variables are for.

How do big projects handle this kind of problem?

They set() the variables. For example kwsys in cmake sources.

All three methods are usable depending on the way how subproject processes its parameter and how the outer project's setting is intended to interact with the user.

  1. Inner project works with the CACHE variable: creates it using set(CACHE) or option():

    # Inner project
    set(FOO_TOOL_PATH "/inner/path" PATH "Path to foo tool")
    option(ENABLE_BAR "Whether need to use bar" FALSE)
    

    In that case the outer project could set CACHE variable too:

    # Outer project, user cannot modify the value.
    set(FOO_TOOL_PATH "/outer/path" INTERNAL "Path to foo tool, selected by <me>")
    # Use `set` because we need to use INTERNAL keyword and 'option()' doesn't support it.
    set(ENABLE_BAR TRUE INTERNAL "Need to use bar")
    

    or, if it wants the value to be modifiable by the user,

    # Outer project, user may modify that value.
    set(FOO_TOOL_PATH "/outer/path" PATH "Path to foo tool")
    option(ENABLE_BAR "Whether need to use bar" TRUE)
    
  2. Inner projects works with normal (non-CACHE) variable: checks its existence using if and, if unset, possibly creates using set():

    # Inner project
    if(NOT FOO_TOOL_PATH)
       set(FOO_TOOL_PATH "/inner/path")
    endif()
    

    If the outer project wants to assign fixed value to the variable, then the variable could be defined using plain set():

    # Outer project, user cannot modify the value.
    set(FOO_TOOL_PATH "/outer/path")
    

    If the outer project wants the value to be modifiable by the user, then it could use either if plus set(), like the inner project

    # Outer project, user may modify that value.
    if(NOT FOO_TOOL_PATH)
       set(FOO_TOOL_PATH "/outer/path")
    endif()
    

    or it could create a CACHE variable:

    # Outer project, user may modify that value.
    set(FOO_TOOL_PATH "/outer/path" CACHE PATH "Path to foo tool")
    
Related