List of most important and complex Beetroot functionalities

Advanced and convenient handling of targets’ parameters

Parameters to each target are handled in three ways:

  1. Default values are specified in the targets.cmake, when declaring the parameters.
  2. Default values are overriden by existing variables that have the same name.
  3. Existing variables are overriden by named arguments given to the get_target and build_target functions.

Furthermore, system allows for default values to include references to the any variables (including defined parameters themselves) and any other CMake string processing commands. For instance, this is a perfectly valid definition:

get_filename_component(TMP "${ARCH_PREFIX}" NAME)

set(DEFINE_MODIFIERS
        ARCH    SCALAR  CHOICE(GPU;CPU) CPU
        ARCH_PREFIX     SCALAR  STRING "${SUPERBUILD_ROOT}/external-${ARCH}"
        ARCH_RELDIR     SCALAR  STRING  "${TMP}"
)

The intention is to let the user specify only the ARCH parameter, and the ARCH_PREFIX and ARCH_RELDIR gets calculated from it.

Target definition can make multiple physical targets, each one with different set of parameters

When you require a target FOO and specify target parameters, the target with this specific set of parameters will be defined and build. If you require the same target FOO with different set of parameters, by default Beetroot will define another instance of the same target, built using the other set of parameters. The targets’ CMake names (and file names) will have a numeral suffix to distinguish them apart. This implies that in general you cannot know the name of the target, until you query for it (or you declared that there can be only one instance of it).

Special handling for target parameters that describe features

Sometimes a parameter defines additional feature of the target, that is turned on/advanced independently of the rest of the target functionality. Parameter is a feature, when the dependee who does not require it can still use the target that has it. Features can be flags like "FORTRAN_SUPPORT" (if Fortran support does not affect the rest) or a "VERSION" (if target development makes sure the code backward compatible ). Support for Features makes re-using the same target in more than one place much better, especially if the target is heavy to build (e.g. big external dependency) and have many different features that can be turned on/off that are consumed by different parts of our project.

Target definition file can describe more than one template/target

There is one-to-many relationship between target definition files and target definition. User can put multiple target/template names in the targets.cmake. First parameter of generate_target() and declare_dependencies() is the name of the target/template, so the user code that handles target definition and dependencies can handle each target/template in different way. The feature was introduced to faciliate defining multiple similar targets in one file. It is used typically in external targets, where e.g. Boost defines multiple targets.

Support for CMake code that do not produce targets

Sometimes we need CMake to do something that does not necessary ends up being a new target, but rather modify existing target. This situation include dedicated code for definition of unit tests (of course unit tests can also be defined inside generate_target() that defines the executable), code that applies additional preprocesor definitions or old external targets that not use imported targets CMake mechanism.

Comprehensive error checking

We spent a great deal of effort to catch as many usage errors as possible. In particular:

  • We make sure that template modifiers and parameters share the same name space and report all violations.
  • System makes sure, that the actual template(s) you want to build actually produce targets.
  • System makes sure that dependencies that do not generate targets are not dependent on templates that also do not produce targets.
  • System detects lack of both generate_target() and apply_to_target() for internal projects.
  • System detects and forbids variables that start with double underscore, as this can interfere with the Beetroot’s inner working.
  • System checks the types and possible values of all arguments against a declaration in targets.cmake.

External dependencies are external to the Beetroot system

The system faciliates calling them, manages their external dependencies, build and install location, but ultimately it calls them as if they were a simple external CMake projects, builds them and installs them in the totally normal way.

Automatic superbuild by default

If there is at least one external target defined, all external dependencies are built in the superbuild phase, using the dependencies derrived from the template definitions. Only then the project build is called (implemented as a “external” dependency of all actual external dependencies), so all external dependencies are always already built and installed when you use them. If there are no external dependencies, CMake will build project directly.

List of the most visible features of the Beetroot

Targets are defined inside the function generate_target() in target definition file

In CMake targets (in contrast to variables) are internally represented by the object which lives in a global namespace, even if defined inside the function. Putting target definitions inside a function prevents leaking of temporary variables and pollution of the variable namespace.

Dependencies of targets are set inside the function declare_dependencies() in target definition file

Target dependencies are handled by function rather than data structure, which allows for maximum flexibility (dependencies can depend in complicated way on the target parameters/features). Because Beetroot structure needs dependencies to be resolved before target definition (and possibly be called multiple times on the same target), the only place to put them is in a dedicated user-supplied function. Code inside this function should be omnipotent, because it can be executed multiple times in a single run. The code will be executed only during the target declaration phase.

By default the code you write (targets.cmake) does not depend on your target name

Unless instructed otherwise, the system dictates the name you give to each target. This way targets’ names are not fixed, and it is possible to have multiple instances of them. This fact is used to let the target definition files (targets.cmake) define whole family of targets parametrized by the target parameters and features. The beetroot guarantees, that for each distinct set of target parameter there will be a separate target defined and built.

There is only one type of user-supplied input file that defines the targets

All code that define targets and their dependencies can be placed inside so-called target definition files. These files can be put anywhere in the project and must be named targets.cmake, or be placed in the special subfolder cmake/targets and have an extension .cmake. The latter files usually define external dependencies. The only thing that is influenced by the location of the file, is the value of the ${CMAKE_CURRENT_SOURCE_DIR} CMake variable available in generate_target() user function.

The user file works by defining any of the following cmake variables: ENUM_TEMPLATES, ENUM_TARGETS, BUILD_PARAMETERS, LINK_PARAMETERS, BUILD_FEATURES FILE_OPTIONS and DEFINE_EXTERNAL_PROJECT and by defining any of the following functions: generate_targets(), declare_dependencies() and apply_dependency_to_target(). Of course, not all combinations of those definitions are legal and any violation of the legality of the definitions is cought and meaningfully reported to the user.

The other file a user needs to supply is a CMakeLists.txt. This file serves as a point of entry. This file should consist of a standard boilerplate code, calls to the build_target() and finally a call to finalize(). Standard CMake commands should not be used to define targets. The only purpose of this file is to specify what targets with what parameters must be build by calling a Beetroot function build_target() or get_target() and letting it do the work.

The role of the CMakeLists.txt is hugely downplayed

There is no need to use add_subdirectory(), because Beetroot knows where to look for every managed target. That’s why the only CMakeLists.txt that is needed is the one you manually call with cmake ... (Besides CMakeLists.txt inside CMake external dependencies of your project)

The location of the CMakeLists.txt is no longer relevant

As long as the CMakeLists.txt is somewhere inside the root project and it adheres to the Beetroots’ mandatory boilerplate code, its location is irrelevant. All components are searched for by name, not by folder, and the system requires them to be written in a way that all paths are absolute (it is achieved by simply prefixing filenames with the ${CMAKE_CURRENT_SOURCE_DIR}).

As long Beetroot is responsible for all targets in your code, you can simply copy a CMakeLists.txt from one subfolder of your project to another and they will build just fine there, resulting in exactly the same executable.