Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Recipe

Add a library your project depends on

Use when: your code needs a library from outside the project, such as {fmt} or a JSON parser, and the build should get it by itself, with nothing installed by hand first

Goal: the library's :: target, pinned to one exact version and linked into your own target, building the same way on every machine

Taught in FetchContent, building a dependency from source · a later section extends this recipe with more cases

  1. 1

    Look up the library's home page or repository. Note where its source can be downloaded and the exact release you want.

    Example

    {fmt} lives at https://github.com/fmtlib/fmt.git, and its release 12.2.0 is a tag. Its commit hash is 1be298e1bd68957e4cd352e1f676f00e07dcfb57, if you'd rather pin the commit.

    • it's a Git repository with release tags — you'll use GIT_REPOSITORY with GIT_TAG set to the release tag, plus GIT_SHALLOW TRUE.
    • you'd rather pin a full commit hash — you'll use GIT_REPOSITORY with GIT_TAG set to the hash, and no GIT_SHALLOW. A shallow clone only fetches the tip of a branch or tag, so the commit wouldn't be there.
    • it publishes release archives — you'll use URL with URL_HASH SHA256= and the archive's 64-character fingerprint.
    • the source is already on disk, like the lab's minijson — you'll use SOURCE_DIR with that folder.
    • some machines already have the library installed, through vcpkg, Conan or the system — add FIND_PACKAGE_ARGS (CMake 3.24 or newer) to the declaration, so an installed copy is used and the download is only the fallback.
    • Not yet: it's a big library, such as Boost, Qt or OpenSSL, that would take minutes to rebuild in every fresh build folder — covered in vcpkg in manifest mode

    Why: a tag or a commit names one fixed version of the code. A branch such as master means "whatever is there today," so two people building a month apart would get different code.

  2. 2

    Write include(FetchContent), then one FetchContent_Declare with a name and the source you found in Step 1.

    Example

    include(FetchContent)
    FetchContent_Declare(fmt
        GIT_REPOSITORY https://github.com/fmtlib/fmt.git
        GIT_TAG        12.2.0
        GIT_SHALLOW    TRUE
    )
    

    Why: include is what defines the FetchContent_ commands. Declaring only records where the source comes from. Nothing is downloaded yet.

  3. 3

    Call FetchContent_MakeAvailable with the same name.

    Example

    FetchContent_MakeAvailable(fmt)
    

    On the first configure, this downloads {fmt} into build/_deps/fmt-src/ and runs {fmt}'s own CMakeLists.txt as part of your build.

    Why: running the library's CMakeLists.txt inside your build is what creates its targets, such as fmt::fmt, so you can link them.

  4. 4

    Find the target name the library creates. Its README usually says, or look for the ALIAS line in its CMakeLists.txt. Then link it with target_link_libraries.

    Example

    minijson's CMakeLists.txt has add_library(minijson::minijson ALIAS minijson), so the target is minijson::minijson. The lab's query.h includes minijson.hpp, so:

    target_link_libraries(jsontool_core PUBLIC minijson::minijson)
    

    For {fmt}, used only inside main.cpp:

    target_link_libraries(app PRIVATE fmt::fmt)
    
    • only your .cpp files include the library's headers — link it PRIVATE.
    • one of your own headers includes the library's headers — link it PUBLIC, so anyone using your target gets the library's include folder too.

    Why: linking the target brings its usage requirements, such as the include folder, along with its compiled code. You never write an include path or a library file by hand.

  5. 5

    Configure and build as usual: cmake -S . -B build, then cmake --build build. The download happens during the configure step only.

    Example

    After the first configure, build/_deps/ holds fmt-src/ (the downloaded source) and fmt-build/ (where it's compiled). The compile line for main.cpp now has an include folder inside fmt-src/, which came from linking fmt::fmt.

    • you're online, or the source is a SOURCE_DIR — nothing extra to do.
    • you're offline and build/_deps/ is already full — configure with -DFETCHCONTENT_FULLY_DISCONNECTED=ON, so nothing tries the network.
    • you're offline in a fresh build folder — point the dependency at a copy on disk with -DFETCHCONTENT_SOURCE_DIR_FMT=<folder> (the name in capitals).

    Why: FetchContent only works while CMake configures. cmake --build never downloads anything, so an offline build of an already-configured folder just works.

Common mistakes

  • Pinning with GIT_TAG master, a branch that moves, so builds stop matching each other.
  • Pairing a commit hash with GIT_SHALLOW TRUE. The shallow clone holds only one commit, the tip of a branch or tag, so checking out any other commit fails at configure time.
  • Adding find_package(fmt) as well, after a plain declaration. FetchContent already made the target, and nothing is installed for find_package to find. (A declaration with FIND_PACKAGE_ARGS or OVERRIDE_FIND_PACKAGE makes a later find_package(fmt) succeed, as described under "Installed copy first".)
  • Calling FetchContent_Declare before include(FetchContent).
  • Linking a library PRIVATE when your own public header includes it, so code that uses your target can't find the header.
  • Writing a URL without URL_HASH, so a changed or cut-short download is compiled without anyone noticing.