Skip to content

CMake boilerplate for an opinionated project structure.

License

Notifications You must be signed in to change notification settings

thejohnfreeman/cupcake.cmake

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cupcake.cmake

GitHub checks GitHub releases CMake

cupcake.cmake is a CMake module. It is named with the .cmake extension to distinguish it from cupcake.py, which is a Python tool for working with Conan and CMake projects, with additional features for projects using cupcake.cmake.

Install

cupcake.cmake requires CMake >= 3.21. The recommended method to import cupcake.cmake is with find_package():

find_package(cupcake.cmake REQUIRED)

Unlike include(), find_package() lets us easily check version compatibility and lean on package managers like Conan. For that to work, an installation must be found on the CMAKE_PREFIX_PATH. There are a few ways to accomplish that.

Install from Conan

First, add cupcake.cmake as a non-tool1 requirement to your Conan recipe:

requires = ['cupcake.cmake/1.2.1']

Second, tell Conan how to find cupcake.cmake. You can either:

  • Point it to my public Redirectory:

    conan remote add redirectory https://conan.jfreeman.dev
  • Copy the recipe from this project:

    conan export .

Install manually

# In this project:
cmake -B <build-dir> -DCMAKE_INSTALL_PREFIX=<path> .
cmake --build <build-dir> --target install
# In your project:
cmake -B <build-dir> -DCMAKE_PREFIX_PATH=<path> .

Install as submodule

Alternatively, you can embed this project in yours as a submodule and import it with add_subdirectory():

add_subdirectory(path/to/cupcake.cmake)

Interface

There are two categories of commands, general and special. General commands have no special requirements, but special commands require a cupcake.json file in the project's root directory. Special commands effectively relocate essential CMake configuration data from multiple CMake listfiles sprinkled throughout a project to a single JSON file that is more easily read and written by other tools. Special commands are documented with example cupcake.json and Pythonic pseudocode.

General commands

Special commands

Examples

A project using only general commands might look like this:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(example LANGUAGES CXX)
find_package(cupcake REQUIRED)
cupcake_project()
cupcake_find_package(abc)
cupcake_add_library(example)
target_link_libraries(${this} PUBLIC abc::abc)
cupcake_add_executable(example)
target_link_libraries(${this} PUBLIC example.libexample)
cupcake_enable_testing()
cupcake_install_project()
cupcake_install_cpp_info()
# tests/CMakeLists.txt
cupcake_find_package(xyz PRIVATE)
cupcake_add_test(example)
target_link_libraries(${this} PUBLIC xyz::xyz example.libexample)

A project using special commands might look like this:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.21)
project(example LANGUAGES CXX)
find_package(cupcake REQUIRED)
cupcake_project()
cupcake_find_packages(main)
cupcake_link_libraries(example.imports.main INTERFACE main)
cupcake_add_libraries()
cupcake_add_executables()
cupcake_enable_testing()
cupcake_install_project()
cupcake_install_cpp_info()
# tests/CMakeLists.txt
cupcake_find_packages(test PRIVATE)
cupcake_link_libraries(example.imports.test INTERFACE test)
cupcake_add_tests()
// cupcake.json
{
    "project": {
        "name": "example"
    },
    "imports": [
        { "name": "abc", "file": "abc", "targets": ["abc::abc"] },
        { "name": "xyz", "file": "xyz", "targets": ["xyz::xyz"], "groups": ["test"] }
    ],
    "libraries": [
        { "name": "example", "links": ["abc::abc"] }
    ],
    "executables": [
        { "name": "example", "links": [{ "target": "example.libexample", "scope": "PUBLIC" }] }
    ],
    "tests": [
        {
            "name": "example", "links": [
                "xyz::xyz",
                { "target": "example.libexample", "scope": "PUBLIC" }
            ]
        }
    ]
}

cupcake_project

⬆️ #️⃣ general

cupcake_project()

Define project variables used by other cupcake.cmake commands, and choose different defaults for built-in CMake commands and variables.

cupcake_project() must be called after a call of the built-in CMake command project() and before any other cupcake.cmake commands in that project. It should be called in the project's root CMakeLists.txt. The recommended pattern looks like this:

