5.3.8 · D4Build Systems & Toolchain

Exercises — CMake — CMakeLists.txt, add_executable, add_library, target_link_libraries

2,288 words10 min readBack to topic

A quick reminder of the three words the whole page turns on — read them once so no symbol is unearned:

The picture below is the mental model for every L3–L5 question: a graph of targets with arrows labelled by keyword.

Figure — CMake — CMakeLists.txt, add_executable, add_library, target_link_libraries

Level 1 — Recognition

Exercise 1.1. Which command produces a runnable program, and which produces a linkable library?

Recall Solution
  • add_executable(name sources...) → a program you can run (calc, calc.exe).
  • add_library(name [STATIC|SHARED|INTERFACE] sources...) → code other targets link against.

Why: an executable has an entry point (main) and is the end of a link chain; a library is a reusable unit that something else pulls in.

Exercise 1.2. In target_link_libraries(calc PRIVATE mathlib), name the four pieces and what each is.

Recall Solution
  • target_link_libraries — the command that declares "one target depends on another".
  • calc — the consumer target (does the linking).
  • PRIVATE — the propagation keyword (who inherits the dependency past calc).
  • mathlib — the dependency being linked in.

Exercise 1.3. True or false: cmake_minimum_required(VERSION 3.16) may appear anywhere in the file.

Recall Solution

False. It must be the first command, before project(). It fixes CMake's policy behaviour so the build is reproducible; placing it later triggers a hard error/warning.


Level 2 — Application

Exercise 2.1. Write the smallest valid CMakeLists.txt that builds one program game from main.cpp, C++ only.

Recall Solution
cmake_minimum_required(VERSION 3.16)
project(Game LANGUAGES CXX)
add_executable(game main.cpp)

Three lines, in this order: version pin → project (sets up the C++ compiler & ${PROJECT_SOURCE_DIR}) → target. Anything less either errors (no version) or warns (no project).

Exercise 2.2. You have physics/vec.cpp + physics/vec.h (the header is part of the public API) and sim/main.cpp. Write a CMakeLists.txt so sim can #include "vec.h" without its own include line.

Recall Solution
cmake_minimum_required(VERSION 3.16)
project(Sim LANGUAGES CXX)
 
add_library(physics STATIC physics/vec.cpp)
target_include_directories(physics PUBLIC physics)   # PUBLIC = users see it too
 
add_executable(sim sim/main.cpp)
target_link_libraries(sim PRIVATE physics)

Why PUBLIC on the include dir? vec.h is in physics's interface, so anyone linking physics must be able to find it. PUBLIC makes that include directory flow into sim automatically. Why PRIVATE on the link? sim is an executable — nothing links against an executable, so propagating past it is meaningless; PRIVATE is the minimal correct choice. (On Header files and Include Guardsvec.h still needs its own include guard regardless.)

Exercise 2.3. Turn physics into a header-only library (all code lives in vec.h, no .cpp).

Recall Solution
add_library(physics INTERFACE)                        # no sources
target_include_directories(physics INTERFACE physics) # only users need it
add_executable(sim sim/main.cpp)
target_link_libraries(sim INTERFACE physics)

An INTERFACE library compiles nothing; its whole job is to hand include dirs/flags to users. Everything about it is INTERFACE because physics itself has no implementation to serve.


Level 3 — Analysis (trace the graph)

Exercise 3.1. Given:

add_library(util STATIC util.cpp)
add_library(core STATIC core.cpp)
add_executable(app app.cpp)
target_link_libraries(core PUBLIC util)
target_link_libraries(app  PRIVATE core)

Does app link util? Does app see util's headers? Justify from the graph.

Recall Solution

Yes to both. core → util is PUBLIC, so util is part of core's interface. When app links core, the PUBLIC (interface) portion of core — which includes util and util's include dirs — flows one step further into app. Trace it on figure s01: follow the yellow PUBLIC arrow core→util, then the app→core edge — the PUBLIC edge keeps propagating. Link order (util before core) is computed by CMake from this graph; you never spell it out.

Exercise 3.2. Now change one word: target_link_libraries(core PRIVATE util). Re-answer 3.1.

Recall Solution
  • Does app link util? Yes — a STATIC private dependency's object code is still needed at final link time (the symbols core.cpp calls must resolve), so util is pulled into the final binary. Linkage as code still happens.
  • Does app see util's headers? NoPRIVATE hides util's include dirs inside core. app cannot #include util's headers, which is correct: app never asked for util.

This is the key distinction: PRIVATE hides the usage requirements (headers/flags), not the raw object code a static link physically needs.

Exercise 3.3. mathlib is SHARED. app links it PRIVATE. Is mathlib's object code copied into app?

Recall Solution

