Overview

Design

The modules’ interface are designed to be functional and clear. Each function is designed to handle a specific operation, and do so while producing reproducible results in any invocation; few side-effects.

These modules don’t introduce alternative CMake idioms nor do they inhibit using the CMake language in conjunction with these modules. Instead, these provide reusable CMake functions helping users follow best practices, and produce correct, usable CMake based projects.

The largest influences include Daniel Daniel Pfeifer’s Effective CMake, Crascit’s Professional CMake, and KitWare’s CMake Documentation.

Many opinionated aspects of projects are enforced. These include target names, project structure, file naming, and more. Trying to conform to existing projects or alternative designs is not the goal.

Project Structures

These modules enforce the Canonical Project Structure.

Certain modules may be used on projects following a different project structure, but this is not guaranteed.

Using Dependencies

The provider of dependencies are not defined in CMake, nor do these modules try to define this. This keeps the project agnostic to C++ package managers, system package managers, etc.

It’s recommended that all external requirements are to be found with find_package() and linked with target_link_libraries(), but these modules do not prohibit adding dependencies as subprojects, and jgd-cmake-modules supports its consumptions as a subproject.

Acquiring dependencies through find_package() is recommended because it specifies the usage requirements of a dependency, and not a series of steps to provide the dependency from a specific source. This provides maximum flexibility for future users to provide the dependency in whatever manner is most suitable for them, like a package manager. Furthermore, since only usage requirements are specified, it’s seamless to opt into a direct means of providing the dependency such as FetchContent or add_subdirectory(), to fulfill those needs. Dependency Providers is a direct mechanism for this. Trying to do the reverse is often more convoluted, even considering FetchContent’s find_package integration.

Components

“Component” is a generic term in CMake for project subsets. The term is further refined within jgd-cmake-modules.

Components are optional subsets of a project. In the vast majority of cases, these components represent libraries (one library per component). As an example, a project, libviewer, may offer components core, gtk, and qt, providing the libraries libviewer-core, libviewer-gtk, and libviewer-qt, respectively.

However, a component may rarely represent an executable. For example, if a project produces multiple executables that operate together, such as a CLI and a daemon, these may be offered as components of the same project.

JCM introduces a target property, COMPONENT, which the commands jcm_add_executable() and jcm_add_library() set to the value provided to their respective COMPONENT arguments. CMake extensively uses COMPONENT in function arguments - this property merely stores the value on the target to be queried throughout JCM.

CMake components refer to what’s described in the Canonical Project Structure as a “family of libraries”. Discussions with its author, Boris Kolpackov, have clarified how the project structure extends to executable components - the results of which are enforced by jgd-cmake-modules.

Prefer separating services, libraries, executables, etc. into individual projects instead of adding them as components. Use components where appropriate to avoid bloating projects and complicating their usage.

Target Names

Target names are automatically generated by this library. This is to:

  1. Ensure consistency between targets across projects

  2. Follow target naming best practices.

    • Since target names within a CMake build must be unique, internal target names should be prefixed by the project name to limit naming conflicts. If a project created with these modules were to consume another library, or be consumed by another project, the prefixed target names ensures uniqueness.

    • exported target names should include :: . Since this is invalid to have in a file path, when linking libraries with target_link_libraries(), CMake is limited to only use CMake target names instead of finding the associated binary on disk, or some other binary with the same name. When “linked”, CMake targets propagate properties in addition to simply linking against a binary, so it’s important that targets are used.

  3. Allow the exported naming scheme to indicate what the target is and where it came from.

    • libcandy::libcandy -> A library from a project, libcandy. This project’s purpose is to provide a library ( lib candy::), and this is the central library target.

    • cool-shell::cool-shell -> An executable from a project, cool-shell. This project’s purpose is to provide an executable ( lib cool-shell:: ), and this is the central executable target.

    • libcandy::extra -> the extra component of project libcandy, whose main purpose is to provide a library ( lib candy:: ). This is a library target, which provides the “extra” features for libcandy’s extra component. As such, this project offers multiple libraries.

    • cool-shell::server -> the server component of project cool-shell, whose main purpose is to provide an executable ( lib cool-shell:: ). This is an executable target, which provides the “server” feature for cool-shell’s server component. As such, this project offers multiple executables.

    • libprotobuf::protoc -> an executable, protoc, from a project, libprotobuf, that’s large enough to mainly provide a library (lib protobuf::protobuf), while also providing an executable. Here, the executable is distributed with the library project instead of as a separate project because the executable is required to use the library.

Exported target naming follows, and is generated by jcm_library_naming() and jcm_executable_naming(). Functions, like jcm_add_library(), provide the generated target name through the argument OUT_TARGET.

Notice how the full project name always prefixes the target, where only the part following :: changes.

Project Name

lib<name>

<name>

Created

Target

Library

lib<name>::lib<name>

<name>::lib<name>

Library Component

lib<name>::<component>

<name>::lib<name>-<component>

Executable

lib<name>::<name>

<name>::<name>

Executable Component

lib<name>::<name>-<component>

<name>::<component>

Examples

  1. In the project libcandy (name is candy in the above table), a main library is created, without any COMPONENT argument. The exported target name will be libcandy::libcandy.

  2. In the project candy (name is candy in the above table), a component executable is created with the component wrap. The exported target name will be candy::wrap.