project(${PROJECT_NAME} LANGUAGES CXX)
find_package(cupcake REQUIRED)
cupcake_project()

cupcake_project() takes no arguments directly, instead taking them all from the variables set by the call to project(). I would have liked this command to wrap the call to project(), if possible, but CMake requires a "literal, direct call to the project() command" in the root CMakeLists.txt, and thus it cannot be wrapped.

cupcake_project() adds one special INTERFACE library target, ${PROJECT_NAME}.imports.main, that projects can use to aggregate the "main" group of required libraries. Other targets can conveniently link to this one target instead of to each requirement individually, and automatically link to new requirements as they are added. Projects can use the special command cupcake_link_libraries() to link all the "main" required libraries listed in cupcake.json.

cupcake_project() adds three more special internal targets:

  • ${PROJECT_NAME}.libraries: An INTERFACE library target.
  • ${PROJECT_NAME}.executables: A custom target.
  • ${PROJECT_NAME}.tests: A custom target.

All three are excluded from the "all" target. Each has an external alias with the same name, except the dot (.) is replaced with a double colon (::). They each depend on all of the libraries, executables, or tests, respectively, added (by a cupcake_add_<target>() command) in the project directly , i.e. not in a subproject. Further, if the project is the root project, equivalent targets are available under the unqualified names libraries, executables, and tests, respectively. These targets are intended to be automatic groups with names that can be easily passed to cmake --build.

cupcake_project() changes these default behaviors:

# Variable Value
1 CMAKE_POLICY_DEFAULT_CMP0087 NEW
2 CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE
3 CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL
3 CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC
4 CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/external
5 CMAKE_CXX_VISIBILITY_PRESET hidden
5 CMAKE_VISIBILITY_INLINES_HIDDEN TRUE
6 CMAKE_EXPORT_COMPILE_COMMANDS TRUE
7 CMAKE_BUILD_RPATH_USE_ORIGIN TRUE
7 CMAKE_INSTALL_RPATH ${origin} ${origin}/${relDir}
8 CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_OUTPUT_PREFIX}/${CMAKE_INSTALL_BINDIR}
8 CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_OUTPUT_PREFIX}/${CMAKE_INSTALL_LIBDIR}
8 CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_OUTPUT_PREFIX}/${CMAKE_INSTALL_LIBDIR}
  1. Lets install(CODE) use generator expressions, which is required by cupcake_install_cpp_info().
  2. Makes find_package() try Config mode before Module mode by default.
  3. Sorts packages by latest semantic version when multiple versions are installed.
  4. Lets find_package() find the project's Find Modules.
  5. Treats symbols in shared libraries as private by default, hiding them. Harmonizes the default on non-Windows platforms with the existing defaults on Windows and for C++ modules. Public symbols must be explicitly exported with annotations. These annotations are supplied by preprocessor macros defined in headers generated for each library by cupcake_add_library().2
  6. Generates a "compilation database" file (compile_commands.json) used by language servers like clangd.
  7. Uses relative rpaths both when building and installing, which lets you move your build directory or install prefix without breaking the executables underneath.
  8. See section "Output directory".

cupcake_project() adds these project variables (different for each subproject):

  • PROJECT_EXPORT_SET: The name of the default export set for the project's exported targets.
  • PROJECT_EXPORT_DIR: The directory in which to place generated export files, i.e. the package configuration file, the package version file, and any target export files.
  • ${PROJECT_NAME}_FOUND: TRUE, to short-circuit calls to find_package() looking for this project's package from other subprojects, e.g. examples or siblings.

cupcake_project() adds these global variables (same for all subprojects):

Output directory

CMake has a variable, CMAKE_RUNTIME_OUTPUT_DIRECTORY, that chooses the default value for the target property RUNTIME_OUTPUT_DIRECTORY, which chooses the output directory for RUNTIME targets, which includes executables on all platforms and DLLs on Windows. That is, each runtime target (an executable or a DLL) has a property RUNTIME_OUTPUT_DIRECTORY that chooses where it is placed, and the default value for that target property is the value of the variable CMAKE_RUNTIME_OUTPUT_DIRECTORY when the target is added (with add_executable() or add_library()). Setting this variable is necessary on Windows to ensure that DLLs end up in the same directory as the executables (including tests) that load them, which is where those executables look for them.