No. A SHARED library (.so/.dll) is loaded at runtime, never copied in. app records a reference to mathlib and the loader finds it when app starts. PRIVATE vs PUBLIC only affects whether mathlib's headers/flags propagate to app's consumers — it does not change static-vs-dynamic copy behaviour. See Static vs Dynamic Linking.


Level 4 — Synthesis (design a project)

Exercise 4.1. Design CMakeLists.txt for a project with these facts:

  • logger (STATIC) — its header logger.h is used inside other libs' headers.
  • network (STATIC) — uses logger in network.h (exposes it), uses a private helper crypto (STATIC) only in network.cpp.
  • server (executable) — links network.
  • The right propagation must let server include logger.h but not crypto.h.
Recall Solution
cmake_minimum_required(VERSION 3.16)
project(Server LANGUAGES CXX)
 
add_library(logger  STATIC logger.cpp)
target_include_directories(logger PUBLIC logger)
 
add_library(crypto  STATIC crypto.cpp)
target_include_directories(crypto PUBLIC crypto)
 
add_library(network STATIC network.cpp)
target_include_directories(network PUBLIC network)
target_link_libraries(network PUBLIC  logger)   # logger appears in network.h
target_link_libraries(network PRIVATE crypto)   # crypto only inside network.cpp
 
add_executable(server main.cpp)
target_link_libraries(server PRIVATE network)

Trace the requirement: server → network (PRIVATE, fine — server has no users). network → logger is PUBLIC, so logger's headers flow through network into serverserver can #include "logger.h". ✅ network → crypto is PRIVATE, so crypto's include dir stays inside networkserver cannot see crypto.h. ✅ (crypto's object code is still linked into the final binary because it's STATIC.)

Exercise 4.2. Extend 4.1: logger needs the fmt package (found via find_package) and fmt types appear in logger.h. Add the two lines.

Recall Solution
find_package(fmt REQUIRED)                       # brings in the fmt::fmt target
target_link_libraries(logger PUBLIC fmt::fmt)    # fmt types are in logger.h -> PUBLIC

Because fmt appears in logger's header, it is part of logger's interface → PUBLIC. It then rides the PUBLIC chain logger → network → server, so server transparently gets fmt too. See Package Management find_package.


Level 5 — Mastery (debug the real trap)

Exercise 5.1. A student writes:

include_directories(math)                 # top of file
link_libraries(mathlib)
add_executable(a a.cpp)
add_executable(b b.cpp)                    # b does NOT use mathlib

b links mathlib even though it never uses it, and a stray flag broke another target. Explain why, and rewrite correctly.

Recall Solution

Why it breaks: include_directories() and link_libraries() are directory-scoped, non-target commands. They apply to every target declared afterward in that directory — including b. So b links mathlib it never asked for, and any global flag leaks into unrelated targets. This is exactly the "leak" the parent note warns about. Correct rewrite (target-centric):

add_library(mathlib STATIC math/add.cpp)
target_include_directories(mathlib PUBLIC math)
 
add_executable(a a.cpp)
target_link_libraries(a PRIVATE mathlib)   # only a links it
 
add_executable(b b.cpp)                    # b stays clean

Now each target declares exactly its own dependencies — no leakage.

Exercise 5.2. Compute the total number of times shared.cpp is compiled in each design. Design A: add_executable(x x.cpp shared.cpp) and add_executable(y y.cpp shared.cpp) and add_executable(z z.cpp shared.cpp). Design B: add_library(sh STATIC shared.cpp) then x, y, z each target_link_libraries(... PRIVATE sh). Give both counts and the saving.

Recall Solution
  • Design A: shared.cpp is compiled once per executable that lists it → 3 compilations.
  • Design B: shared.cpp lives in one library target → compiled 1 time; x, y, z just link the resulting object.
  • Saving: redundant compilations avoided (a reduction). This is the concrete cost of the "just add the .cpp everywhere" mistake — and it grows linearly with the number of consumers.

Exercise 5.3. In a chain app → core (PUBLIC) → util (PUBLIC) → base (PUBLIC), how many libraries does app end up linking, and how many transitive (i.e. not directly named by app)?

Recall Solution

app names only core directly. Because every edge is PUBLIC, the interface flows all the way down: app links core, util, base3 libraries total, of which 2 (util, base) are transitive. Follow the unbroken chain of yellow PUBLIC arrows on figure s02 — one broken (PRIVATE) link anywhere would stop the flow at that point.


Wrap-up recall

Recall Fast self-check (answer, then expand)
  • PRIVATE hides what but still links what (static)? ::: hides headers/flags; still links object code.
  • Header-only lib keyword? ::: INTERFACE everywhere.
  • Dependency in your header ⇒ keyword? ::: PUBLIC.
  • shared.cpp in 3 executables vs 1 library — compilations? ::: 3 vs 1.
  • Who computes link order? ::: CMake, from the dependency graph.