DependencyManager

Overview

This module facilitates a super-build model for structuring a project as part of a software ecosystem. It manages a tree of dependencies with possible duplicates and version clashes.

Declaring Dependency

DependencyManager_Declare
DependencyManager_Declare(<name> <gitRepository>
                          [VERSION_RANGE <versionRange>]
                          [PARENT_NAME <parentName>]
                          [<contentOptions>...])

The DependencyManager_Declare() function is a wrapper over FetchContent_Declare() with specialised functionality

  1. Source code is downloaded into ${DEPENDENCYMANAGER_BASE_DIR}/<name>

  2. STAMP_DIR is in source, by default at ${DEPENDENCYMANAGER_BASE_DIR}/.cmake_stamp_dir

  3. Only Git repositories are supported

  4. Commit hash of dependency must be stored in a file ${name}_SHA1 in directory from which DependencyManager_Declare() is called

The cached variable DEPENDENCYMANAGER_BASE_DIR is the top level location where source is cloned. It is set to ${CMAKE_SOURCE_DIR}/dependencies by default and should not be modified in the middle of the configuration process.

The content <name> must be supported by FetchContent_Declare(). For version checking <name> must be the name given to top level call of project() in the dependencies CMakeLists.txt.

<gitRepository> must be a valid GIT_REPOSITORY as understood by ExternalProject_Add.

The <contentOptions> can be any of the GIT download or update/patch options that the ExternalProject_Add command understands, except for GIT_TAG and GIT_REPOSITORY which are specified separately.

The value of GIT_TAG passed to FetchContent must be a commit hash stored in ${DEPENDENCIES_DIR}/<name>_SHA1 file.

<versionRange> is a list of compatible versions using comma , as a separator (NOT semicolon ;). Version must be specified as <major>.[<minor>[.<patch>[.<tweak>]]] and only specified elements are compared, i.e. 1.2.3 = 1.2 is TRUE where 1.2 is the version range. It can be preceded by relational operators <, <=, >, >= to specify boundaries of the range. If no relational operators are given that an exact match is requested. For example, VERSION_RANGE ">=1.2.3,<1.8" means from version 1.2.3 up to but not including version 1.8.

Name of the parent node, <parentName>, is needed to construct the dependency tree. By default it is the name of the most recently called project(), i.e. ${PROJECT_NAME}. In case there are multiple project() calls parent name can be specified explicitly with option PARENT_NAME.

Populating Dependency

DependencyManager_Populate
DependencyManager_Populate(<name>
                           [PARENT_NAME <parentName>]
                           [DO_NOT_MAKE_AVAILABLE]
                           [NO_VERSION_ERROR])

This is a again a wrapper over FetchContent_Populate(). Dependency being populated must have been declared sometime before.

<name> must be the same as in previous call to DependencyManager_Declare().

<parentName> is the name of the parent node. By default, it is the last called project(). It must be the same value as in previous call to DependencyManager_Declare(). Even if PARENT_NAME was not specified during declaration, the default values might differ if a different project() call was made at the same scope.

After populating the content add_subdirectory() is called by default, unless DO_NOT_MAKE_AVAILABLE is set.

During population stage . When there are duplicate dependencies <versionRange> is checked and if an already populated dependency is outside that range an error is raised during configuration.

If subdirectory gets added, a version check is performed. Version of a dependency is read from the ${<name>_VERSION} variable which is automatically set when VERSION is specified in the project() call. If cloned version is not compatible with VERSION_RANGE specified in DependencyManager_Populate() than an error gets raised and build configuration stops. With option NO_VERSION_ERROR only a warning is printed and configuration continues. ${name}_VERSION is also brought up to PARENT_SCOPE.

If cached option DEPENDENCYMANAGER_VERSION_ERROR is set to OFF, then an error is not raised when version clash is found. Note, that NO_VERSION_ERROR takes precedence.

If option DEPENDENCYMANAGER_FETCHCONTENT is set, then everything is implemented using standard FetchContent, instead of dependencies being brought in to ${DEPENDENCIES_DIR}

Note, that file locking is used which acts as a mutex when multiple configurations are run simultaneously. The file lock files are stored in STAMP_DIR.

Update of Commit Hash

Every time CMake configuration is rerun an update step is initiated which uses commit hash from <name>_SHA1 file, checking out the correct version if for some reason a dependency is at a different commit. Only advanced users with good knowledge of software stack should modify the <name>_SHA1 file. This applies to developers who in this paradigm need to be able to modify the source code of dependencies and/or checkout a different commit and successfully configure the build. Setting cache variable DEPENDENCYMANAGER_HASH_UPDATE to ON will overwrite <name>_SHA1 file with the currently checked out hash before the update stage, making sure that the work is preserved.

Graph of Dependency Tree

DependencyManager_DotGraph
DependencyManager_DotGraph([NAME <name>])

Writes a dot file with the current structure of dependency tree. It can be compiled to a graphics using graphviz. By default, the dot file is written to ${CMAKE_CURRENT_BINARY_DIR}/dependencyManager_dotGraph.dot. This can be changed by passing <name>, which can be an absolute path or a path relative to ${CMAKE_CURRENT_BINARY_DIR}

[For Developers]

