Project Modularization

Project modularization is a technique that aims to maximize code reusability, allowing components to be split up as independent modules that can be shared with other projects, while only building and packaging the components that are really needed.

Top Level Project

A top level project is a project that is split into separate independent subprojects, and each of those subprojects are referred to as modules. A top level project will often have no source files of its own, simply serving as a lightweight container for its modules.

Project Module

A (project) module is a completely independent BASIS project with its own dependencies that resides in the modules/ directory of a top-level project. Each module will often reside in a separate repository that is designed to be shared with other projects.

Because modules are usually developed by the same development team, name conflicts are uncommon and can be avoided by appropriate naming conventions. Therefore, all modules share a common namespace, namely the one of the top-level project.

For example, if BASIS_USE_TARGET_UIDS is enabled in config/Settings.cmake of the top-level project, the actual build target names of the top-level project and its modules are of the form <toplevel>.<target>, where <toplevel> is the package name of the top-level project which usually is the same as the name of the top-level project, and <target> is the target name argument of basis_add_executable() or basis_add_library(). Note that if BASIS_USE_FULLY_QUALIFIED_TARGET_UIDS is disabled (the default), the <toplevel> part is only used for the export of the target.

The basis_project() call of a module must use the NAME parameter to set the name of the module (instead of SUBPROJECT).

Subproject

A subproject is very similar to a project module with a few important differences. While project modules are lightweight subprojects which are tightly integrated into the top-level project, subprojects are more self-sustained and should be treated as separate smaller projects. The top-level project serves as meta-project to group multiple subprojects. A use case would be to bundle several more or less independent software projects in a single package. The top-level project can be thus be seen as collection of related software packages, which may or may not depend on each other.

Because subprojects are usually developed by different development teams, name conflicts are more likely to occur. Therefore, each subproject has its own (nested) namespace inside the namespace of the package it belongs to, whereas the symbols of modules have no own namespace, but are directly defined within the namespace of the top-level project.

For example, if BASIS_USE_TARGET_UIDS is enabled in config/Settings.cmake of the top-level project, the actual build target names are of the form <package>.<subproject>.<target>, where <package> is the package name of the subproject which corresponds to the package name of the top-level project if not specified, and <target> is the target name argument of basis_add_executable() or basis_add_library(). Note that if BASIS_USE_FULLY_QUALIFIED_TARGET_UIDS is disabled (the default), the <package> part is only used for the export of the target.

Other differences are that BASIS will install separate uninstaller scripts for each subproject and also register each subproject installation if -DBASIS_REGISTER is enabled. Therefore, a subproject which is installed by one package can be used directly by other packages as if the subproject was installed separate from the other subprojects and modules of the top-level project.

The basis_project() call of a subproject must use the SUBPROJECT parameter to set the name of the subproject (instead of NAME). Additionally, as subprojects are likely shared by multiple top-level projects, it is recommended to set the PACKAGE_NAME (short PACKAGE) to the name of the package which this subproject belongs to primarily. Note that this package need not actually exist. By providing this package name, the namespace of the subproject will always be the same no matter what the name of the top-level project is.

Note

It should be noted that the concept of a namespace can be extended to all aspects of a software project, not only symbols of programming languages which have it built in such as C++. Therefore, the symbols which belong to the package namespace include project modules, target names, C++ classes and functions, as well as scripted libraries.

See also

See Modularize a Project for usage instructions and Project Template for a reference implementation.

Filesystem Layout

By default each module is placed in its own modules/<module_name> subdirectory, but this can be configured in config/Settings.cmake by modifying the PROJECT_MODULES_DIR variable. More details can be found in the Filesystem Layout.

The Top Level project often excludes the src/ subdirectory, and instead includes the modules/ directory where the project’s modules reside.

Dependency Requirements

There are several features and limitations when one top level or subproject uses code from another.

  • Modules may depend on each other.
  • Each module of a top level project may depend on other modules of the same project, or external projects and packages.
  • Only one level of submodules are allowed in a top level project
  • An external project can also be another top-level project with its own modules.

Module CMake Variables

CMake variables available to any project utilizing BASIS. These options can be modified with the ccmake command. CMake Options describes other important CMake options.

