CMake - Erweiterte Funktionen

Sobald Sie die Grundlagen von CMake beherrschen, können erweiterte Funktionen Ihnen helfen, die Build-Konfiguration komplexer Projekte flexibler zu verwalten.

Dieser Artikel behandelt sechs Hauptthemen: benutzerdefinierte Module, Multi-Konfigurations-Builds, erweiterte Paketsuche, benutzerdefinierte Build-Schritte, plattformübergreifende Cross-Kompilierung und Konfiguration von Target-Eigenschaften.

Gesamtübersicht des Inhalts:

Thema Kernfähigkeit Typisches Anwendungsszenario
Benutzerdefinierte Module und Skripte Wiederverwendbare CMake-Logik kapseln Einheitliche Compiler-Optionen, benutzerdefinierte Suchlogik, im Team gemeinsam genutzte Build-Regeln
Build-Konfigurationen und Targets Multi-Konfiguration und Multi-Target verwalten Umschalten zwischen Debug/Release, Erstellen mehrerer Ausgabeartefakte
Erweiterte Suche und Konfiguration Externe Abhängigkeiten flexibel lokalisieren Auffinden bestimmter Komponenten, Übergeben von Build-Informationen über Konfigurationsdateien
Benutzerdefinierte Build-Schritte Build-Prozess erweitern Codegenerierung, Ressourcenverarbeitung, Post-Build-Automatisierung
Plattformübergreifend und Cross-Compilation Multiplattform-Builds Embedded-Entwicklung, Kompilieren für verschiedene Betriebssysteme
Target-Eigenschaften und -Konfiguration Feingranulare Kontrolle über Kompilierung und Linken Warnstufen und Link-Pfade für bestimmte Targets festlegen

Benutzerdefinierte CMake-Module und -Skripte

Wenn mehrere CMakeLists.txt-Dateien dieselbe Logik wiederholen müssen, kann das Kapseln dieser Logik in benutzerdefinierte Module die Code-Redundanz stark reduzieren.

Das Wesen benutzerdefinierter Module und Skripte besteht darin, häufig verwendete CMake-Funktionen, Makros und Konfigurationen in separate Dateien auszulagern, um sie in mehreren Projekten wiederverwenden zu können.

Benutzerdefinierte CMake-Module

Ein benutzerdefiniertes Modul ist eine.cmake-Datei mit .cmake als Erweiterung, die CMake-Funktionen und Makros definiert, die über include() geladen werden können.

Erstellungsschritte:

Schritt Aktion Beschreibung
1 Erstellecmake/Verzeichnis Erstellen Sie es unter dem Projektstammverzeichnis, um benutzerdefinierte Module zentral zu speichern.
2 Moduldatei erstellen Zum Beispielcmake/MyModule.cmakebenutzerdefinierte Funktionen schreiben
3 Modulsuchpfad erweitern Registrieren Sie dieses Verzeichnis über CMAKE_MODULE_PATH in CMakeLists.txt
4 Modul laden Verwenden Sie include(), um das Modul zu importieren und seine Funktionen aufzurufen

Beispiel für ein benutzerdefiniertes Modul — MyModule.cmake:

Beispiel

# Dateipfad: cmake/MyModule.cmake
# Benutzerdefiniertes Modul: Stellt Hilfsfunktionen bereit, die im gesamten Projekt verwendet werden

# Funktion: Compiler-Warnoptionen einheitlich für alle Ziele festlegen
# Parameter ARG_TARGET: Name des Ziels, für das Warnungen festgelegt werden müssen
function(example_set_warnings ARG_TARGET)
    # Je nach Compilertyp unterschiedliche Warnflags festlegen
    if(MSVC)
        # MSVC-Compiler: Warnstufe /W4 aktivieren
        target_compile_options(${ARG_TARGET} PRIVATE /W4)
    else()
        # GCC-/Clang-Compiler: übliche Warnungen aktivieren
        target_compile_options(${ARG_TARGET} PRIVATE -Wall -Wextra -Wpedantic)
    endif()
endfunction()

