Finding and using dependencies

Questions

  • How can I use CMake to detect and use the dependencies of my project?

Objectives

  • Learn how to use find_package.

  • Learn what other detection alternatives exist.

The vast majority of software projects do not happen in a vacuum: they will have dependencies on existing frameworks and libraries. Good documentation will instruct your users to ensure that these are satisfied in their programming environment. The build system is the appropriate place to check that these preconditions are met and that your project can be built correctly. In this episode, we will show you few examples of how to detect and use dependencies in your CMake build system.

Finding dependencies

CMake offers a family of commands to find artifacts installed on your system:

  • find_file to retrieve the full path to a file.

  • find_library to find a library, shared or static.

  • find_package to find and load settings from an external project.

  • find_path to find the directory containing a file.

  • find_program to find an executable.

The workhorse of dependency discovery is find_package, which will cover your needs in almost all use cases.

We cannot stress this enough: you should only use the other commands in the find_ family in very special, very narrow circumstances. Why so?

  1. For a large selection of common dependencies, the Find<PackageName>.cmake modules shipped with CMake work flawlessly and are maintained by the CMake developers. This lifts the burden of programming your own dependency detection tricks.

  2. find_package will set up imported targets: targets defined outside your project that you can use with your own targets. The properties on imported targets defines usage requirements for the dependencies. A command such as:

    target_link_libraries(your-target
      PUBLIC
        imported-target
      )
    

    will set compiler flags, definitions, include directories, and link libraries from imported-target to your-target and to all other targets in your project that will use your-target.

These two points simplify enormously the burden of dependency detection and consistent usage within a multi-folder project.

Using find_package

When attempting dependency detection with find_package, you should make sure that:

  • A Find<PackageName>.cmake module exists,

  • Which components, if any, it provides, and

  • What imported targets it will set up.

A complete list of Find<PackageName>.cmake can be found from the command-line interface:

$ cmake --help-module-list | grep "Find"

Exercise 10: Using OpenMP

We want to compile the following OpenMP sample code: [1]

// example adapted from
// http://www.openmp.org/wp-content/uploads/openmp-examples-4.5.0.pdf page 85

#include <cstdlib>
#include <iostream>

void long_running_task(){
  // do something
  std::cout << "long_running_task" << std::endl; 
};

void loop_body(int i, int j){
  // do something
  std::cout << "i = " << i << " j = " << j << std::endl; 
};

void parallel_work() {
  int i, j;
#pragma omp taskgroup
  {
#pragma omp task
    long_running_task(); // can execute concurrently

#pragma omp taskloop private(j) grainsize(500) nogroup
    for (i = 0; i < 100; i++) { // can execute concurrently
      for (j = 0; j < i; j++) {
        loop_body(i, j);
      }
    }
  }
}

int main() {
  parallel_work();
  return EXIT_SUCCESS;
}

Note the usage of the taskloop construct, which was introduced in OpenMP 4.5: we need to make sure our C++ compiler is suitably compatible with at least that version of the standard.

From the documentation of the FindOpenMP.cmake module:

$ cmake --help-module FindOpenMP | less

we find that the module provides the components C, CXX, and Fortran and that OpenMP::OpenMP_CXX target will be provided, if detection is successful. The scaffold project is in content/code/10_taskloop. You will need to find the suitable OpenMP libary and link against the imported target.

We can configure and build verbosely. [2] Notice that compiler flags, include directories, and link libraries are properly resolved by CMake.

You can find the complete working example in the solution subfolder.

Exercise 11: Using MPI

In this exercise, you will attempt compiling a “Hello, world” program that uses the message passing interface (MPI).

  1. Check whether a FindMPI.cmake module exists in the built-in module library.

  2. Get acquainted with its components and the variables and imported targets it defines.

The scaffold project is in content/code/11_mpi-cxx.

  1. Compile the source file to an executable.

  2. Link against the MPI imported target.

  3. Invoke a verbose build and observe how CMake compiles and links.

A working example is in the solution subfolder.

Alternatives: Config scripts and pkg-config

What to do when there is no built-in Find<PackageName>.cmake module for a package you depend on? The package developers might be already prepared to help you out:

  • They ship the CMake-specific file <PackageName>Config.cmake which describes how the imported target should be made for their package. In this case, you need to point CMake to the folder containing the Config file using the special <PackageName>_DIR variable:

    $ cmake -S. -Bbuild -D<PackageName>_DIR=/folder/containing/<PackageName>Config.cmake
    
  • They include a .pc file, which, on Unix-like platforms, can be detected with the pkg-config utility. You can then leverage pkg-config through CMake:

    # find pkg-config
    find_package(PkgConfig REQUIRED)
    # ask pkg-config to find the UUID library and prepare an imported target
    pkg_search_module(UUID REQUIRED uuid IMPORTED_TARGET)
    # use the imported target
    if(TARGET PkgConfig::UUID)
      message(STATUS "Found libuuid")
    endif()
    

    This was the strategy adopted in Probing compilation, linking, and execution when testing the use of the UUID library.

Keypoints

  • CMake has a rich ecosystem of modules for finding software dependencies. They are called Find<package>.cmake.

  • The Find<package>.cmake modules are used through find_package(<package>).

  • You can also use the classic Unix tool pkg-config to find software dependencies, but this is not as robust as the CMake-native Find<package> modules.

Footnotes