Structure of the Dependency Tree

To correctly resolve valid versions and provide useful summaries we need to store the structure of the dependency tree.

Each node has a unique nodeID, represented with a dot separated list of integers i1.i2.i3.i4. ..., where i1 is the position of root project (there might be multiple roots at top level), i2 is the position among children of i1 at level 2, etc. For example A->{B->{C}, C->{E}, D->{E}} becomes 1->{1.1->{1.1.1}, 1.2->{1.2.1}, 1.3->{1.3.1}}

A node contains the following features:

  1. NAME is name of the project on declaration

  2. PARENT_NAME is name of the parent project on declaration

  3. CHILDREN is an ordered set of nodeID’s for its children

  4. GIT_REPOSITORY is the Git url to repository

  5. GIT_TAG is the commit hash stored in relevant <name>_SHA1 file

  6. VERSION_RANGE is the range of required versions

During declaration stage we register each node and add it as a child of a parent node. If a child node by that name already exists, than its content is overwritten and a warning about duplicate node gets printed.

By design, nodes that are made available form a unique set. We call them parent nodes, as they are the only ones that can declare more dependencies as children. We track parent nodes and store the following features:

  1. NAME

  2. NODE_ID

  3. VERSION is the actual version of the project

This is the complete definition of dependency tree . It is used to check the version and generate its graphical representation.

Global Properties:

  1. __DependencyManager_property_nodeFeatures_${nodeID} – store node features, one for each node in the full tree

  2. __DependencyManager_property_nodeList – keeps track of nodes as they are created by storing a list of names and node IDs

    in multi-value-arguements NAME and NODE_ID respectively.

  3. __DependencyManager_property_parentNodes – store extra features of parent nodes in multi-value-arguments:

    NAME - list of parent names; NODE_ID - list of corresponding nodeIDs; VERSION - list of corresponding versions

Verbose Output

Passing --log-level=debug -D DEPENDENCYMANAGER_VERBOSE=ON to command-line, turns on extra printouts. This is useful for debugging only.

Documentation of Utility Functions

__DependencyManager_hasDuplicates(<list> <out>)

If <list> contains duplicates, sets variable called <out> to TRUE, otherwise to FALSE.

__DependencyManager_updateParentNodes(<name> <nodeID> <version>)

Register a node as a parent by adding its parent node features to the global property.

__DependencyManager_getParentNodes(<prefix>)

Makes full content of parent nodes property available at parent scope via lists ${<prefix>}_NAME, ${<prefix>}_NODE_ID, ${<prefix>}_VERSION.

__DependencyManager_getParentNodeInfo(<prefix> <name>)

Makes parent node features of a parent <name> available at parent scope via variables ${<prefix>}_NODE_ID, ${<prefix>}_VERSION.

__DependencyManager_getNodeFeatures(<prefix> <nodeID>)

Makes node features of a node <name> available at parent scope via variables ${prefix}_name, ${prefix}_parentName, ${prefix}_gitRepository, ${prefix}_gitTag, ${prefix}_versionRange, ${prefix}_children. If <nodeID> is 1, than this is a root node and it gets created empty on the first call

__DependencyManager_addChild(<parentName> <child_nodeID>)

Appends a child to a parent node.

__DependencyManager_currentNodeID(<prefix> <name> <parentName>)

Deduces the current nodeID by looking at the children of the parent node. Sets the following variables at parent scope:

  1. ${prefix}_duplicate to TRUE if there is already a node under that name among children;

  2. ${prefix}_nodeID to deduced current node ID. If the node is a duplicate uses nodeID of the relevant child.

__DependencyManager_updateNodeList(<name> <nodeID>)

Updates global list of nodes by storing <name> and <nodeID>.

__DependencyManager_getNodeList(<name> <nodeID>)

Updates global list of nodes by storing <name> and <nodeID>. Makes node list available at parent scope via variables ${<prefix>}_NAME, ${<prefix>}_NODE_ID.

__DependencyManager_saveNode(<name> <parentName> <gitRepository> <gitTag> <versionRange>)

Store node features. If the node is a duplicate, overwrite content of the registered node. Otherwise, create a new node property and add itself as a child of the parent node.

__DependencyManager_update_SHA1(<name>)

If DEPENDENCYMANAGER_HASH_UPDATE is ON, than updates <name>_SHA1 of a cloned dependency. The repository and SHA1 file must be in current directory.

__DependencyManager_VersionCompare(<version1> <comp> <version2> <out>)

Compares <version1> and <version2>. <comp> can be one of "", =, <, <=, >, >=. Empty is equivalent to =. Comparison operators are mapped to VERSION_<COMPARISON> options in if() statements. If <version2> has less digits than <version1>, truncate <version1> so they are same length. Result is set to variable <out> in parent scope.

__DependencyManager_VersionCheck(<versionRange> <version> <out>)

Checks that <version> is within <versionRange>. If it is within, then store TRUE in variable <out>, otherwise store FALSE.

__DependencyManager_getProjectName(<path> <out>)

Determines the name of the first encountered project() call in the file pointed to by the provided <path> and writes the determined name into <out> (in the parent scope). If the parsing has failed or the given file does not contain a project() invocation, then <out> is set to an empty string.