# Funktion: grundlegende Build-Informationen für ein Ziel ausgeben
function(example_print_target_info ARG_TARGET)
    message(STATUS "Zielname: ${ARG_TARGET}")
    message(STATUS "Quelldateiliste: $<TARGET_PROPERTY:${ARG_TARGET},SOURCES>")
endfunction()

Laden und verwenden Sie das Modul in CMakeLists.txt:

Beispiel

# Dateipfad: CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject CXX)

# Das Verzeichnis cmake/ zum Modulsuchpfad hinzufügen
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")

# Benutzerdefiniertes Modul laden
include(MyModule)

# Ausführbares Ziel hinzufügen
add_executable(MyApp main.cpp)

# Benutzerdefinierte Funktion im Modul aufrufen, um Warnoptionen für MyApp festzulegen
example_set_warnings(MyApp)

# Benutzerdefinierte Funktion im Modul aufrufen, um Zielinformationen auszugeben
example_print_target_info(MyApp)

CMAKE_MODULE_PATHist die Suchpfadliste, die CMake beim Suchen nach Moduldateien verwendet. CMake durchsucht zuerst den integrierten Modulpfad und sucht, falls nicht gefunden, die durch CMAKE_MODULE_PATH angegebenen Pfade.

Benutzerdefinierte CMake-Skripte verwenden

Benutzerdefinierte Skripte ähneln Modulen, werden aber typischerweise verwendet, um Konfigurationsvorgänge auszuführen, anstatt wiederverwendbare Funktionen zu definieren.

Skripte können direkt mit include() geladen werden, und CMake führt jeden darin enthaltenen Befehl der Reihe nach aus.

Skriptdatei config.cmake erstellen:

Beispiel

# Dateipfad: config.cmake
# Benutzerdefiniertes Skript: Projektkonfigurationsoptionen zentral verwalten

# Standard-Build-Typ auf Release setzen
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build-Typ" FORCE)
    message(STATUS "Build-Typ nicht angegeben, standardmäßig wird Release verwendet")
endif()

# Je nach Build-Typ unterschiedliche Kompilierungsoptionen festlegen
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    message(STATUS "Debug-Modus aktivieren: Optimierungen deaktivieren, Debug-Symbole aktivieren")
else()
    message(STATUS "Release-Modus aktivieren: O2-Optimierung aktivieren")
endif()

Rufen Sie das Skript in CMakeLists.txt auf:

# 使用绝对路径加载脚本
include(${CMAKE_SOURCE_DIR}/config.cmake)

Build-Konfigurationen und Ziele

CMake unterstützt die Verwaltung mehrerer Build-Konfigurationen (wie Debug und Release) im selben Projekt und die Definition mehrerer Build-Ziele.

Multi-Konfigurations-Generatoren

Verschiedene CMake-Generatoren behandeln Konfigurationen unterschiedlich.

Zum Beispiel Visual Studio und XcodeMulti-Konfigurations-Generatorenermöglichen das Umschalten zwischen Debug/Release im selben Build-Verzeichnis, während Unix Makefiles usw.Single-Konfigurations-Generatorenden Build-Typ in der Konfigurationsphase bestimmen müssen.

Generator-Typ Typische Generatoren Methode zur Angabe der Konfiguration
Single-Konfiguration Unix Makefiles, Ninja Zur Konfigurationszeit über-DCMAKE_BUILD_TYPE=Releaseangeben
Multi-Konfiguration Visual Studio、Xcode Zur Build-Zeit über--config Releaseswitch

Standardkonfiguration in CMakeLists.txt festlegen:

# 仅对单配置生成器生效,多配置生成器会忽略此变量
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type")

CMAKE_BUILD_TYPE ist nur für Single-Configuration-Generatoren wirksam. Wenn Sie Visual Studio verwenden, wechseln Sie die Konfiguration bitte über cmake --build . --config Release oder in der IDE.

Build-Ziele

In einem CMake-Projekt können mehrere Build-Ziele definiert werden, und jedes Ziel konfiguriert unabhängig Kompilierungsoptionen und Link-Abhängigkeiten.

