從入門到精通:CMakeLists.txt 完全指南
CMake 是一個跨平臺的自動化構建系統,它使用名為 CMakeLists.txt 的配置文件來控制軟件的編譯過程。無論你是剛接觸 CMake 的新手,還是希望提升 CMake 技能的中級開發者,這篇指南都將帶你從基礎到高級全面掌握 CMakeLists.txt 的編寫技巧。
一、CMake 基礎入門
1.1 什么是 CMake?
CMake 是一個跨平臺的開源構建系統,它通過讀取 CMakeLists.txt 文件中的指令來生成標準的構建文件(如 Unix 的 Makefile 或 Windows 的 Visual Studio 項目文件)。CMake 的主要優勢在于:
- 跨平臺性:可以生成適用于不同操作系統和編譯器的構建文件
- 簡化構建過程:自動處理依賴關系和編譯順序
- 模塊化設計:支持大型項目的模塊化管理
- 可擴展性:可以通過自定義命令和函數擴展功能
1.2 最簡單的 CMakeLists.txt
讓我們從一個最簡單的 “Hello World” 項目開始:
# 指定 CMake 的最低版本要求
cmake_minimum_required(VERSION 3.10)# 設置項目名稱
project(HelloWorld)# 添加可執行文件
add_executable(HelloWorld main.cpp)
這個簡單的 CMakeLists.txt 文件包含三個基本指令:
cmake_minimum_required
:指定構建此項目所需的最低 CMake 版本project
:定義項目名稱和相關信息add_executable
:指定要生成的可執行文件和源文件
1.3 構建過程
使用這個 CMakeLists.txt 文件的典型構建流程是:
mkdir build # 創建構建目錄(推薦外部構建)
cd build # 進入構建目錄
cmake .. # 生成構建系統
make # 編譯項目
這種"外部構建"的方式(在單獨的 build 目錄中構建)是推薦的做法,因為它不會污染源代碼目錄。
二、CMakeLists.txt 核心語法詳解
2.1 基本指令深入
2.1.1 project() 指令
project()
指令不僅可以指定項目名稱,還可以設置版本、描述和使用的編程語言:
project(MyProject VERSION 1.0.0 DESCRIPTION "A sample CMake project"LANGUAGES CXX)
VERSION
:設置項目版本號DESCRIPTION
:項目描述信息LANGUAGES
:指定項目使用的編程語言(C 表示 C 語言,CXX 表示 C++)
2.1.2 添加可執行文件
add_executable()
指令用于生成可執行文件:
add_executable(TargetName source1.cpp source2.cpp header1.h)
CMake 會自動識別 .cpp
文件為源文件,.h
文件為頭文件。雖然頭文件可以列出,但通常不需要,除非它們包含需要被 moc 或其他預處理器處理的代碼。
2.1.3 添加庫文件
使用 add_library()
可以創建庫文件:
add_library(LibraryName STATIC source1.cpp source2.cpp)
庫的類型可以是:
STATIC
:靜態庫(.a 或 .lib)SHARED
:動態庫(.so 或 .dll)MODULE
:模塊庫(不被鏈接,但可能被運行時加載)
2.2 變量與屬性
2.2.1 變量設置與使用
CMake 使用 set()
命令定義變量:
set(MY_VARIABLE "Hello World")
set(SOURCE_FILES main.cpp utils.cpp)
使用 ${}
語法引用變量:
message(STATUS "The value is: ${MY_VARIABLE}")
add_executable(MyApp ${SOURCE_FILES})
2.2.2 緩存變量
緩存變量會保存在 CMakeCache.txt 中,可以在后續構建中使用:
set(MY_CACHE_VAR "DefaultValue" CACHE STRING "A description of this variable")
緩存變量可以在命令行通過 -D
選項設置:
cmake -DMY_CACHE_VAR="CustomValue" ..
2.2.3 環境變量
讀取和使用環境變量:
set(ENV{PATH} "$ENV{PATH}:/opt/local/bin")
message(STATUS "Current PATH: $ENV{PATH}")
2.3 控制流
2.3.1 條件語句
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")message(STATUS "Running on Linux")
elseif(WIN32)message(STATUS "Running on Windows")
else()message(STATUS "Running on unknown system")
endif()
常用條件表達式:
DEFINED var
:檢查變量是否定義EXISTS path
:檢查路徑是否存在file1 IS_NEWER_THAN file2
:檢查文件時間戳version1 VERSION_LESS version2
:版本比較
2.3.2 循環語句
# foreach 循環
foreach(i RANGE 1 10)message(STATUS "Counter: ${i}")
endforeach()# while 循環
set(i 0)
while(i LESS 10)message(STATUS "Counter: ${i}")math(EXPR i "${i} + 1")
endwhile()
2.4 文件操作
2.4.1 包含目錄
include_directories(${PROJECT_SOURCE_DIR}/include)
現代 CMake 更推薦使用 target_include_directories()
:
target_include_directories(MyTarget PUBLIC include)
2.4.2 鏈接庫
target_link_libraries(MyTarget PUBLIC MyLibrary)
PUBLIC
、PRIVATE
和 INTERFACE
關鍵字控制依賴的傳遞性:
PRIVATE
:僅當前目標使用INTERFACE
:僅依賴此目標的其他目標使用PUBLIC
:當前目標和其他依賴目標都使用
2.4.3 文件操作命令
# 查找所有 .cpp 文件
file(GLOB SOURCES "src/*.cpp")# 復制文件
file(COPY data DESTINATION ${CMAKE_BINARY_DIR})# 讀寫文件
file(READ "${PROJECT_SOURCE_DIR}/VERSION" PROJECT_VERSION)
file(WRITE "${CMAKE_BINARY_DIR}/generated.h" "#define VERSION \"${PROJECT_VERSION}\"")
三、項目結構組織
3.1 基本項目結構
一個典型的 CMake 項目結構如下:
MyProject/
├── CMakeLists.txt # 頂層 CMake 配置
├── build/ # 構建目錄(外部構建)
├── include/ # 公共頭文件
│ └── MyLib/
│ └── header.h
├── src/ # 源文件
│ ├── CMakeLists.txt # 子目錄 CMake 配置
│ ├── main.cpp
│ └── utils.cpp
└── tests/ # 測試代碼└── CMakeLists.txt
3.2 子目錄管理
使用 add_subdirectory()
將項目分解為多個子目錄:
# 頂層 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)add_subdirectory(src) # 包含 src 子目錄
add_subdirectory(tests) # 包含 tests 子目錄
每個子目錄有自己的 CMakeLists.txt 文件。
3.3 源文件收集
有多種方式指定源文件:
- 顯式列出所有源文件:
add_executable(MyApp src/main.cpp src/utils.cpp)
- 使用
aux_source_directory
自動收集:
aux_source_directory(. SRC_FILES)
add_executable(MyApp ${SRC_FILES})
- 使用
file(GLOB)
更靈活地匹配文件:
file(GLOB SRC_FILES "src/*.cpp" "src/*.c")
add_executable(MyApp ${SRC_FILES})
注意:GLOB
不會自動檢測新增文件,需要重新運行 CMake。
四、依賴管理與查找
4.1 查找系統庫
使用 find_package
查找系統安裝的庫:
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
target_link_libraries(MyTarget PUBLIC Boost::filesystem Boost::system)
REQUIRED
表示必須找到該包,否則報錯。
4.2 自定義查找模塊
如果 CMake 沒有提供某個庫的查找模塊,可以自己編寫 FindXXX.cmake
:
# FindMyLib.cmake
find_path(MYLIB_INCLUDE_DIR mylib.h HINTS /usr/local/include)
find_library(MYLIB_LIBRARY NAMES mylib HINTS /usr/local/lib)include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MyLib DEFAULT_MSG MYLIB_LIBRARY MYLIB_INCLUDE_DIR)if(MyLib_FOUND)set(MyLib_LIBRARIES ${MYLIB_LIBRARY})set(MyLib_INCLUDE_DIRS ${MYLIB_INCLUDE_DIR})
endif()
然后在 CMakeLists.txt 中使用:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
find_package(MyLib REQUIRED)
target_include_directories(MyTarget PUBLIC ${MyLib_INCLUDE_DIRS})
target_link_libraries(MyTarget PUBLIC ${MyLib_LIBRARIES})
4.3 第三方依賴管理
現代 CMake 項目常用這些方法管理第三方依賴:
- Git 子模塊:
git submodule add https://github.com/xxx/yyy.git extern/yyy
然后在 CMakeLists.txt 中:
add_subdirectory(extern/yyy)
target_link_libraries(MyTarget PUBLIC yyy)
- FetchContent(CMake 3.11+):
include(FetchContent)
FetchContent_Declare(googletestGIT_REPOSITORY https://github.com/google/googletest.gitGIT_TAG release-1.11.0
)
FetchContent_MakeAvailable(googletest)
target_link_libraries(MyTarget PRIVATE gtest_main)
- ExternalProject(更復雜但更靈活):
include(ExternalProject)
ExternalProject_Add(MyExternalLibURL "http://example.com/mylib.tar.gz"CONFIGURE_COMMAND ""BUILD_COMMAND ""INSTALL_COMMAND ""
)
五、高級特性與技巧
5.1 生成器表達式
生成器表達式允許在生成構建系統時進行條件判斷,常用于平臺特定設置:
target_compile_definitions(MyTargetPUBLIC $<$<CONFIG:Debug>:DEBUG_MODE=1>$<$<CXX_COMPILER_ID:GNU>:EXTRA_FEATURE=1>
)
常用生成器表達式:
$<CONFIG:cfg>
:如果配置是 cfg 則為 1$<PLATFORM_ID:platform>
:平臺匹配檢查$<COMPILE_LANGUAGE:lang>
:編譯語言檢查
5.2 自定義命令與目標
# 自定義命令
add_custom_command(OUTPUT generated.cppCOMMAND generator.py ${CMAKE_CURRENT_SOURCE_DIR}/input.txt > generated.cppDEPENDS generator.py input.txt
)# 自定義目標
add_custom_target(GenerateDocs ALLCOMMAND doxygen DoxyfileWORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}COMMENT "Generating documentation"
)
5.3 交叉編譯
設置交叉編譯工具鏈:
# toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)set(CMAKE_FIND_ROOT_PATH /path/to/sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
然后使用:
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake ..
5.4 測試與打包
5.4.1 添加測試
enable_testing()add_test(NAME MyTest1COMMAND MyTestExe --test1WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})add_test(NAME MyTest2COMMAND MyTestExe --test2)
可以使用 CTest 運行測試:
ctest -V # 運行所有測試并顯示詳細輸出
5.4.2 安裝規則
install(TARGETS MyTargetRUNTIME DESTINATION binLIBRARY DESTINATION libARCHIVE DESTINATION lib/static)install(DIRECTORY include/ DESTINATION include)
install(FILES README.md DESTINATION doc)
5.4.3 打包
include(InstallRequiredSystemLibraries)
set(CPACK_PACKAGE_VENDOR "My Company")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)
生成包:
make package # 生成二進制包
make package_source # 生成源碼包
六、現代 CMake 最佳實踐
6.1 目標為中心的設計
現代 CMake 強調以目標(target)為中心的構建方式,每個庫或可執行文件都是一個目標,明確指定其屬性:
add_library(MyLibrary STATIC src/lib.cpp)
target_include_directories(MyLibrary PUBLIC include)
target_compile_features(MyLibrary PUBLIC cxx_std_17)
target_link_libraries(MyLibrary PUBLIC SomeOtherLib)
6.2 作用域控制
正確使用 PRIVATE
、PUBLIC
和 INTERFACE
控制依賴傳遞:
# MyLibrary 的 CMakeLists.txt
target_include_directories(MyLibraryPUBLIC include # 使用者和被使用者都需要PRIVATE src # 僅實現需要INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface # 僅使用者需要
)
6.3 避免全局設置
避免使用全局設置如 include_directories()
和 link_directories()
,而是使用目標特定的命令。
6.4 模塊化設計
將大型項目分解為多個邏輯組件,每個組件有自己的 CMakeLists.txt:
components/
├── core/
│ ├── CMakeLists.txt
│ ├── include/
│ └── src/
└── gui/├── CMakeLists.txt├── include/└── src/
6.5 工具鏈兼容性
編寫可移植的 CMake 腳本:
if(MSVC)target_compile_options(MyTarget PRIVATE /W4 /WX)
else()target_compile_options(MyTarget PRIVATE -Wall -Wextra -pedantic)
endif()
七、常見問題與解決方案
7.1 頭文件找不到
問題:編譯時報告頭文件找不到。
解決方案:
- 使用
target_include_directories()
明確指定包含路徑 - 確保路徑正確,使用絕對路徑或相對于
CMAKE_CURRENT_SOURCE_DIR
的路徑 - 檢查拼寫錯誤
7.2 庫鏈接失敗
問題:鏈接時報告未定義的引用。
解決方案:
- 確保
target_link_libraries()
指定了所有需要的庫 - 檢查庫文件路徑是否正確
- 確保庫的順序正確(被依賴的庫放在后面)
7.3 跨平臺問題
問題:在 Windows 上工作正常,但在 Linux 上失敗。
解決方案:
- 使用
if(WIN32)
、if(UNIX)
等條件語句處理平臺差異 - 避免使用平臺特定的路徑分隔符(總是使用
/
) - 使用
CMAKE_CXX_COMPILER_ID
檢查編譯器
7.4 構建速度慢
問題:大型項目構建時間過長。
解決方案:
- 使用
ccache
緩存編譯結果 - 啟用并行構建:
make -j8
或cmake --build . --parallel 8
- 減少不必要的依賴和包含
7.5 調試 CMake
技巧:
- 使用
message()
打印變量值 - 添加
--trace
或--trace-expand
選項查看詳細執行過程 - 使用
cmake --graphviz=graph.dot
生成依賴圖
八、實戰項目示例
8.1 基礎項目結構
讓我們看一個完整的基礎項目結構:
MyApp/
├── CMakeLists.txt
├── build/
├── include/
│ └── MyApp/
│ ├── utils.h
│ └── config.h
├── src/
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── utils.cpp
└── tests/├── CMakeLists.txt└── test_utils.cpp
頂層 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)# 設置 C++ 標準
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)# 添加子目錄
add_subdirectory(src)
add_subdirectory(tests)
src/CMakeLists.txt:
# 添加庫
add_library(MyLib STATIC utils.cpp)
target_include_directories(MyLib PUBLIC ../include)# 添加可執行文件
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib)
tests/CMakeLists.txt:
# 啟用測試
enable_testing()# 添加測試可執行文件
add_executable(TestUtils test_utils.cpp)
target_link_libraries(TestUtils PRIVATE MyLib)# 添加測試用例
add_test(NAME TestUtils COMMAND TestUtils)
8.2 使用外部依賴的項目
更復雜的項目可能依賴第三方庫:
cmake_minimum_required(VERSION 3.12)
project(AdvancedApp)# 查找依賴
find_package(Boost 1.70 REQUIRED COMPONENTS system filesystem)
find_package(OpenCV REQUIRED)
find_package(Threads REQUIRED)# 添加項目目標
add_executable(AdvancedApp main.cpp)
target_link_libraries(AdvancedApp PRIVATE Boost::system Boost::filesystem OpenCV::OpenCV Threads::Threads
)# 安裝規則
install(TARGETS AdvancedApp DESTINATION bin)
install(FILES config.xml DESTINATION etc/AdvancedApp)
九、總結與進階學習資源
9.1 CMake 學習路徑
-
初學者:
- 掌握基本命令:
project()
,add_executable()
,target_link_libraries()
- 理解變量和作用域
- 學習簡單的項目組織
- 掌握基本命令:
-
中級開發者:
- 掌握現代 CMake 目標為中心的方法
- 學習依賴管理和查找
- 理解生成器表達式
-
高級開發者:
- 掌握交叉編譯和工具鏈文件
- 學習編寫復雜的自定義命令和目標
- 理解 CMake 內部機制和模塊開發
9.2 推薦資源
-
官方文檔:
- CMake 官方文檔
- CMake Tutorial
-
書籍:
- 《Professional CMake: A Practical Guide》
- 《CMake Cookbook》
-
在線教程:
- Modern CMake
- CMake 菜鳥教程
-
開源項目參考:
- 學習知名開源項目(如 KDE、VTK)的 CMake 配置
- 參考 GitHub 上的現代 CMake 模板項目
9.3 結語
CMake 是一個功能強大但學習曲線較陡的工具。掌握 CMake 不僅能提高你的構建系統技能,還能讓你更好地理解軟件項目的組織和管理。從簡單的單文件項目開始,逐步嘗試更復雜的場景,最終你將能夠駕馭任何規模的 CMake 項目。
記住,好的 CMake 腳本應該是:
- 模塊化:易于維護和擴展
- 可移植:能在不同平臺和編譯器上工作
- 高效:最小化不必要的重新構建
- 明確:清晰地表達項目的結構和依賴關系