CMake Variable Description
MODULE_<module> Builds the module named <module> when set to ON and excludes it when OFF. It is automatically set to ON if it is required by another module that is ON.
BUILD_MODULES_BY_DEFAULT Sets the default state of each MODULE_<module> switch. ON by default.
BUILD_ALL_MODULES Global switch enabling the build of all modules. Overrides all MODULE_<module> variables.
PROJECT_IS_MODULE Specifies if the current project is a module of another project.

It is recommended that customized defaults for these variables be set in config/Settings.cmake.

Implementation

The modularization is mainly implemented with the following hierarchy presented in the same manner as a stack trace with the top function being the last function called:

The script then takes the following steps:

  1. The basis_project_modules() function searches the subdirectories in the modules/ directory for the presence of the BasisProject.cmake file.

  2. BasisProject.cmake is then loaded to retrieve the meta-data of each module such as its name and dependencies.

  3. A MODULE_<module> option is added to the build configuration for each module and module dependencies are defined that correspond to the settings in BasisProject.cmake. This enables the eventual execution of the build step to be in the correct topological order. The MODULE_<module> settings obey the following constraints:

    • When OFF the module is excluded from both the project build and any package generated by CPack.
    • When ON the module builds as part of the top-level project.
    • If one module requires another, the required module will automatically be set to ON.
    • All MODULE_<module> options are superceded by the BUILD_ALL_MODULES when it is set to ON.

Besides adding these options, the basis_project_modules() function ensures that the modules are configured with the right dependencies so that the generated build files will compile them correctly.

It also helps the basis_find_package() function find the other modules’ package configuration files, which are either generated from the default Config.cmake.in file or a corresponding file found in the config/ directory of each module.

The other BASIS CMake functions may also change their actual behaviour depending on the PROJECT_IS_MODULE variable, which specifies whether the project that is currently being configured is a module of another project (i.e., PROJECT_IS_MODULE is TRUE) or a top-level project (i.e., PROJECT_IS_MODULE is FALSE).

Origin

The modularization concepts and part of the CMake implementation are from the ITK 4 project. See the Wiki of this project for details on the ITK 4 Modularization.

Reuse

Modules can be built standalone without a Top Level Project.

This is why the BasisProject.cmake meta-data requires an explicit PACKAGE_NAME. When you configure the build system of a project module directly, i.e., by using the module’s subdirectory as root of the source tree, it will still build as if it was part of a Top Level Project with name equal to the PACKAGE_NAME of the project.

The explicit package name is also important for the executable (target) referencing that is used for subprocess invocations covered in Calling Conventions. A developer can use the target name (e.g., basis.basisproject) in the BASIS utility functions for executing a subprocess, and the path to the actually installed binary is resolved by BASIS. This allows the developer of the respective module to change the location/name of a binary file through the CMake configuration and other code which uses this module’s executable can still call it by its unchanged build target name. As the target name includes the package name of a project to avoid name conflicts among packages, the package name which a module belongs to must be known even if the module is build independently without any Top Level Project.

Superbuild

Note

The superbuild of project modules is yet experimental and not fully documented!

CMake’s ExternalProject module is sometimes used to create a superbuild, where components of a software or its external dependencies are compiled separately. This has already been done with several projects.

An experimental superbuild of project modules is implemented by the basis_add_module() function. It is disabled by default, i.e. each module is configured right away using add_subdirectory. The -DBASIS_SUPERBUILD_MODULES option can be used to enable the superbuild of modules. This can dramatically speed up the build system configuration for projects which contain a large number of modules, because the configuration of each module is deferred until the build step. Moreover, only modules which were modified since the last build will be reconfigured when the top-level project is re-build. Without the superbuild approach, the entire build system of the top-level project needs to be reconfigured in such case.

If the superbuild of modules should always be enabled, add the following CMake code to config/Settings.cmake:

if (NOT BASIS_SUPERBUILD_MODULES)
  set (
    BASIS_SUPERBUILD_MODULES ON CACHE BOOLEAN
      "This project always builds the modules using a superbuild approach."
    FORCE
  )
  message (WARNING "Option BASIS_SUPERBUILD_MODULES set to ON as this project"
                   " always builds its modules using a superbuild approach."
                   " The BASIS_SUPERBUILD_MODULES option cannot be changed.")
endif ()

Alternatively, the following line would be sufficient as well without feedback for the user:

set (BASIS_SUPERBUILD_MODULES OFF)

See also

A superbuild can also take care of building BASIS itself if it is not installed on the system, as well as any other external library that is specified as dependency of the project. See the Superbuild of BASIS and other dependencies.