Grab is a CMake module that makes it easy to include and manage external libraries in your project. Each project pins the versions it builds against in its own library database.

Libraries are only the start: anything that lives in a git repository, such as data and the tools that process it, can be grabbed too. A project can then describe a whole versioned environment, and CMake how its parts work together (see Integrating components and Describing a pipeline).

Grab ships with a reference catalog of common libraries. Organizations adapt it in their own fork to list the libraries they approve.


Usage

Organization setup

Fork the Grab repository and update libs.db.csv to match your organization’s needs. This is the reference catalog: projects start from it, but pin their own versions.

Project setup

Copy Grab from your organization’s fork into the project and commit it:

shell
mkdir your-project
cd your-project
git init

git clone --depth 1 --branch v0.1 https://code-hub.mooo.com/gpuoti/grab.git
rm -rf grab/.git
touch CMakeLists.txt

Keep in grab/libs.db.csv the libraries the project uses, at the versions the release builds against, then include Grab in your CMake configuration:

CMakeLists.txt
cmake_minimum_required( VERSION 3.15 )

include( grab/grab.cmake )

project( hello CXX )

grab( spdlog )
grab( cxxopts )
grab( catch2 )

First use

Every project keeps its own library database next to grab.cmake, in the grab/ directory, under version control with the project:

your-project/
├── CMakeLists.txt
└── grab/
    ├── grab.cmake
    └── libs.db.csv

Each line of libs.db.csv describes one library as <name> | <tag> | <url>. Blank lines and lines starting with # are ignored. Use master or your branch name if a library has no tag yet. This is the database of the examples/hello project:

grab/libs.db.csv
spdlog  | v1.17.0 | https://github.com/gabime/spdlog
cxxopts | v3.3.1  | https://github.com/jarro2783/cxxopts
catch2  | v3.15.0 | https://github.com/catchorg/Catch2
Note
The format of libs.db.csv will change. It is not stable in version 0.1, but we expect it to stay compatible or convertible.

Only the libraries listed there can be grabbed. If the database is missing, CMake stops at the first grab() call and tells you where it looked for it:

cmake output
CMake Error at grab/grab.cmake:37 (message):
  Library database
  /path/to/your-project/grab/libs.db.csv
  does not exist.

  The database belongs to your project: create it in that directory (its name
  comes from GRAB_LIBS_DB, `libs` by default).  Each line must have the
  following structure:

    <name> | <tag> | <url>

Create the file with the libraries you need and configure again. The same happens if you select a database with GRAB_LIBS_DB (see Switching library sets) and its file is not there yet.

Integrating components

A project has a single Grab and a single library database. They describe the build environment, so they live in a meta component, which also holds the top-level CMakeLists.txt. The components leave Grab out:

integration/
├── meta/
│   ├── CMakeLists.txt
│   └── grab/
│       ├── grab.cmake
│       └── libs.db.csv
└── component/
    ├── CMakeLists.txt
    └── src/
meta/CMakeLists.txt
cmake_minimum_required( VERSION 3.15 )
project( integration NONE )

include( grab/grab.cmake )

add_subdirectory( ../component component )
component/CMakeLists.txt
add_library( component INTERFACE )
target_include_directories( component INTERFACE src )

Configure from meta, with cmake -S meta. In a real project the component comes from its own repository, through an entry in libs.db.csv.

A component developed on its own has a meta of its own, used while developing it. When the component is integrated, the meta of the integrating project takes its place and the component’s grab/ is not brought along.

Including Grab a second time is an error, because the project would have two databases:

cmake output
CMake Error at component/grab/grab.cmake:8 (message):
  Grab is already loaded from /path/to/integration/meta/grab, it cannot be
  included again from /path/to/integration/component/grab.

  The grab/ directory in /path/to/integration/component/grab describes the
  development environment of a component, not the integrated one. Keep grab/
  and its database only in the project that describes the build environment,
  and let the components leave it out.

Describing a pipeline

Grab fetches whatever lives in a git repository, so an environment can bundle more than libraries: versioned data, and the tools that process it. A component that is not code still has a CMakeLists.txt: it only tells CMake where its content is.

data/CMakeLists.txt
add_library( data INTERFACE )
set_target_properties( data PROPERTIES
  DATA_DIR   ${CMAKE_CURRENT_LIST_DIR}/files
  DATA_FILES ${CMAKE_CURRENT_LIST_DIR}/files/input.txt )
tool/CMakeLists.txt
add_library( tool INTERFACE )
set_target_properties( tool PROPERTIES
  TOOL_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/tool.cmake )

meta grabs them at the versions of its database, and CMake describes the pipeline: which command runs on which data. The urls below are placeholders: to try the example, point them at your own repositories.

meta/grab/libs.db.csv
# The versions of the environment: what the pipeline runs on.
# The urls are placeholders: point them at your own repositories.
data | v1 | https://example.com/pipeline-data.git
tool | v1 | https://example.com/pipeline-tool.git
meta/CMakeLists.txt
cmake_minimum_required( VERSION 3.15 )
project( pipeline NONE )

include( grab/grab.cmake )

grab( data )
grab( tool )

add_custom_command(
  OUTPUT  result.txt
  COMMAND ${CMAKE_COMMAND}
            -DIN=$<TARGET_PROPERTY:data,DATA_FILES>
            -DOUT=${CMAKE_CURRENT_BINARY_DIR}/result.txt
            -P $<TARGET_PROPERTY:tool,TOOL_SCRIPT>
  DEPENDS $<TARGET_PROPERTY:data,DATA_FILES>
          $<TARGET_PROPERTY:tool,TOOL_SCRIPT> )

add_custom_target( pipeline ALL DEPENDS result.txt )

Run it with cmake --build <build> --target pipeline. Listing the files in DEPENDS makes CMake rerun the step whenever the data or the tool change, that is, whenever their tag changes in the database.

Data and tools are expected to be in git repositories, even if that is not the best way to publish them: Grab does not download archives or binaries yet.


How it works

Grab uses CMake’s built-in FetchContent module to download and integrate the requested libraries. Each dependency must support CMake-based builds.


Customization

Dependency location

By default, resolved dependencies are placed in the _deps directory inside your build directory (managed by FetchContent). You can override this by setting GRAB_BINARY_DIR before including Grab.

The following example places dependencies in the source tree instead:

CMakeLists.txt
cmake_minimum_required( VERSION 3.15 )

project( your-project )

set( GRAB_BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps )
include( grab/grab.cmake )

grab( plog )
grab( cxxopts )
Important
This is just a placeholder example. Placing dependencies in the source tree is generally not recommended.

Switching library sets

Organizations working across projects from different eras may need to maintain multiple library databases to handle compatibility requirements. Grab supports this through the GRAB_LIBS_DB variable (default: libs).

For example, suppose fancy-lib has two flavors:

  • latest — defined in libs.db.csv (the default)

  • legacy — defined in a separate legacy.db.csv file

Projects that require the legacy flavor can opt in by setting GRAB_LIBS_DB before including Grab:

CMakeLists.txt
cmake_minimum_required( VERSION 3.15 )

project( your-project )

set( GRAB_LIBS_DB legacy )
include( grab/grab.cmake )

grab( plog )
grab( cxxopts )

Both database files sit next to grab.cmake, in the grab/ directory. Only the selected one is loaded.


License

This project is licensed under the MIT License. See the LICENSE file for details.