We can use this variable and its cousins, CMAKE_ARCHIVE_OUTPUT_DIRECTORY (for static libraries) and CMAKE_LIBRARY_OUTPUT_DIRECTORY (for shared libraries on non-Windows platforms), to construct at build-time (i.e. before installation) a directory structure that mimics, in part, the structure that will be created by an installation, and isolated from the intermediate files littering the build directory. In other words, they let us create a pseudo-installation where builders (and tools) can inspect and use the interesting outputs of a build before they are installed, including outputs like tests that will not be installed.

Each build configuration requires a separate pseudo-installation because they are not guaranteed to use unique file names. Each pseudo-installation is rooted at a CMAKE_OUTPUT_PREFIX, akin to CMAKE_INSTALL_PREFIX. All output prefixes are nested under a common output directory, CMAKE_OUTPUT_DIR, akin to CMAKE_BINARY_DIR. In fact, CMAKE_OUTPUT_PREFIX is just ${CMAKE_OUTPUT_DIR}/$<CONFIG>.

There is no similar variable choosing the output directory for generated headers, just like there is no add_header() command to add a generated header as a target. cupcake.cmake fills this gap by defining the variable CMAKE_HEADER_OUTPUT_DIRECTORY. Generated headers are not configuration-specific, though, so they are not placed under any output prefix. Instead, CMAKE_HEADER_OUTPUT_DIRECTORY is just ${CMAKE_OUTPUT_DIR}/Common/${CMAKE_INSTALL_INCLUDEDIR}.

cupcake_find_package

⬆️ #️⃣ general

cupcake_find_package(<package-name> [<version>] [PRIVATE] ...)

Import targets from a requirement by calling find_package().

<version> is forwarded to find_package(), but it is an optional parameter for this command. I recommend that you do not include it. Instead, version declaration and checking should happen at the package manager level, e.g. in your Conan recipe.

In the underlying call to find_package(), REQUIRED is always passed so that missing requirements raise an error. Optional requirements should always be guarded by an option, e.g. with_xyz, rather than conditionally linking based on whether or not CMake succeeded in finding them.

Unless PRIVATE is passed, this command saves the package name (but not the version, even when given) in a list of dependencies for the project. That list is kept in a DIRECTORY property of PROJECT_SOURCE_DIR named PROJECT_DEPENDENCIES. It affects the behavior of cupcake_install_project(): the generated package configuration file will transitively call find_dependency() for all non-private dependencies.

Remaining arguments are passed through to find_package().

cupcake_find_package() returns a variable <package-name>_TARGETS containing a list of the targets imported by the command.

cupcake_add_subproject

⬆️ #️⃣ general

cupcake_add_subproject(<name> [PRIVATE] [<path>])

Import targets from a requirement by calling add_subdirectory().

<path> is forwarded to add_subdirectory() as the <source_dir> argument. If it is absent, then <name> is used instead. Relative paths, like the subproject name, are relative to CMAKE_CURRENT_SOURCE_DIR.

<name> should match the name passed to the project() command in the CMakeLists.txt of the subdirectory.

PRIVATE has the same meaning as it does for cupcake_find_package().

cupcake_add_library

⬆️ #️⃣ general

cupcake_add_library(<name> [PRIVATE])

Add targets for a library by calling add_library().

Unless PRIVATE is passed, the library is exported, meaning it is included when installing the project. PRIVATE libraries can be good for sharing code among tests.

cupcake_add_library() adds an internal target named ${PROJECT_NAME}.libraries.<name>, with an abbreviated alias named ${PROJECT_NAME}.l.<name>. If the library has the same name as the project, then it is the project's default library target and gets the additional alias ${PROJECT_NAME}.library.

