Exercises — CMake — CMakeLists.txt, add_executable, add_library, target_link_libraries
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.

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 pastcalc).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 Guards — vec.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
applinkutil? Yes — a STATIC private dependency's object code is still needed at final link time (the symbolscore.cppcalls must resolve), soutilis pulled into the final binary. Linkage as code still happens. - Does
appseeutil's headers? No —PRIVATEhidesutil's include dirs insidecore.appcannot#includeutil's headers, which is correct:appnever asked forutil.
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 headerlogger.his used inside other libs' headers.network(STATIC) — usesloggerinnetwork.h(exposes it), uses a private helpercrypto(STATIC) only innetwork.cpp.server(executable) — linksnetwork.- The right propagation must let
serverincludelogger.hbut notcrypto.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 server ⇒
server can #include "logger.h". ✅
network → crypto is PRIVATE, so crypto's include dir stays inside network ⇒ server
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 -> PUBLICBecause 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 mathlibb 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 cleanNow 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.cppis compiled once per executable that lists it → 3 compilations. - Design B:
shared.cpplives in one library target → compiled 1 time;x,y,zjust link the resulting object. - Saving: redundant compilations avoided (a reduction).
This is the concrete cost of the "just add the
.cppeverywhere" 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, base → 3 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.cppin 3 executables vs 1 library — compilations? ::: 3 vs 1.- Who computes link order? ::: CMake, from the dependency graph.