Dies ermöglicht es Ihnen, mehrere ausführbare Dateien oder mehrere Bibliotheken in einem einzigen Build zu erzeugen.

Beispiel

# Zwei ausführbare Ziele definieren, jedes mit unterschiedlichen Quelldateien
add_executable(MyApp src/main.cpp)
add_executable(MyTool src/tool.cpp)

# Unterschiedliche Kompilierungsmakros für verschiedene Ziele festlegen, um die bedingte Kompilierung zu steuern
set_target_properties(MyApp PROPERTIES COMPILE_DEFINITIONS "APP_MODE")
set_target_properties(MyTool PROPERTIES COMPILE_DEFINITIONS "TOOL_MODE")

# Unterschiedliche Bibliotheken für verschiedene Ziele verlinken
target_link_libraries(MyApp PRIVATE MyLib)
target_link_libraries(MyTool PRIVATE MyLib CLI11::CLI11)

Erweitertes Finden und Konfigurieren

Neben der grundlegenden Verwendung von find_package unterstützt CMake außerdem das Angeben von Komponenten, die präzise Steuerung von Versionen und die Weitergabe von Build-Informationen über Konfigurationsdateivorlagen.

Erweiterte Verwendung von find_package

COMPONENTSDas Schlüsselwort ermöglicht es, nur bestimmte Untermodule einer Bibliothek statt des gesamten Pakets zu finden.

Dies ist sehr wichtig für große Bibliotheken wie Boost—das bedarfsgerechte Importieren vermeidet das Verlinken nicht benötigter Komponenten.

Beispiel

# Nur die filesystem- und system-Komponenten von Boost finden (nicht das gesamte Boost)
# filesystem hängt intern von system ab, daher müssen beide deklariert werden
find_package(Boost REQUIRED COMPONENTS filesystem system)

# Prüfen, ob die erforderlichen Komponenten gefunden wurden
if(Boost_FOUND)
    message(STATUS "Boost-Version: ${Boost_VERSION}")
    message(STATUS "Boost-Include-Verzeichnisse: ${Boost_INCLUDE_DIRS}")

    # Jede erforderliche Komponente separat linken
    target_link_libraries(MyApp PRIVATE
        Boost::filesystem
        Boost::system
    )
endif()

Suchpfade angeben (für nicht standardmäßige Installationsorte):

# 方式一:通过变量指定根目录
cmake .. -DBOOST_ROOT=/path/to/custom/boost

# 方式二:在 CMakeLists.txt 中预先设置
set(BOOST_ROOT "/path/to/custom/boost")
find_package(Boost REQUIRED COMPONENTS filesystem)

Konfigurationsdateien und Build-Optionen

configure_file()Die Direktive kann die Werte von CMake-Variablen in Codedateien einfügen, wodurch Konfigurationsinformationen zur Kompilierzeit übergeben werden können.

Dies ist sehr nützlich, wenn Sie Informationen wie Versionsnummern, Build-Zeit usw. in das Programm kompilieren müssen.

Erstellen Sie die Konfigurationsvorlagendatei config.h.in:

Beispiel

// Dateipfad: config.h.in
// CMake-Konfigurationsvorlagendatei, generiert config.h über configure_file()

// Projektversionsnummer (ersetzt durch CMake-Variable @PROJECT_VERSION@)
#define EXAMPLE_VERSION "@PROJECT_VERSION@"

// Build-Typ (Debug oder Release)
#define EXAMPLE_BUILD_TYPE "@CMAKE_BUILD_TYPE@"

// Build-Zeitstempel
#define EXAMPLE_BUILD_TIMESTAMP "@BUILD_TIMESTAMP@"

Konfigurieren und generieren Sie die Header-Datei in CMakeLists.txt:

Beispiel

# Aktuellen Zeitstempel abrufen und in einer Variable speichern
string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S")

# @variables@ in config.h.in durch die Werte der CMake-Variablen ersetzen
# Ausgabe nach config.h im Build-Verzeichnis
configure_file(config.h.in config.h)