If the library is exported, then cupcake_add_library() adds external ALIAS targets matching all of the above names, but with the dot (.) separators replaced with double colons (::), i.e. ${PROJECT_NAME}::libraries::<name>, ${PROJECT_NAME}::l::<name>, and ${PROJECT_NAME}::library.

Commands in the same project should use the internal target name. Commands in different projects, even if they are children (e.g. examples) or siblings (e.g. fellow dependencies) sharing the same root project, should use the external target name. The external target names match the ones supplied by the installed package configuration file (see cupcake_install_project()) and/or the installed cpp_info.py script (see cupcake_install_cpp_info()).

The internal target is added as a dependency of the internal INTERFACE library target ${PROJECT_NAME}.libraries, which has an external alias ${PROJECT_NAME}::libraries, and an internal alias libraries if the project is the root project (see cupcake_project()).

cupcake_add_library() returns a variable, this, with the name of the internal target for convenient use in subsequent commands. Commands configuring the target should be called immediately after it, to keep all of a target's configuration in one place.

A library's public headers must be either the single file include/<name>.hpp (or .h) or every file under the directory include/<name>/. Private headers may be placed under src/lib<name>/. Exported libraries export their public headers only.

If a library has sources, they should be either the single file src/lib<name>.cpp or every .cpp file under the directory src/lib<name>/. If a library does not have sources, i.e. if it is a header-only library, then the target will be an INTERFACE library. If a library does have sources, then the target will be a STATIC or SHARED library depending on the value of variable BUILD_SHARED_LIBS.

A library may include its own public headers by their paths relative to include/, and its own private headers by their paths relative to the project's root directory (i.e. starting with src/lib<name>/), but it may not include other headers in the project, even relative to those same directories, unless it links to a library exporting those headers. In fact, it cannot include unlinked headers because cupcake_add_library() creates temporary symbolic links in the build directory pointing to the permitted headers, and only those will be found by the compiler.

Each library is given two generated headers. These headers are installed with the library (if it is installed). Libraries must not define their own public headers with these names.

  • <name>/export.hpp: An export header with preprocessor macros for annotating public and deprecated symbols in shared libraries.
    • ${NAME_UPPER}_EXPORT
    • ${NAME_UPPER}_DEPRECATED
  • <name>/version.hpp: A version header with preprocessor macros deconstructing the package version string.

cupcake_add_executable

⬆️ #️⃣ general

cupcake_add_executable(<name> [PRIVATE])

Add targets for an executable by calling add_executable().

Unless PRIVATE is passed, the executable is exported, meaning it is included when installing the project. PRIVATE executables can be good for manual testing.

cupcake_add_executable() adds an internal target named ${PROJECT_NAME}.executables.<name>, with an abbreviated alias named ${PROJECT_NAME}.e.<name>. If the executable has the same name as the project, then it is the project's default executable target and gets the additional alias ${PROJECT_NAME}.executable.

If the executable is exported, then cupcake_add_executable() adds external ALIAS targets matching all of the above names, but with the dot (.) separators replaced with double colons (::), i.e. ${PROJECT_NAME}::executables::<name>, ${PROJECT_NAME}::e::<name>, and ${PROJECT_NAME}::executable.

Commands in the same project should use the internal target name. Commands in different projects, even if they are children (e.g. examples) or siblings (e.g. fellow dependencies) sharing the same root project, should use the external target name. The external target names match the ones supplied by the installed package configuration file (see cupcake_install_project()) and/or the installed cpp_info.py script (see cupcake_install_cpp_info()).

The internal target is added as a dependency of the internal custom target ${PROJECT_NAME}.executables, which has no external alias, but has an internal alias executables if the project is the root project (see cupcake_project()).

cupcake_add_executable() returns a variable, this, with the name of the internal target for convenient use in subsequent commands. Commands configuring the target should be called immediately after it, to keep all of a target's configuration in one place.

An executable must have sources, and they should be either the single file src/<name>.cpp or every .cpp file under the directory src/<name>/.

An executable may include its own private headers by their paths relative to the project's root directory (i.e. starting with src/<name>/), but it may not include other headers in the project, even relative to the same directory, unless it links to a library exporting those headers. In fact, it cannot include unlinked headers because cupcake_add_library() creates temporary symbolic links in the build directory pointing to the permitted headers, and only those will be found by the compiler.

