Files
playground/docs/CMAKE.md
T
paulhorn 2c21df4be0 docs: CLion-Abschnitt in CMAKE.md, cmake-build-* ignorieren
Die Meldung 'This file does not belong to any project target' kommt nicht
vom CMakeLists.txt: die File API ordnet calculator.hpp korrekt dem Target
pda zu (LIBPDA_HEADERS geht an add_library). CLion hatte nur kein Modell.

Dokumentiert dazu die Falle, dass eine aus dem Dock gestartete GUI die PATH
aus .zprofile nicht erbt -- 'CMAKE_C_COMPILER: clang' loest dort auf Apple
clang auf, im Terminal auf Homebrew-LLVM. Da binaryDir in beiden Faellen
build/debug ist, gewinnt lautlos, wer zuerst konfiguriert.

.gitignore deckte /build/ ab, aber nicht CLions Default cmake-build-<profil>/
im Wurzelverzeichnis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:22:19 +02:00

555 lines
18 KiB
Markdown

# CMake verstehen
Diese Datei erklärt CMake an *diesem* Projekt. Kein Referenzhandbuch — die
Reihenfolge folgt der Frage „wann mache ich was?".
---
## 1. Das Modell: es gibt nur Targets
CMake ist keine Skriptsprache, die Compiler-Kommandos zusammenbaut. Es ist ein
**Generator**: es baut einen Graphen aus *Targets* und schreibt daraus
Ninja-Dateien. Alles, was du in einer `CMakeLists.txt` schreibst, hat genau
einen Zweck — Targets anzulegen und ihre Eigenschaften zu setzen.
Ein Target ist:
| Art | Befehl | in diesem Projekt |
|---|---|---|
| Bibliothek | `add_library` | `pda`, `pda_shell`, `unity` |
| Programm | `add_executable` | `pda_app`, `calculator.test` |
| INTERFACE-Target | `add_library(x INTERFACE)` | `pda_warnings`, `doctest` |
Ein **INTERFACE-Target** hat keinen eigenen Quellcode. Es ist ein Bündel
Eigenschaften, das man weiterreicht — `pda_warnings` in `cmake/Warnings.cmake`
ist nichts als eine Liste von `-W`-Flags mit einem Namen.
Eigenschaften vererben sich am Graphen entlang. Das ist der ganze Trick, und
es ist der Grund, warum man **niemals** `include_directories()` oder
`add_definitions()` benutzt: die wirken global auf alles, statt an einem Target
zu hängen.
> **Faustregel:** Jeder Befehl, den du benutzt, sollte `target_` am Anfang
> haben. Wenn nicht, frag dich, warum.
---
## 2. PUBLIC, PRIVATE, INTERFACE
Das ist der Begriff, an dem die meisten hängenbleiben. Die drei Wörter
beantworten *eine* Frage: **Wer sieht diese Eigenschaft?**
```
ich selbst wer mich linkt
PRIVATE ja nein
INTERFACE nein ja
PUBLIC ja ja
```
Aus `libpda/CMakeLists.txt`:
```cmake
target_include_directories(pda PUBLIC ...) # contact.hpp braucht der Benutzer
target_link_libraries(pda PRIVATE pda_warnings) # unsere Warnungen sind unsere Sache
target_compile_features(pda PUBLIC cxx_std_23) # <expected> steht IM Header
```
Die Entscheidung hängt nur davon ab, **ob die Sache im Header vorkommt**:
- Etwas steht in einem öffentlichen Header → `PUBLIC`
- Etwas kommt nur in der `.cpp` vor → `PRIVATE`
`target_compile_features(pda PUBLIC cxx_std_23)` ist dafür das beste Beispiel.
`contact.hpp` gibt `std::expected` zurück. Wer libpda mit C++17 benutzen will,
soll eine klare Meldung bekommen und keinen Header-Fehler dreißig Zeilen tief.
Ein Fall aus diesem Projekt, in `pda/CMakeLists.txt`:
```cmake
target_link_libraries(pda_shell PUBLIC pda::pda)
```
`PUBLIC`, obwohl `pda_shell` intern ist — weil `shell.hpp` die Zeile
`#include <libpda/contact.hpp>` enthält. Wäre das `PRIVATE`, würde
`shell.test.cpp` mit „file not found" scheitern.
---
## 3. Reihenfolge: was muss wann stehen
CMake liest von oben nach unten. Manches wird **beim Anlegen eines Targets**
eingefroren, anderes erst am Ende ausgewertet.
**Muss VOR dem ersten Target stehen** (sonst wirkungslos):
```cmake
set(CMAKE_CXX_STANDARD 23) # wird ins Target kopiert
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ...)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
```
Deshalb steht das alles in `cmake/ProjectDefaults.cmake`, das ganz oben
eingebunden wird. Ein `set(CMAKE_CXX_STANDARD 23)` *nach* `add_library()`
ändert nichts mehr — der häufigste stumme Fehler in CMake.
**Darf nach dem Target stehen** (arbeitet auf dem Target):
```cmake
target_link_libraries(...)
target_include_directories(...)
set_target_properties(...)
```
**Muss ins oberste `CMakeLists.txt`:**
```cmake
enable_testing() # nur hier legt es die CTestTestfile.cmake an, die ctest sucht
```
---
## 4. Generator-Ausdrücke
`$<...>` wird **nicht** beim Einlesen ausgewertet, sondern erst beim
Generieren — wenn CMake schon weiß, welche Konfiguration gebaut wird und ob
gerade installiert wird. Deshalb kann man damit Dinge sagen, die zur Lesezeit
noch nicht feststehen.
Die zwei wichtigsten stehen in `libpda/CMakeLists.txt`:
```cmake
target_include_directories(pda PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
"$<INSTALL_INTERFACE:include>")
```
- **`BUILD_INTERFACE`** gilt, solange im Quellbaum gebaut wird. Include-Root
ist `<repo>/libpda`, damit `<libpda/contact.hpp>` auf
`<repo>/libpda/libpda/contact.hpp` zeigt.
- **`INSTALL_INTERFACE`** gilt nach dem Installieren, relativ zum Präfix.
**Ohne diese Trennung zeigt die installierte Bibliothek auf dein
Build-Verzeichnis.** Beim Benutzer existiert das nicht. Das ist *der*
klassische Fehler beim Verteilen einer Bibliothek, und er fällt im eigenen
Build nie auf — `tools/check-install.sh` existiert genau deswegen.
Der zweite Fall ist subtiler:
```cmake
target_link_libraries(pda PRIVATE "$<BUILD_INTERFACE:pda_warnings>")
```
Warum nicht einfach `PRIVATE pda_warnings`? Eine **statische** Bibliothek
linkt ihre Abhängigkeiten nicht selbst — das muss der tun, der sie benutzt.
CMake trägt `PRIVATE`-Abhängigkeiten deshalb trotzdem als `$<LINK_ONLY:...>`
ins Interface ein. `install(EXPORT)` sieht dort `pda_warnings`, findet es in
keinem Export-Set und bricht ab:
```
install(EXPORT "pdaTargets" ...) includes target "pda" which requires
target "pda_warnings" that is not in any export set.
```
`$<BUILD_INTERFACE:...>` löst im Build zu `pda_warnings` auf und beim
Installieren zu nichts.
---
## 5. Superprojekt und Teilprojekte
```
playground/ <- Superprojekt, kein eigener Quellcode
├── libpda/ <- eigenes project()
└── pda/ <- eigenes project()
```
P1204R0 verlangt die Trennung von Bibliothek und Programm. Der praktische
Grund: die Bibliothek muss **ohne** die Anwendung baubar sein, sonst ist sie
keine Bibliothek.
Jedes Teilprojekt hat deshalb diesen Block:
```cmake
if(PROJECT_IS_TOP_LEVEL)
# Alles holen, was sonst das Superprojekt bereitstellt
endif()
```
`PROJECT_IS_TOP_LEVEL` ist `TRUE`, wenn dieses `project()` das oberste ist.
Beide Wege müssen funktionieren:
```sh
cmake --preset debug # Superprojekt: baut beides
cmake -S libpda -B build/nur-lib # nur die Bibliothek
cmake -S pda -B build/nur-app \ # nur die Anwendung, gegen installierte libpda
-DCMAKE_PREFIX_PATH=/pfad/zur/installation
```
Der dritte Fall ist der wertvollste: `pda/CMakeLists.txt` benutzt dann
`find_package(pda REQUIRED)` — also **genau den Weg, den ein fremdes Projekt
geht**. Damit ist die Anwendung gleichzeitig der Test für den Export.
Weil `cmake/` und `third_party/` im Wurzelverzeichnis liegen, brauchen die
Teilprojekte im Alleinbetrieb die zweiargumentige Form:
```cmake
add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../third_party" third_party)
```
Das zweite Argument ist das *Build*-Verzeichnis. Ohne es bricht CMake ab, weil
das Quellverzeichnis außerhalb des Projekts liegt und CMake nicht raten will,
wohin die Artefakte sollen.
Damit `cmake/Warnings.cmake` aus beiden Richtungen eingebunden werden kann,
steht dort `include_guard(GLOBAL)`. Ohne das wäre der zweite
`add_library(pda_warnings INTERFACE)` ein harter Fehler.
---
## 6. Targetname ≠ Dateiname
```cmake
add_library(pda ...) # -> libpda.a (CMake setzt "lib" davor)
add_executable(pda_app pda/main.cpp)
set_target_properties(pda_app PROPERTIES OUTPUT_NAME pda) # -> bin/pda
```
Zwei Targets dürfen nicht gleich heißen — die Bibliothek belegt schon `pda`.
Der *Dateiname* darf trotzdem `pda` sein. Ein Target namens `libpda` ergäbe
übrigens `liblibpda.a`.
Dazu der Alias:
```cmake
add_library(pda::pda ALIAS pda)
```
Er kostet nichts und hat einen konkreten Nutzen: ein Tippfehler in einem Namen
**mit** Doppelpunkt ist ein sofortiger CMake-Fehler. Ohne Doppelpunkt hält
CMake ihn für eine Systembibliothek und scheitert erst beim Linken, mit einer
viel schlechteren Meldung.
---
## 7. Eine Bibliothek zum Verteilen: install und export
Vier Schritte, alle in `libpda/CMakeLists.txt`:
```cmake
# 1. Dateien kopieren UND das Target für den Export vormerken
install(TARGETS pda EXPORT pdaTargets
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}")
# 2. Header kopieren -- FILES_MATCHING, sonst kommen die .cpp mit
install(DIRECTORY libpda/ DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libpda"
FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp")
# 3. pdaTargets.cmake erzeugen -- die Datei, die das Target beschreibt
install(EXPORT pdaTargets FILE pdaTargets.cmake NAMESPACE pda::
DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/pda")
# 4. pdaConfig.cmake erzeugen -- das, was find_package(pda) sucht
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/../cmake/pdaConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/pdaConfig.cmake"
INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/pda")
```
`find_package(pda)` sucht `pdaConfig.cmake`, das lädt `pdaTargets.cmake`, und
darin steht `pda::pda` mit allen Include-Pfaden und Flags. Der Benutzer
schreibt nur noch:
```cmake
find_package(pda 0.1 REQUIRED)
target_link_libraries(consumer PRIVATE pda::pda)
```
Kein `-I`, kein `-lpda`, kein Pfad von Hand. Siehe `examples/consumer/`.
Zwei Details, die man leicht übersieht:
- **`GNUInstallDirs`** liefert `CMAKE_INSTALL_LIBDIR`. Auf Fedora ist das
`lib64`, nicht `lib`. Nie selbst hinschreiben.
- **`configure_package_config_file`** statt `configure_file`: es definiert
`@PACKAGE_INIT@`, das die Pfade relativ zum tatsächlichen Ort auflöst. Ein
von Hand geschriebenes Config-File mit absoluten Pfaden funktioniert nur auf
dem Rechner, auf dem installiert wurde.
Hätte libpda externe Abhängigkeiten, müssten sie in `pdaConfig.cmake.in`
stehen:
```cmake
find_dependency(ZLIB REQUIRED)
```
Das ist Pflicht: `pdaTargets.cmake` verweist auf `ZLIB::ZLIB`, und dieses
Target existiert beim Benutzer nur, wenn es vorher gefunden wurde.
---
## 8. Symbolsichtbarkeit
`ProjectDefaults.cmake` setzt:
```cmake
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
```
Damit ist in einer Shared Library **kein** Symbol exportiert, solange es nicht
ausdrücklich markiert ist. Das ist unter Windows ohnehin das Verhalten — es
auch unter Unix zu erzwingen, findet Portabilitätsfehler sofort.
Markiert wird mit einem generierten Makro:
```cmake
include(GenerateExportHeader)
generate_export_header(pda BASE_NAME PDA
EXPORT_FILE_NAME "${CMAKE_CURRENT_BINARY_DIR}/generated/libpda/pda_export.h")
```
und im Header:
```cpp
#include <libpda/pda_export.h>
class PDA_EXPORT ContactBook { ... };
[[nodiscard]] PDA_EXPORT std::string format_error(const EvalError&);
```
Dazu gehört zwingend:
```cmake
if(NOT BUILD_SHARED_LIBS)
target_compile_definitions(pda PUBLIC PDA_STATIC_DEFINE)
endif()
```
Ohne das löst `PDA_EXPORT` im statischen Bau unter Windows zu
`__declspec(dllimport)` auf und der Linker sucht eine DLL, die es nicht gibt.
`PUBLIC`, weil der Benutzer denselben Header inkludiert.
**Der Nutzen ist prüfbar:**
```sh
cmake -S . -B build/shared -DBUILD_SHARED_LIBS=ON && cmake --build build/shared
nm -gU build/shared/lib/libpda.0.1.0.dylib | c++filt | grep -c 'pda::' # 39
nm -gU build/shared/lib/libpda.0.1.0.dylib | c++filt | grep -c 'Parser' # 0
```
Die interne `Parser`-Klasse aus `calculator.cpp` steht im anonymen Namespace
und ist nicht exportiert — sie gehört niemandem außer der Übersetzungseinheit.
Dazu noch:
```cmake
set_target_properties(pda PROPERTIES
VERSION 0.1.0 # libpda.dylib.0.1.0
SOVERSION 0) # libpda.dylib.0 <- der ABI-Stand
```
`SOVERSION` ist die Zusicherung an den Linker: alles mit derselben Zahl ist
binärkompatibel. Sie wird erhöht, wenn sich das ABI ändert — nicht bei jedem
Release.
---
## 9. Tests
```cmake
enable_testing() # nur im obersten CMakeLists.txt
add_test(NAME "unity.textfile" COMMAND textfile.test)
```
Für C++ läuft das nicht von Hand, sondern über doctest:
```cmake
doctest_discover_tests(calculator.test TEST_PREFIX "doctest.")
```
Das ruft die Test-Executable **beim Build** mit `--list-test-cases` auf und
legt einen CTest-Eintrag pro `TEST_CASE` an. Ohne das sähe CTest nur einen
groben Eintrag pro Datei, und neotest könnte Testnamen nicht auf Quellzeilen
abbilden.
> Nach einem frischen Clone ist der Testbaum deshalb leer, bis einmal gebaut
> wurde. Das ist kein Fehler.
Die Anwendung braucht einen Kniff, der weit über CMake hinaus gilt:
```cmake
add_library(pda_shell STATIC pda/shell.cpp) # alles Testbare
add_executable(pda_app pda/main.cpp) # nur Ein-/Ausgabe
target_link_libraries(pda_app PRIVATE pda_shell)
```
Ein Unit-Test kann `pda_shell` linken. Würde die Logik direkt in `pda_app`
stecken, müsste der Test `shell.cpp` erneut übersetzen **und** würde
`main.cpp` mitziehen — zwei `main()` in einer Executable sind ein
Linkerfehler.
---
## 10. Presets
`CMakePresets.json` ersetzt handgeschriebene Kommandozeilen:
```sh
cmake --preset debug # konfigurieren
cmake --build --preset debug # bauen
ctest --preset debug # testen
```
| Preset | Zweck |
|---|---|
| `debug` | Alltag |
| `release` | RelWithDebInfo |
| `asan-ubsan` | AddressSanitizer + UBSan |
| `apple-clang` | Portabilitätsprobe, macOS-Systemcompiler |
| `homebrew-gcc` | Portabilitätsprobe, GCC-Frontend |
| `linux-gcc` | nur auf Linux sichtbar (`condition`) |
`binaryDir` ist `build/<preset>` und die **Tiefe ist bindend**: der
neotest-CTest-Adapter sucht nur drei Ebenen tief, und der Neovim-Target-Picker
leitet den File-API-Pfad daraus ab.
Optionen setzt man ohne neuen Preset dazu:
```sh
cmake --preset debug -DBUILD_SHARED_LIBS=ON -DPDA_BUILD_TESTS=OFF
```
`BUILD_SHARED_LIBS` ist ein CMake-Konventionsname — `add_library()` ohne
`STATIC`/`SHARED` richtet sich danach. Deshalb nie selbst erfinden.
---
## 11. Wann mache ich was?
**Eine neue Datei zu einem bestehenden Modul**
Nichts. Header werden über `#include` gefunden.
**Ein neues Modul in der Bibliothek**
1. `libpda/libpda/<name>.hpp` + `.cpp` anlegen
2. Beide in `LIBPDA_SOURCES` / `LIBPDA_HEADERS` eintragen — **kein `file(GLOB)`**
3. Öffentliche Deklarationen mit `PDA_EXPORT` markieren
4. `libpda/libpda/<name>.test.cpp` daneben, `libpda_add_doctest_test(<name>)`
**Warum kein `file(GLOB)`:** CMake wertet ihn beim *Konfigurieren* aus. Kommt
später eine Datei dazu, merkt der Build es nicht — er bleibt grün, und die
Datei ist einfach nicht dabei. `CONFIGURE_DEPENDS` mildert das, kostet aber bei
jedem Build einen Verzeichnis-Scan und ist laut CMake-Doku nicht zuverlässig.
**Ein neues Kommando in der Anwendung**
Nur `pda/pda/shell.cpp` ändern. `shell.cpp` steht schon in `pda_shell`.
**Eine externe Abhängigkeit**
```cmake
find_package(fmt REQUIRED)
target_link_libraries(pda PRIVATE fmt::fmt)
```
Steht sie in einem **öffentlichen Header**, muss sie `PUBLIC` sein **und** in
`cmake/pdaConfig.cmake.in` als `find_dependency(fmt REQUIRED)` auftauchen.
Sonst scheitert `find_package(pda)` beim Benutzer mit „target fmt::fmt not
found".
**Etwas soll nur im Debug-Bau passieren**
```cmake
target_compile_definitions(pda PRIVATE "$<$<CONFIG:Debug>:PDA_DEBUG>")
```
Nicht `if(CMAKE_BUILD_TYPE STREQUAL Debug)` — das bricht bei
Multi-Config-Generatoren (Xcode, Visual Studio), wo die Konfiguration erst
beim Bauen feststeht.
---
## 12. Fehler, die dieses Projekt schon eingebaut hat
| Symptom | Ursache | Wo nachlesen |
|---|---|---|
| `requires target "pda_warnings" that is not in any export set` | `PRIVATE` reicht bei statischen Bibliotheken nicht | Abschnitt 4 |
| Shared-Bau linkt nicht, alle Symbole fehlen | `hidden` gesetzt, aber `PDA_EXPORT` nirgends benutzt | Abschnitt 8 |
| `set(CMAKE_CXX_STANDARD)` wirkt nicht | steht nach `add_library()` | Abschnitt 3 |
| Benutzer bekommt Pfad ins Build-Verzeichnis | `BUILD_INTERFACE`/`INSTALL_INTERFACE` fehlt | Abschnitt 4 |
| Neue Datei wird nicht gebaut | `file(GLOB)` | Abschnitt 11 |
Die ersten beiden sind beim Bau *dieses* Projekts tatsächlich aufgetreten.
---
## 13. CLion
CLion liest **dieselbe** CMake File API wie jedes andere Werkzeug — es hat
keine eigene Projektdatei. Die Meldung
> This file does not belong to any project target
heisst deshalb fast nie, dass mit dem `CMakeLists.txt` etwas nicht stimmt. Sie
heisst: *CLion hat (noch) kein CMake-Modell.* Nachsehen kann man das selbst:
```sh
mkdir -p build/debug/.cmake/api/v1/query
touch build/debug/.cmake/api/v1/query/codemodel-v2
cmake --preset debug
python3 -c "
import json,glob
d=json.load(open(glob.glob('build/debug/.cmake/api/v1/reply/target-pda-*')[0]))
print(*[s['path'] for s in d['sources']], sep='\n')"
```
Stehen die Header dort — und sie stehen dort, weil `LIBPDA_HEADERS` an
`add_library()` geht — dann ist das Modell korrekt und CLion muss nur neu
laden: **File → Reload CMake Project**.
### Die Falle: zwei Compiler, ein Build-Verzeichnis
Der Preset `debug` setzt `"CMAKE_C_COMPILER": "clang"`. Das wird über `PATH`
aufgelöst — und eine aus dem Dock gestartete GUI-Anwendung erbt die `PATH` aus
`.zprofile` **nicht**. In CLion löst `clang` deshalb auf `/usr/bin/clang`
(Apple clang) auf, im Terminal auf `/opt/homebrew/opt/llvm/bin/clang`.
Weil `binaryDir` in beiden Fällen `build/debug` ist, gewinnt schlicht, wer
zuerst konfiguriert — CMake speichert den **vollen Pfad** im Cache und löst
`clang` danach nie wieder auf. Es gibt keine Fehlermeldung, nur einen anderen
Compiler als gedacht.
Zwei saubere Auswege:
1. **CLion ein eigenes Build-Verzeichnis geben** (Default `cmake-build-debug`,
in `.gitignore` eingetragen). Terminal und IDE kommen sich nie in die Quere.
2. **Denselben Compiler erzwingen:** in CLion unter *Settings → Build,
Execution, Deployment → Toolchains* C/C++-Compiler auf
`/opt/homebrew/opt/llvm/bin/clang` bzw. `clang++` setzen.
Weg 1 ist der bequemere, Weg 2 der ehrlichere -- dann sieht clangd in CLion
dieselben Diagnosen wie im Terminal.
## 14. Werkzeuge zum Nachsehen
```sh
cmake --build build/debug --target help # alle Targets
cmake -S . -B build/debug --graphviz=g.dot # Abhängigkeitsgraph
cmake --install build/debug --prefix /tmp/p # ohne echte Installation ausprobieren
./tools/check-install.sh # Export-Kette Ende-zu-Ende
```
Um zu sehen, was ein Target tatsächlich erbt:
```cmake
get_target_property(dirs pda INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "pda erbt: ${dirs}")
```
Und für die echten Compiler-Kommandos: `build/debug/compile_commands.json` —
dieselbe Datei, die clangd liest.