# Das Build-Verzeichnis zum Suchpfad für Headerdateien hinzufügen, damit Quelldateien config.h finden können
include_directories(${CMAKE_BINARY_DIR})

Verwenden Sie die generierte Konfiguration in Quelldateien:

// 在 main.cpp 中包含生成的配置文件
#include "config.h"

std::cout << "EXAMPLE Version: " << EXAMPLE_VERSION << std::endl;
std::cout << "Build Type: " << EXAMPLE_BUILD_TYPE << std::endl;

Benutzerdefinierte Build-Schritte generieren

Neben der standardmäßigen Kompilierung und Verlinkung ermöglicht CMake das Einfügen benutzerdefinierter Operationen in den Build-Prozess, wie z. B. Codegenerierung, das Kopieren von Ressourcen und Benachrichtigungen nach dem Build.

Benutzerdefinierte Befehle

add_custom_command()Wird verwendet, um einen benutzerdefinierten Befehl zu definieren, der ausgeführt wird, wenn eine Datei aktualisiert werden muss.

Das Kernkonzept dieses Befehls istabhängigkeitsgetrieben— der Befehl wird nur ausgeführt, wenn die OUTPUT-Datei nicht existiert oder die DEPENDS-Datei aktualisiert wurde.

Beispiel

# Benutzerdefinierter Befehl: Code aus einer Vorlagendatei generieren
# Nur ausführen, wenn generated_file.cpp nicht existiert oder input_template.txt aktualisiert wurde
add_custom_command(
    # Ausgabedatei (CMake prüft diese Datei, um zu entscheiden, ob der Befehl erneut ausgeführt wird)
    OUTPUT ${CMAKE_BINARY_DIR}/generated_file.cpp

    # Auszuführender Befehl (hier wird cmake -E verwendet, um die integrierte Dateigenerierungsoperation auszuführen)
    COMMAND ${CMAKE_COMMAND}
        -E echo "// Auto-generated source file" > ${CMAKE_BINARY_DIR}/generated_file.cpp

    # Abhängigkeitsdatei (der Befehl wird erneut ausgeführt, wenn sich diese Datei ändert)
    DEPENDS ${CMAKE_SOURCE_DIR}/input_template.txt

    # Nachricht, die bei der Ausführung angezeigt wird
    COMMENT "Quelldatei wird aus Vorlage generiert..."
)

Trigger-Befehl für die Build-Phase — wird nach Abschluss des Linkens ausgeführt:

Beispiel

# Nachdem MyApp gebaut wurde, kopiere die ausführbare Datei in das Deployment-Verzeichnis
add_custom_command(
    TARGET MyApp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:MyApp>
        ${CMAKE_SOURCE_DIR}/deploy/
    COMMENT "MyApp in das Deployment-Verzeichnis kopieren"
)

Benutzerdefinierte Targets

add_custom_target()Definiert ein Build-Target, das keine Ausgabedatei erzeugt, und wird normalerweise verwendet, um mehrere benutzerdefinierte Befehle zu gruppieren.

Sofern das Schlüsselwort ALL nicht verwendet wird, werden benutzerdefinierte Targets im Standard-Build nicht ausgeführt; der Target-Name muss explizit angegeben werden.

Beispiel

# Benutzerdefiniertes Ziel: mit dem benutzerdefinierten Befehl oben verknüpfen
# ALL bedeutet, dass es während des Standard-Builds (make) automatisch ausgeführt wird
add_custom_target(generate_code ALL
    # Hängt von der zuvor definierten OUTPUT-Datei ab
    DEPENDS ${CMAKE_BINARY_DIR}/generated_file.cpp
)

# Benutzerdefiniertes Ziel: Hilfsaufgaben ausführen (wird während des Standard-Builds nicht ausgeführt)
add_custom_target(deploy
    COMMAND ${CMAKE_COMMAND} -E echo "Auf Server bereitstellen..."
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_BINARY_DIR}/bin
        /opt/example/deploy/
    COMMENT "Bereitstellungsvorgang ausführen"
)

# Dieses Target während des Builds separat auslösen
# $ cmake --build . --target deploy

