5efc5fccff
Der Einzeldatei-Modus (CppFileRunConfiguration) war der zaeheste Teil: er uebersetzt ohne -I und -std und sieht wie ein Projektfehler aus. Ausserdem als beobachtete Tatsache statt Theorie: CLions 'Reset Cache and Reload Project' hat die CMakeCache.txt in build/debug geloescht, weil die angelegten Preset-Profile dorthin zeigen -- auch deaktiviert. ctest lief danach weiter, nur cmake --build brach ab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
605 lines
21 KiB
Markdown
605 lines
21 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 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/<preset>`.
|
||
|
||
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.
|