cupcake_add_executable() adds two more internal targets. The first is named execute.${PROJECT_NAME}.<name>, aliased as execute.<name> if the project is the root project, and as execute if the executable name matches the project name too. It is a custom target that executes the executable. You can invoke it yourself with the following command instead of digging around in the output directory to find the executable.

cmake --build <build-dir> --target execute.<name>

Additionally, the target passes any CMake list of command-line arguments found in the environment variable CUPCAKE_EXE_ARGUMENTS.3 In other words, if you want to pass any command-line arguments through the custom target to the executable, then you must set environment variable CUPCAKE_EXE_ARGUMENTS to a semicolon-separated (;) list of string arguments, where each argument internally escapes any semicolons (with \;) and is double-quoted (") if it contains any whitespace. If you use cupcake.py, then it will set CUPCAKE_EXE_ARGUMENTS to forward any trailing arguments you pass to cupcake exe <name>.

The second target is named debug.${PROJECT_NAME}.<name>, aliased as debug.<name> if the project is the root project, and as debug if the executable name matches the project name too. It is a custom target that works almost like execute.${PROJECT_NAME}.<name>, except that it executes GDB, the GNU debugger, on the executable target, and initializes it with a command file that sets the command line arguments found in CUPCAKE_EXE_ARGUMENTS and then copies whatever is in the .gdbinit file of your current directory. If you use cupcake.py, then it will set CUPCAKE_EXE_ARGUMENTS to forward any trailing arguments you pass to cupcake debug <name>.

cupcake_enable_testing

⬆️ #️⃣ general

cupcake_enable_testing()

Conditionally add tests to the project, in the style of enable_testing().

The command does nothing if the project is not top-level. Dependents generally want to run a dependency's tests only when the dependency is installed, if at all, not every time the dependent runs its own tests.

If the project is top-level, then the command imports the CTest module. If BUILD_TESTING is ON, which it is by default, then the command calls add_subdirectory(tests) and adds a special INTERFACE library target named ${PROJECT_NAME}.imports.test, akin to the ${PROJECT_NAME}.imports.main target added by cupcake_project(), i.e. a convenient target that projects can use to aggregate the "test" group of required libraries.

Individual tests should be added in the CMakeLists.txt of the tests/ subdirectory. Dependencies that only the tests require should be imported there too.

cupcake_add_test

⬆️ #️⃣ general

cupcake_add_test(<name>)

Add targets for an executable by calling add_executable(), and add a CMake test whose command executes that executable by calling add_test().

In CMake's documentation, the term "test", which I call here a "CMake test", refers to a command that exits with code 0 if and only if it passes. Typical commands simply execute an executable target in the project. cupcake_add_test() implements exactly this pattern. In this document, the term "test" refers to the executable, the target, and the CMake test as a single unit. Where necessary, I differentiate them.

cupcake_add_test() adds an internal target named ${PROJECT_NAME}.tests.<name>, with an abbreviated alias named ${PROJECT_NAME}.t.<name>, as a dependency of the target ${PROJECT_NAME}.tests. If the project is the root project, then it gives the internal target additional unqualified aliases tests.<name> and t.<name> and adds it as a dependency of the unqualified target tests. Tests are never exported, meaning they are never installed nor given external targets. cupcake_add_test() defines the variable this in the parent scope just like cupcake_add_library() and for the same reason.

The target is excluded from the "all" target. This way, resources are not spent building tests unless they are run.

cupcake_add_test() should be called only from the tests subdirectory, where all tests should live. The executable must have sources, and they should be either the single file tests/<name>.cpp or every .cpp file under the directory tests/<name>/.

The executable may include its own private headers by their paths relative to the project's root directory (i.e. starting with tests/<name>/), but it may not include other headers in the project, even relative to the same directory, unless it links to a library exporting those headers. In fact, it cannot include unlinked headers because cupcake_add_library() creates temporary symbolic links in the build directory pointing to the permitted headers, and only those will be found by the compiler.

The CMake test is added to the list of tests run by CTest. It is given a fixture that builds (or rebuilds) the executable before it is run.

cupcake_install_project

⬆️ #️⃣ general

cupcake_install_project()

Add rules to install all exported targets.

cupcake_install_project() installs a package configuration file that calls find_dependency() for all non-PRIVATE packages imported with cupcake_find_package(), and exports all non-PRIVATE libraries and executables added (by cupcake_add_<target>() commands) in the project directly, i.e. not in a subproject. These targets are exported with their external names, i.e. qualified by the project namespace, e.g. ${PROJECT_NAME}::libraries::<name>.

cupcake_install_project() installs a package version file too.

cupcake_install_project() should be called only once, after all exported targets have been added. It should be called from the project's root CMakeLists.txt.

cupcake_install_cpp_info

⬆️ #️⃣ general

cupcake_install_cpp_info()

Add rules to install package metadata for Conan.

This command adds an installation rule to install a Python script at ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_EXPORTDIR}/<PackageName>/cpp_info.py. That script can be executed within the package_info() method of a Python Conan recipe (conanfile.py) to fill in the details of the cpp_info attribute:

