Using dds Packages in a CMake Project

One of dds’s primary goals is to inter-operate with other build systems cleanly. One of dds’s primary outputs is libman package indices. These package indices can be imported into other build systems that support the libman format.

Note

dds doesn’t (yet) have a ready-made central repository of packages that can be downloaded. You’ll need to populate the local package catalog appropriately.

See also

Refer to The Package Catalog for information about remote packages.

Generating a libman Index

Importing libman packages into a build system requires that we have a libman index generated on the filesystem. This index is not generated globally: It is generated on a per-build basis as part of the build setup. The index will describe in build-system-agnostic terms how to include a set of packages and libraries as part of a build.

dds has first-class support for generating this index. The build-deps subcommand of dds will download and build a set of dependencies, and places an INDEX.lmi file that can be used to import the built results.

Declaring Dependencies

dds build-deps accepts a list of dependencies as commnad line arguments, but it may be useful to specify those requirements in a file.

dds build-deps accepts a JSON5 file describing the dependencies of a project as well. This file is similar to a very stripped-down version of a dds package manifest, and only includes the depends key. (The presence of any other key is an error.)

Here is a simple dependencies file that declares a single requirement:

dependencies.json5
{
    depends: {
        'neo-sqlite3': '^0.2.0',
    }
}

Building Dependencies and the Index

We can invoke dds build-deps and give it the path to this file:

$ dds build-deps --deps dependencies.json5

When finished, dds will write the build results into a subdirectory called _deps and generate a file named INDEX.lmi. This file is ready to be imported into any build system that can understand libman files (in our case, CMake).

Note

The output directory and index filepath can be controlled with the --out and --lmi-path flags, respectively.

Importing into CMake

We’ve generated a libman index and set of packages, and we want to import them into CMake. CMake doesn’t know how to do this natively, but there exists a single-file module for CMake that allows CMake to import libraries from libman indices without any additional work.

The module is not shipped with CMake, but is available online as a single stand-alone file. The libman.cmake file can be downloaded and added to a project directly, or it can be obtained automatically through a CMake tool like PMM (recommended).

Enabling libman Support in CMake via PMM

Refer to the README.md file in the PMM repo for information on how to get PMM into your CMake project. In short, download and place the pmm.cmake file in your repository, and include() the file near the top of your CMakeLists.txt:

include(pmm.cmake)

Once it has been included, you can call the pmm() function. To obtain libman, we need to start by enabling CMakeCM:

pmm(CMakeCM ROLLING)

Warning

It is not recommended to use the ROLLING mode, but it is the easiest to use when getting started. For reproducible and reliable builds, you should pin your CMakeCM version using the FROM <url> argument.

Enabling CMakeCM will make available all of the CMake modules available in the CMakeCM repository, which includes libman.cmake.

After the call to pmm(), simply include() the libman module:

include(libman)

That’s it! The only function from the module that we will care about for now is the import_packages() function.

Importing Our Dependencies’ Packages

To import a package from a libman tree, we need only know the name of the package we wish to import. In our example case above, we depend on neo-sqlite3, so we simply call the libman-CMake function import_packages() with that package name:

import_packages("neo-sqlite3")

You’ll note that we don’t request any particular version of the package: All versioning resolution is handled by dds. You’ll also note that we don’t need to specify our transitive dependencies: This is handled by the libman index that was generated by dds: It will automatically import_packages() any of the transitive dependencies required.

Using Out Dependencies’ Libraries

Like with dds, CMake wants us to explicitly declare how our build targets use other libraries. When we import a package from a libman index, the import will generate CMake IMPORTED targets that can be linked against.

In dds and in libman, a library is identified by a combination of namespace and name, joined together with a slash / character. This qualified name of a library is decided by the original package author, and should be documented. In the case of neo-sqlite3, the only target is neo/sqlite3.

When the libman CMake module imports a library, it creates a qualified name using a double-colon “::” instead of a slash. As such, our neo/sqlite3 is imported in CMake as neo::sqlite3. We can link against it as we would with any other target:

add_executable(my-application app.cpp)
target_link_libraries(my-application PRIVATE neo::sqlite3)

Altogether, here is the final CMake file:

CMakeLists.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
cmake_minimum_required(VERSION 3.15)
project(MyApplication VERSION 1.0.0)

include(pmm.cmake)
pmm(CMakeCM ROLLING)

include(libman)
import_packages("neo-sqlite3")

add_executable(my-application app.cpp)
target_link_libraries(my-application PRIVATE neo::sqlite3)

Additional PMM Support

The pmm() function also supports dds directly, similar to CMakeCM mode. This will automatically download a prebuilt dds for the host platform and invoke dds build-deps in a single pass as part of CMake’s configure process. This is especially useful for a CI environment where you want to have a stable dds version and always have your dependencies obtained just-in-time.

To start, pass the DDS argument to pmm() to use it:

pmm(DDS)
..note::

The _deps directory and INDEX.lmi file will be placed in the CMake build directory, out of the way of the rest of the project.

Note

The version of dds that PMM downloads depends on the version of PMM that is in use.

This alone won’t do anything useful, because you’ll need to tell it what dependencies we want to install:

pmm(DDS DEP_FILES dependencies.json5)

You can also list your dependencies as an inline string in your CMakeLists.txt instead of a separate file:

pmm(DDS DEPENDS "neo-sqlite3 ^0.2.2")

Since you’ll probably want to be using libman.cmake at the same time, the calls for CMakeCM and DDS can simply be combined. This is how our new CMake project might look:

CMakeLists.txt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
cmake_minimum_required(VERSION 3.15)
project(MyApplication VERSION 1.0.0)

include(pmm.cmake)
pmm(CMakeCM ROLLING
    DDS DEPENDS "neo-sqlite3 ^0.2.2"
    )

include(libman)
import_packages("neo-sqlite3")

add_executable(my-application app.cpp)
target_link_libraries(my-application PRIVATE neo::sqlite3)

This removes the requirement that we write a separate dependencies file, and we no longer need to invoke dds build-deps externally, as it is all handled by pmm.