CMake targets

Eine große Last wird in der C/C++ Entwicklung von der Vergangenheit erzeugt. Code, der einmal funktioniert hat, bleibt auf unbestimmte Zeit unverändert.

Doch damit verbaut man sich auch oft viele neue Features und verkompliziert sich unnötig den Alltag.


Als ich 2019 endlich auch alle meine Projekte auf CMake umstellte, tat ich das wie so oft mit dem Hintergedanken den Abwärtskompatibilität.

Ich wollte nur “stabile” Features nutzen, die es schon in CMake 3.4 gab, damit ich stets in der Lage war mit alten CMake Varianten zu arbeiten, die noch Codes aus dem Jahre 2000 bauen konnten.

Und so ergeben sich dann viele Aufrufe nach dem Schema

  • add_definitions(...)
  • include_directories(...)
  • link_directories(...)
  • set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --something)

die zwar alle toll funktionieren, aber den globalen Namensraum verschmutzen und gleichzeitig in anderen Projekten wiederholt werden müssen, damit es dort auch klappt.

Nur target_xxx_yyy(target ...) ist richtig

Modernes CMake sollte auf dem korrekten Ausbau von targets aufbauen. Sobald mit
add_library(my_target ...) oder add_executable(my_target ...)
ein target definiert wurde, sollten ihm öffentliche und private Einstellungen zugewiesen werden.
Die privaten werden nur für das target selbst gebraucht, doch öffentliche vererben sich automatisch an ein Folgeprojekt weiter, das mit
target_link_libraries(other_target my_target)
an seine Abhängigkeit gebunden wird.

target_compile_definitions()

Meiner Erfahrung nach sind 90% aller “Definitions” (also von extern gesetzten Makros) nur für ein einzelnes Projekt relevant. Anstatt add_definition(-DBZ_EXPORT) bei einer bzip2 hineinzuwerfen, sollte es target_compile_defintion(bz2 PRIVATE "BZ_EXPORT") heißen.
Ist ein Makro aber in öffentlichen Headern aktiv, kann es mit einer PUBLIC Definition an alle abhängigen Projekt weitervererbt werden.

target_compile_options()

Das schlimmste, was man tun kann, ist: Wegen einer lästigen Warnung alle Warnungen in allen Projekten abzuschalten. Krebsgeschwüre wie -fpermissive haben so unzählige Opfer infiziert.
Anstatt mit solchen Optionen per add_definition den ganzen Baum zu vergiften, kann man per
target_compile_options(mydirtylib PRIVATE "-fpermissive") nur das eine schlampig geschrieben Projekt anpassen.
Ähnlich verhält es sich auch mit anderen - nicht ganz so bösen - Optionen.

target_include_directories()

include_directories(...) zwingt dem Verzeichnisbaum einen Include-Pfad auf, ganz egal, ob es sich um private Sourcen oder öffentliche Header handelt.
Die richtige Arznei ist: target_include_directories

1target_include_directories(mylib 
2  PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include/private"
3)
4
5target_include_directories(mylib 
6  PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/public"
7)

Private Include-Verzeichnisse gelten nur für das Target selbst, während öffentliche Includes an alle Targets “vererbt” werden, die das aktuelle Target linken.
Das spart einiges an Redundanz, denn bisher mussten “Konsumenten” die Include-Directory-Vorlieben ihrer Abhängigkeiten kennen.

Fazit

Meine eigenen externen Bibliotheken sind voll von den “alten” Funktionen, die teils auch von uralten Projekten stammen.

Doch wer ein neues Projekt auf die Beine stellt sollte (von wenigen berechtigten Ausnahmen abgesehen) nur noch Eigenschaften auf CMake Targets anwenden.

Es ist ja auch vollkommen OK, wenn man sich ein öffentliche CMake Funktion schreibt, die einem Projekt bestimmt “Standards” angedeihen lässt, womit viele Targets ähnlich bzw. gleich aufgesetzt sind.

“Das Target” ist der König in CMake, daher muss alles immer erst mit ebendiesem abgestimmt werden.