def package_info(self):
    path = f'{self.package_folder}/share/{self.name}/cpp_info.py'
    with open(path, 'r') as file:
        exec(file.read(), {}, {'self': self.cpp_info})

cupcake_find_packages

⬆️ #️⃣ special

cupcake_find_packages(<group> ...)

Import targets from all requirements belonging to a group.

cupcake_find_packages() first selects all objects in the .imports array of cupcake.json with a .groups array property (default value ["main"]) that contains <group>. Then, it calls cupcake_find_package() for each selected object, passing the .file string property of the object (or if that is missing, the .name string property) and any additional arguments that were passed to cupcake_find_packages().

{
    "imports": [
        { "name": "a", "file": "a", "targets": ["a::a"] },
        { "name": "b", "file": "b", "targets": ["b::b"], "groups": ["main"] },
        { "name": "c", "file": "c", "targets": ["c::c"], "groups": ["test"] }
    ]
}
def cupcake_find_packages(group, *args):
    metadata = json.parse('cupcake.json')
    for package in metadata.get('imports', []):
        if group in package.get('groups', ['main']):
            cupcake_find_package(package['file'] or package['name'], *args)

Note: The .name property is the name of the package in the Conan ecosystem, while the .file property is the name of the package configuration file that the CMakeDeps generator generates for it, corresponding to the cmake_file_name property of the package recipe's cpp_info. When the cmake_file_name property is missing, CMakeDeps uses the package name as its default value.

cupcake_link_libraries

⬆️ #️⃣ special

cupcake_link_libraries(<target> <scope> <group>)

Link a target to all imported targets of all requirements belonging to a group.

cupcake_link_libraries() is typically called to link a convenience INTERFACE target, e.g. ${PROJECT_NAME}.imports.main or ${PROJECT_NAME}.imports.test, to the targets of its corresponding requirement group, following a call to cupcake_find_packages() for that group.

cupcake_link_libraries() first selects all objects in the .imports array of cupcake.json with a .groups array property (default value ["main"]) that contains <group>. Then, it calls target_link_libraries() for each selected object, passing <target>, <scope>, and the .targets array property of the object.

<scope> must be a scope keyword, one of PUBLIC, PRIVATE, or INTERFACE. When <target> is an INTERFACE target, <scope> must be INTERFACE.

{
    "imports": [
        { "name": "a", "file": "a", "targets": ["a::a"] },
        { "name": "b", "file": "b", "targets": ["b::b"], "groups": ["main"] },
        { "name": "c", "file": "c", "targets": ["c::c"], "groups": ["test"] }
    ]
}
def cupcake_link_libraries(target, scope, group):
    metadata = json.parse('cupcake.json')
    for package in metadata.get('imports', []):
        if group in package.get('groups', ['main']):
            name = package['name']
            targets = package.get('targets', [f'{name}::{name}'])
            target_link_libraries(target, scope, targets)

cupcake_add_libraries

⬆️ #️⃣ special

