Articles
CMake Interview Questions: Targets, Usage Requirements, and Dependency Propagation
Practice CMake target interview questions with a worked codec-core-app trace of PUBLIC, PRIVATE, INTERFACE, include paths, and static-library links.
In a CMake interview, saying that PUBLIC “propagates” is only the beginning. A stronger answer names which target compiles with which include directory, which library appears on the final link line, and why a private implementation dependency can behave differently for a static archive. The fastest way to demonstrate that understanding is to trace one small target graph instead of reciting scope definitions.
The example and predictions below are original practice material, not a report of a particular employer's interview. CMake's build-system manual is the authority for target properties and usage requirements. If you want a neighboring build-graph exercise, try Software Engineering Fundamentals questions after working through the trace here.
What does a CMake target actually represent?
An executable target, static or shared library target, object library, imported target, and interface library do different jobs. add_library(codec STATIC codec.cpp) defines an output-producing target. add_library(project_warnings INTERFACE) can carry requirements without producing an archive. An imported target represents something built elsewhere; it can still expose include directories and link requirements to its consumers. A target name is thus more useful than a raw -I or -l string: it gives CMake a place to record how that component must be used.
Two property groups matter in this interview. A target's build specification tells CMake how to compile or link that target itself. Its usage requirements tell CMake what a consumer needs when that consumer links to the target. The official build-system manual describes this distinction and the collection of transitive requirements through link dependencies. Do not confuse “links to a target” with “only the linker cares”: a link edge can carry include directories, compile definitions, and language features into a consumer's compile step.
target_include_directories(core PUBLIC include) adds include to both core's own include path and its consumer interface. PRIVATE affects core's own build; INTERFACE affects consumers. The same scope vocabulary appears on target_compile_definitions, target_compile_features, and target_link_libraries, but the relevant target properties differ. This is a statement about CMake's model, not a claim that every toolchain emits identical command lines.
Interview question: Which target sees each requirement?
Assume this deliberately small project. Its paths are illustrative; the important parts are the target edges and scopes.
cmake_minimum_required(VERSION 3.20)
project(UsageTrace LANGUAGES CXX)
add_library(codec STATIC src/codec.cpp)
target_include_directories(codec PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
target_compile_features(codec PUBLIC cxx_std_17)
add_library(core STATIC src/core.cpp)
target_include_directories(core PRIVATE src/internal)
target_link_libraries(core PUBLIC codec)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core)
Start at codec. Its own source sees the build-tree include directory and compiles with a C++17 requirement. core links to codec with PUBLIC, so core.cpp consumes codec's interface, and core exposes that interface onward. app links to core, so main.cpp also receives codec's public include path and required language feature. The private src/internal path helps compile core.cpp; it is not an include path promised to app. The app PRIVATE core edge does not make core public to some hypothetical consumer of app; it still lets app consume core's interface.
The BUILD_INTERFACE and INSTALL_INTERFACE expressions solve a different problem: the directory used in this source tree need not be the path used by someone consuming an installed package. They do not change the PUBLIC/PRIVATE direction of propagation. CMake's target_include_directories reference explains both scope and these expressions. For an actual installed package, you would also design install/export rules and relocatable dependencies; this snippet only demonstrates the requirement flow.
| Requirement in this project | codec.cpp | core.cpp | main.cpp |
|---|---|---|---|
Public build-tree include from codec | Yes | Yes, through core's link to codec | Yes, through core's public interface |
cxx_std_17 required by codec | Yes | Yes | Yes |
Private src/internal from core | No | Yes | No |
The ledger is about effective requirements, not a promise that the generated build command repeats each flag once. CMake may deduplicate or express a requirement in toolchain-specific ways. In an interview, predict the property flow first, then inspect actual commands if a real build contradicts your expectation.
Interview question: When should the edge be PRIVATE, PUBLIC, or INTERFACE?
Use the public API as the decision test. If core.cpp calls codec but core's public headers hide every codec type and header, PRIVATE is usually the right declaration for that implementation dependency. If core.hpp includes codec.hpp, inherits from a codec type, or exposes one in a public signature, downstream users must be able to compile against codec; PUBLIC is appropriate. If a header-only adapter's headers need codec but the adapter has no compiled source that uses it, an INTERFACE dependency can express that consumer-only requirement. This rule of thumb follows the CMake build-system guidance.
Suppose core.hpp changes from int transform(int) to codec::Value transform(codec::Value). Keeping target_link_libraries(core PRIVATE codec) now conceals a requirement needed by app when it compiles main.cpp. Adding target_include_directories(app PRIVATE include) might silence one local error, but it duplicates the library's contract at every call site and can miss compile definitions or transitive dependencies. Fix the edge on core. The consumer should learn what it needs from the target it uses.
Conversely, marking every edge PUBLIC is not harmless generosity. It makes a private library appear in downstream interfaces, can expose unnecessary headers or compile definitions, and increases coupling. The evidence for scope is the library's public contract, not whether one machine currently builds after a broad global include-directory setting.
The static-library trap: does PRIVATE mean nothing propagates?
No. Say core is a static library and core.cpp calls a function defined in a second static library codec. An archive is primarily a collection of object files; creating libcore.a does not generally resolve every external reference in those objects. When the final executable links, it may still need codec to satisfy core's reference. CMake can therefore carry a privately linked static dependency to the final link step while withholding its compile usage requirements from consumers. The official target_link_libraries examples show this distinction explicitly for a private dependency through a static library.
This is where memorized slogans fail. PRIVATE means that a dependency is not offered as an ordinary public usage interface. It does not mean that the dependency can never appear on a downstream executable's link line. Conversely, seeing codec on that link line does not prove main.cpp should inherit codec's include directory. Compilation requirements and link closure answer different questions.
For a shared library, the platform's dynamic linking model, exported symbols, loader behavior, and CMake policy details matter. Avoid promising identical link lines across a shared and static build. State the platform and generator when investigating a concrete failure. If the prompt shows only a CMake snippet, explain the target model first and qualify command-line predictions that depend on generator or toolchain.
Interview question: What failed—configure, generate, compile, or link?
CMake first configures the project: it evaluates CMakeLists.txt, finds packages, checks conditions, and constructs the target model. It then generates a native build system such as Ninja files or Makefiles. The native tool builds objects and links artifacts. Errors at these stages point to different evidence. An unknown target in target_link_libraries may be a configuration/model issue. A missing header when compiling main.cpp suggests that an include requirement did not reach app. An undefined reference after all objects compile suggests a missing definition, wrong signature or ABI, omitted library, or link-order issue.
An interviewer may ask why #include "codec.hpp" compiles on the developer machine while CI fails. Inspect compile_commands.json (when generated), target properties, and the relevant compile command. A global directory-level include, environment variable, or cached build tree may have hidden a missing target contract locally. Then reproduce from a clean build directory. Do not cure a link failure by adding an include path; that changes compilation, not whether an object containing the needed definition reaches the linker.
For a reproducible diagnosis, run cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON, then cmake --build build --verbose. Verify the effective command for the failing source file, not just the textual CMake line. On a multi-config generator, specify the configuration when the observed command depends on it. CMake's build-system manual distinguishes target properties from the produced build specification; the command inspection is our recommended debugging procedure.
Five practice questions that make the graph concrete
Work each one aloud, then return to the codec → core → app ledger and name which part is a build-order problem, a compile-requirement problem, or a link problem.
| Question | Connection to this article | What to explain aloud |
|---|---|---|
| Trace Source Code to an Executable | Build stages | Separate preprocessing, object compilation, linking, and loading. |
| GCC, Clang, LLVM, and C++ Compiler Optimization | Toolchain boundary | State what CMake controls and what compiler behavior must be measured. |
| Compute build order from dependencies | Target DAG | Explain prerequisites and cycles without confusing order with usage requirements. |
| Order Dependent Tasks and Identify Work to Rerun | Changed-input reachability | Identify downstream work affected by one changed node. |
| Produce a valid deployment order | Graph analogy | Transfer the topological reasoning while stating why deployment is not linking. |
A concise answer you can give at the whiteboard
Draw three boxes: codec, core, and app. Put “build settings” inside each box and “usage requirements” on outward-facing edges. Say: “PUBLIC affects the producer and consumers; PRIVATE affects the producer; INTERFACE affects consumers. The consumer collects requirements through target link dependencies. In this graph, app inherits codec's public headers and C++17 requirement through core, but not core's private internal path. I choose the edge scope from the public headers, then inspect compile and link commands to test the model. A private static dependency can still be needed at final link, without exporting its compile requirements.”
That answer is a useful baseline, not a universal command-line prediction. If the interviewer changes core to a shared library, makes codec header-only, or exports the targets for installation, revise the ledger rather than repeating the same sentence. To practice the first failure boundary, answer Trace Source Code to an Executable with the build stages labeled before you look at its solution.