add_custom_command und add_custom_target unterscheiden sich: Ersteres definiert eine Regel dafür, "wie Dateien generiert werden", und wird durch Abhängigkeiten ausgelöst; Letzteres definiert ein Aktionsziel für "was zu tun ist" und muss explizit aufgerufen werden oder läuft automatisch, wenn ALL verwendet wird.


Plattformübergreifend und Cross-Compilation

Einer der Kernvorteile von CMake ist die plattformübergreifende Unterstützung — dieselbe CMakeLists.txt kann entsprechende Build-Dateien auf verschiedenen Betriebssystemen und Architekturen generieren.

Plattformübergreifende Builds

CMake erkennt automatisch den Compiler und die Systemumgebung der aktuellen Plattform und generiert passende Build-Dateien.

Sie können Zielplattforminformationen auch explizit über Variablen angeben.

Variable Beschreibung Beispielwert
CMAKE_SYSTEM_NAME Zielbetriebssystem Linux、Windows、Darwin、Android
CMAKE_SYSTEM_PROCESSOR Zielprozessorarchitektur x86_64、arm、aarch64
CMAKE_C_COMPILER C-Compiler-Pfad /usr/bin/arm-linux-gnueabihf-gcc
CMAKE_CXX_COMPILER C++-Compiler-Pfad /usr/bin/arm-linux-gnueabihf-g++

Cross-Kompilierung

Cross-Kompilierung bedeutet, Programme, die auf einer anderen Architektur (wie ARM) laufen, auf der aktuellen Plattform (wie x86_64 Linux) zu kompilieren.

CMake verwendeteine Toolchain-Datei (Toolchain File), um den Compiler, Systeminformationen und die für die Cross-Kompilierung erforderliche Linker-Konfiguration anzugeben.

Erstellen Sie eine Toolchain-Datei toolchain.cmake:

Beispiel

# Dateipfad: toolchain.cmake
# Konfigurationsdatei für die ARM-Linux-Cross-Kompilierungs-Toolchain

# Zielsystem ist Linux
set(CMAKE_SYSTEM_NAME Linux)

# Zielprozessorarchitektur ist ARM
set(CMAKE_SYSTEM_PROCESSOR arm)

# Cross-Compiler angeben (erforderlich: den Pfad an die tatsächliche Toolchain-Position anpassen)
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)

# Root-Dateisystempfad des Zielsystems angeben (optional, wird zum Auffinden von Bibliotheken und Header-Dateien verwendet)
set(CMAKE_FIND_ROOT_PATH /path/to/arm-sysroot)

# Bei der Suche nach Programmen nur in Zielsystempfaden suchen (nicht in Host-Pfaden)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# Bei der Suche nach Bibliotheken nur in Zielsystempfaden suchen
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)

# Bei der Suche nach Header-Dateien nur in Pfaden des Zielsystems suchen
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Cross-Kompilierung mithilfe der Toolchain-Datei:

# 配置时通过 -DCMAKE_TOOLCHAIN_FILE 指定工具链文件
mkdir build_arm && cd build_arm
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake ..

# 使用生成的 Makefile 编译(将调用 ARM 交叉编译器)
make

Die Toolchain-Datei muss beim ersten Ausführen von cmake angegeben werden. Wenn CMakeCache.txt bereits existiert, müssen Sie das Build-Verzeichnis löschen und neu konfigurieren, um die Toolchain zu wechseln.


Target-Eigenschaften und Konfiguration

CMake ermöglicht die feingranulare Steuerung von Kompilierungsoptionen, Link-Optionen und verschiedenen Eigenschaften auf Target-Basis, was in modernem CMake die empfohlene Vorgehensweise ist.

Target-Eigenschaften

set_target_properties()Dient zum Festlegen von Eigenschaften auf Target-Ebene, die das Verhalten von Compiler und Linker beeinflussen.

