# 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) # 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 ` 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`** gilt, solange im Quellbaum gebaut wird. Include-Root ist `/libpda`, damit `` auf `/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 "$") ``` 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 `$` 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. ``` `$` 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 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/` 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/.hpp` + `.cpp` anlegen 2. Beide in `LIBPDA_SOURCES` / `LIBPDA_HEADERS` eintragen — **kein `file(GLOB)`** 3. Öffentliche Deklarationen mit `PDA_EXPORT` markieren 4. `libpda/libpda/.test.cpp` daneben, `libpda_add_doctest_test()` **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 "$<$: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 der Fehler liegt bei CLion. ### Die Reihenfolge, die tatsächlich hilft Beim ersten Einrichten dieses Projekts war es genau diese: 1. **Projekt über die `CMakeLists.txt` öffnen**, nicht über den Ordner (*File → Open…* → die Datei im Wurzelverzeichnis wählen → „Open as Project"). Ein als Ordner geöffnetes Projekt bekommt kein CMake-Modell, und CLion bietet stattdessen den **Einzeldatei-Modus** an. 2. **Toolchain setzen** (siehe unten), dann *Tools → CMake → Reset Cache and Reload Project* — in der **Menüleiste**, nicht in den Einstellungen. 3. **Die Einzeldatei-Konfiguration löschen**: *Run → Edit Configurations…* → unter „C/C++ File" den Eintrag mit dem Dateinamen → **−**. Schritt 3 ist der, den man am ehesten übersieht. Diese Konfiguration (`CppFileRunConfiguration`) uebersetzt eine einzelne Datei **ohne jede Projekteinstellung**: ``` clang++ .../pda/pda/main.cpp -o main # kein -I, kein -std ``` Solange sie oben rechts ausgewählt ist, scheitert jeder Lauf an `'libpda/details/version.hpp' file not found` — obwohl das Projekt völlig in Ordnung ist. Danach steht in derselben Liste `pda_app`, `calculator.test`, `integration_basics` und so weiter. ### 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. Und es bleibt nicht bei einem anderen Compiler. Beim Einrichten hier hat CLions *Reset Cache and Reload Project* die **`CMakeCache.txt` im Terminal-Build-Verzeichnis geloescht** -- `build/debug` behielt `build.ninja`, `compile_commands.json` und alle Binaries, nur der Cache war weg: ``` $ cmake --build --preset debug Error: not a CMake build directory (missing CMakeCache.txt) ``` Kurios dabei: `ctest --preset debug` lief weiter durch, weil `CTestTestfile.cmake` und die gebauten Testprogramme noch dalagen. Der Schaden faellt also erst beim naechsten Bauen auf. Behoben mit einem einfachen `cmake --preset debug`. Ausgeloest hat das ein **deaktiviertes** Preset-Profil -- CLion legt beim Laden fuer jeden Preset eines an (hier zehn, teils doppelt als `debug` und `debug - debug`), und deren `GENERATION_DIR` zeigt auf `build/`. Zwei saubere Auswege: 1. **CLion ein eigenes Build-Verzeichnis geben** (Default `cmake-build-debug`, in `.gitignore` eingetragen) und die Preset-Profile unter *Settings → Build, Execution, Deployment → CMake* **loeschen**, nicht nur deaktivieren. 2. **Denselben Compiler erzwingen:** unter *Settings → Build, Execution, Deployment → Toolchains* C/C++-Compiler auf `/opt/homebrew/opt/llvm/bin/clang` bzw. `clang++` setzen. Die Toolchain muss dabei **ganz oben** in der Liste stehen (fett = Standard), sonst benutzt das CMake-Profil weiter die alte. Danach ist ein Cache-Reset Pflicht -- CMake verweigert den Compilerwechsel sonst: `You have changed variables that require your cache to be deleted.` Am besten beides: eigenes Verzeichnis **und** derselbe Compiler. Dann sieht die IDE dieselben Diagnosen wie das Terminal, ohne sich mit ihm zu schlagen. ## 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.