cupcake_add_libraries()

Add targets for all libraries in the project.

For each object in the .libraries array of cupcake.json, cupcake_add_libraries() first calls cupcake_add_library() with the object's .name string property, passing PRIVATE if the object has a .private Boolean property that is true. Then it calls target_link_libraries() for each value in the object's .links array property.

Each link takes one of two forms. If it is a string, then cupcake_add_libraries() calls target_link_libraries() with it as the name of the linked target and PUBLIC as the scope. If it is an object, then cupcake_add_libraries() calls target_link_libraries() with its .target string property as the name of the linked target and its optional .scope string property (default value PUBLIC) as the scope.

{
    "libraries": [
        { "name": "x", "links": ["a::a"] },
        { "name": "y", "links": ["b::b", { "target": "c::c", "scope": "PRIVATE" }
    ]
}
def cupcake_add_libraries():
    metadata = json.parse('cupcake.json')
    for library in metadata.get('libraries', []):
        target = cupcake_add_library(
            library['name'], PRIVATE if library['private'] else None
        )
        for link in library.get('links', []):
            if type(link) == str:
                target_link_libraries(target, PUBLIC, link)
            else:
                target_link_libraries(target, link.get('scope', PUBLIC), link['target'])

cupcake_add_executables

⬆️ #️⃣ special

cupcake_add_executables()

Add targets for all executables in the project.

For each object in the .executables array of cupcake.json, cupcake_add_executables() first calls cupcake_add_executable() with the object's .name string property, passing PRIVATE if the object has a .private Boolean property that is true. Then it calls target_link_libraries() for each value in the object's .links array property.

Each link takes one of two forms. If it is a string, then cupcake_add_executables() calls target_link_libraries() with it as the name of the linked target and PUBLIC as the scope. If it is an object, then cupcake_add_executables() calls target_link_libraries() with its .target string property as the name of the linked target and its optional .scope string property (default value PUBLIC) as the scope.

{
    "executables": [
        { "name": "x", "links": ["a::a"] },
        { "name": "y", "links": ["b::b", { "target": "c::c", "scope": "PRIVATE" }
    ]
}
def cupcake_add_executables():
    metadata = json.parse('cupcake.json')
    for executable in metadata.get('executables', []):
        target = cupcake_add_executable(
            executable['name'], PRIVATE if executable['private'] else None
        )
        for link in executable.get('links', []):
            if type(link) == str:
                target_link_libraries(target, PUBLIC, link)
            else:
                target_link_libraries(target, link.get('scope', PUBLIC), link['target'])

cupcake_add_tests

⬆️ #️⃣ special

cupcake_add_tests()

Add targets for all tests in the project.

For each object in the .tests array of cupcake.json, cupcake_add_tests() first calls cupcake_add_test() with the object's .name string property. Then it calls target_link_libraries() for each value in the object's .links array property.

Each link takes one of two forms. If it is a string, then cupcake_add_tests() calls target_link_libraries() with it as the name of the linked target and PUBLIC as the scope. If it is an object, then cupcake_add_tests() calls target_link_libraries() with its .target string property as the name of the linked target and its optional .scope string property (default value PUBLIC) as the scope.

{
    "tests": [
        { "name": "x", "links": ["a::a"] },
        { "name": "y", "links": ["b::b", { "target": "c::c", "scope": "PRIVATE" }
    ]
}
def cupcake_add_tests():
    metadata = json.parse('cupcake.json')
    for test in metadata.get('tests', []):
        target = cupcake_add_test(test['name'])
        for link in test.get('links', []):
            if type(link) == str:
                target_link_libraries(target, PUBLIC, link)
            else:
                target_link_libraries(target, link.get('scope', PUBLIC), link['target'])

Footnotes

  1. The CMakeDeps generator will not generate a package configuration file for a tool requirement.

  2. If you ever define an inline function in a public header that either (a) has its address taken or (b) defines a static variable, then you will need to make inlined functions visible to ensure that different translation units that see that definition resolve its addresses in the same way.

  3. An environment variable must be used because cmake --build does not forward any command-line arguments.