Eigenschaft Beschreibung Beispiel
COMPILE_OPTIONS Kompilierungsoptionen festlegen -Wall、-O2
COMPILE_DEFINITIONS Präprozessor-Makros festlegen DEBUG、VERSION=2
LINK_FLAGS Linker-Flags setzen -L/path/to/lib
OUTPUT_NAME Ausgabedateinamen ändern Der Zielname ist MyApp, und die Ausgabe ist my_app

Beispiel

# Ein ausführbares Ziel erstellen
add_executable(MyApp main.cpp)

# Kompilierungsoptionen und Linker-Flags für das Ziel setzen
set_target_properties(MyApp PROPERTIES
    COMPILE_OPTIONS "-Wall;-Wextra"
    COMPILE_DEFINITIONS "EXAMPLE_DEBUG"
    LINK_FLAGS "-L/usr/local/lib"
    OUTPUT_NAME "my_app"
)

# Der obige set_target_properties-Befehl entspricht den folgenden drei Befehlen:
# target_compile_options(MyApp PRIVATE -Wall -Wextra)
# target_compile_definitions(MyApp PRIVATE EXAMPLE_DEBUG)
# target_link_options(MyApp PRIVATE -L/usr/local/lib)

In modernem CMake wird empfohlen, target_compile_options(), target_compile_definitions() und target_link_options() anstelle von set_target_properties() zu verwenden, da sie die Sichtbarkeitssteuerung PUBLIC/PRIVATE/INTERFACE unterstützen und Abhängigkeiten klarer machen.

Benutzerdefinierte Kompilierungs- und Link-Optionen

Die Verwendung der target-Befehlsfamilie ermöglicht eine präzisere Steuerung des Weitergabebereichs von Optionen.

Beispiel

# Kompilierungsoptionen für MyApp festlegen
# PRIVATE: Nur MyApp selbst verwendet diese Optionen beim Kompilieren
target_compile_options(MyApp PRIVATE -Wall -Wextra -Wpedantic)

# Präprozessor-Definitionen für MyApp festlegen
# Entspricht dem Schreiben von #define EXAMPLE_VERSION "1.0" in der Quelldatei.
target_compile_definitions(MyApp PRIVATE EXAMPLE_VERSION="1.0")

# Link-Optionen für MyApp festlegen
target_link_options(MyApp PRIVATE -L/usr/local/lib)

# Wenn es eine Bibliothek ist, ermöglicht die Verwendung von PUBLIC, dass Ziele, die diese Bibliothek linken, auch diese Optionen erben
# Zum Beispiel: Jedes Ziel, das MyLib verwendet, aktiviert automatisch C++17
target_compile_features(MyLib PUBLIC cxx_std_17)

Hinweise

CMAKE_BUILD_TYPE ist nur für Single-Configuration-Generatoren gültig.Wenn das Projekt sowohl Makefile als auch Visual Studio unterstützen muss, verlassen Sie sich nicht auf den Wert von CMAKE_BUILD_TYPE. Verwenden Sie Generator-Ausdrücke$<$<CONFIG:Debug>:DEBUG_VALUE>um dynamisch basierend auf der tatsächlichen Konfiguration umzuschalten.

Bevorzugen Sie die target_xxx-Befehlsreihe.Obwohl set_target_properties leistungsfähig ist, fehlt ihm die Sichtbarkeitskontrolle. In Szenarien, die PUBLIC/PRIVATE/INTERFACE-Semantik erfordern, ist es besser, spezielle Befehle wie target_compile_options zu verwenden.

Die Toolchain-Datei muss beim ersten cmake-Aufruf angegeben werden.Beim Cross-Kompilieren kann das erneute Angeben der Toolchain in einem Verzeichnis mit einer vorhandenen CMakeCache.txt zu einer unvollständigen Konfiguration führen. Konfigurieren Sie Cross-Kompilierung immer in einem sauberen Build-Verzeichnis.

Es wird empfohlen, Funktionsnamen in benutzerdefinierten Modulen ein Präfix hinzuzufügen.Vermeiden Sie Namenskonflikte mit in CMake integrierten Befehlen oder anderen Modulen. Verwenden Sie beispielsweise den Projektnamen als Präfix: myproject_set_warnings().

Weitere Erweiterungen