CMake 学习笔记
CMake 學習筆記
CMake 已經是 C++ 構建系統的事實標準。
主要是對小彭老師的 C++ 視頻課程中 CMake 相關部分的一些筆記和整理,視頻鏈接如下
- 學 C++ 從 CMake 學起
- 現代 CMake 高級教程
包含視頻中的代碼和 PPT 的倉庫見以下鏈接
https://github.com/parallel101/course
本筆記重點關注與 CMake 相關的一些知識點,需要的前置知識為 C++ 本身的頭文件機制、編譯流程、Makefile 的基本認知等內容,所以不會贅述課程中出現的一些很基本的內容。
目錄-
CMake 學習筆記
-
學 C++ 從 CMake 學起
- 基本的 C++ 編譯相關的命令
- CMake 簡介
- 靜態庫和動態庫
- CMake 中的靜態庫和動態庫
- CMake 中的子模塊
- CMake 中其他目標選項
-
第三方庫
- 純頭文件引入
- 子模塊引入
- 引用系統中預安裝的庫
- 包管理器
-
現代 CMake 高級教程
-
命令行小技巧
-B和--build選項-D選項-G選項
- 添加源文件
-
項目變量配置
- 構建模式
CMAKE_BUILD_TYPE project函數相關變量- C++ 標準變量
CMAKE_CXX_STANDARD
- 構建模式
- 標準 C++ 項目模板
-
鏈接庫文件
- 對象庫
add_library的默認參數- 動態庫無法鏈接靜態庫
- Windows 下的動態鏈接庫
- 對象的屬性
-
鏈接第三方庫
- 以鏈接 tbb 為例
- Windows 使用
find_package查找第三方庫 - 鏈接 Qt5
- 可選依賴
- 輸出和變量
-
變量與緩存
- CMake 緩存
- 緩存變量
- 緩存變量類型
- 繞開緩存
-
跨平臺與編譯器
- 宏變量跨平臺
- 編譯器
- 條件與判斷
-
變量與作用域
- 變量的傳播
- 獨立作用域
- 變量的訪問
-
其他小建議
- CCache:編譯加速緩存
- 添加 run 偽目標
- 添加 configure 偽目標
-
命令行小技巧
-
學 C++ 從 CMake 學起
學 C++ 從 CMake 學起
基本的 C++ 編譯相關的命令
編譯單文件為可執行文件
g++/clang++ main.cpp -o main.out
查看 binary 文件的反匯編代碼
objdump -D binary | less
查看 binary 文件的共享庫依賴
ldd binary
CMake 簡介
- 構建系統的構建系統。
- 跨平臺,只需要一份 CMakeLists.txt 文件就可以在不同的平臺上使用相應的構建系統來構建項目。
- 自動檢測源文件和頭文件之間的依賴關系,導出到 Makefile 里。
- 自動檢測編譯器,使用對應的 flag。
靜態庫和動態庫
- 靜態庫相當于直接把代碼插入到生成的可執行文件中,會導致體積變大,但是只需要一個文件即可運行
- 動態庫則只在生成的可執行文件中生成“插樁”函數,當可執行文件被加載時會讀取指定目錄中的 .dll 文件,加載到內存中空閑的位置,并且替換相應的“插樁”指向的地址為加載后的地址,這個過程稱為重定向。這樣以后函數被調用就會跳轉到動態加載的地址去。
CMake 中的靜態庫和動態庫
使用 add_library 生成庫文件
add_library(test STATIC source1.cpp source2.cpp) # 生成靜態庫 libtest.a
add_library(test SHARED source1.cpp source2.cpp) # 生成動態庫 libtest.so
創建庫之后,要在某個可執行文件中使用該庫,只需要:
target_link_libraries(myexec PUBLIC test)
CMake 中的子模塊
在根目錄的 CMakeLists.txt 文件中使用 add_subdirectory 添加子目錄,然后在子目錄中也寫一個 CMakeLists.txt,然后在其中定義庫,所有的子模塊都可以使用這個庫。目錄結構如下
project
├── CMakeLists.txt
├── hellolib
│?? ├── CMakeLists.txt
│?? ├── hello.cpp
│?? └── hello.h
└── main.cpp
其中根目錄下的 CMakeLists.txt 文件如下
cmake_minimum_required(VERSION 3.12)
project(hellocmake LANGUAGES CXX)
add_subdirectory(hellolib)
add_executable(a.out main.cpp)
target_link_libraries(a.out PUBLIC hellolib)
hellolib 子目錄下的 CMakeLists.txt 文件如下
add_library(hellolib STATIC hello.cpp)
target_include_directories(hellolib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) # 添加當前目錄到 hellolib 的頭文件搜索路徑中, PUBLIC 表示傳播給 hellolib 的用戶
其中 target_include_directories 的作用是將當前目錄添加到 hellolib 庫的頭文件搜索路徑中,這樣在 hellolib 庫中的頭文件就可以直接使用 #include "hello.h" 的方式來引用了,甚至可以用 #include <hello.h> 的方式來引用,因為 CMake 會自動將當前目錄添加到 hellolib 庫的頭文件搜索路徑中。
而其中 PUBLIC 的作用是將這個屬性傳播給 hellolib 庫的用戶,這樣 hellolib 的用戶也可以直接使用 #include "hello.h" 的方式來引用了,例如在根目錄下的 main.cpp 中可以直接使用 #include "hello.h" 的方式來引用。
如果不想讓 hellolib 的用戶自動添加這個頭文件搜索路徑,可以使用 PRIVATE 屬性,這樣只有 hellolib 庫內部才能使用 #include "hello.h" 的方式來引用。
CMake 中其他目標選項
target_include_directories(myapp PUBLIC /usr/include/eigen3) # 添加頭文件搜索目錄
target_link_libraries(myapp PUBLIC hellolib) # 添加要鏈接的庫
target_add_definitions(myapp PUBLIC MY_MACRO=1) # 添加一個宏定義
target_add_definitions(myapp PUBLIC -DMY_MACRO=1) # 與 MY_MACRO=1 等價
target_compile_options(myapp PUBLIC -fopenmp) # 添加編譯器命令行選項
target_sources(myapp PUBLIC hello.cpp other.cpp) # 添加要編譯的源文件
現在已經不推薦使用不針對特定目標的命令了,例如 add_definitions、include_directories、link_libraries 等,而是使用 target_xxx 的方式來添加屬性,這樣可以針對特定的目標添加屬性,而不是添加全局屬性。
第三方庫
純頭文件引入
這里是一些好用的 header-only 庫:
- nothings/stb - 大名鼎鼎的 stb_image 系列,涵蓋圖像,聲音,字體等,只需單頭文件!
- Neargye/magic_enum - 枚舉類型的反射,如枚舉轉字符串等(實現方式很巧妙)
- g-truc/glm - 模仿 GLSL 語法的數學矢量/矩陣庫(附帶一些常用函數,隨機數生成等)
- Tencent/rapidjson - 單純的 JSON 庫,甚至沒依賴 STL(可定制性高,工程美學經典)
- ericniebler/range-v3 - C++20 ranges 庫就是受到他啟發(完全是頭文件組成)
- fmtlib/fmt - 格式化庫,提供 std::format 的替代品(需要 -DFMT_HEADER_ONLY)
- gabime/spdlog - 能適配控制臺,安卓等多后端的日志庫(和 fmt 沖突!)
優點:簡單方便,只需要把他們的 include 目錄或頭文件下載下來,然后 include_directories(spdlog/include) 即可。
缺點:函數直接實現在頭文件里,沒有提前編譯,從而需要重復編譯同樣內容,編譯時間長。
子模塊引入
直接把相應的庫放到工程的根目錄,然后 add_subdirectory 即可。
這些庫能夠很好地支持作為子模塊引入:
- fmtlib/fmt - 格式化庫,提供 std::format 的替代品
- gabime/spdlog - 能適配控制臺,安卓等多后端的日志庫
- ericniebler/range-v3 - C++20 ranges 庫就是受到他啟發
- g-truc/glm - 模仿 GLSL 語法的數學矢量/矩陣庫
- abseil/abseil-cpp - 旨在補充標準庫沒有的常用功能
- bombela/backward-cpp - 實現了 C++ 的堆棧回溯便于調試
- google/googletest - 谷歌單元測試框架
- google/benchmark - 谷歌性能評估框架
- glfw/glfw - OpenGL 窗口和上下文管理
- libigl/libigl - 各種圖形學算法大合集
引用系統中預安裝的庫
可以通過 find_package 命令尋找系統中的包/庫:
find_package(fmt REQUIRED)
target_link_libraries(myexec PUBLIC fmt::fmt)
值得注意的是,這里不是簡單的 fmt 而是 fmt::fmt,這是因為現代 CMake 認為一個包 (package) 可以提供多個庫,又稱組件 (components),比如 TBB 這個包,就包含了 tbb, tbbmalloc, tbbmalloc_proxy 這三個組件。
可以指定使用哪些組件,如下
find_package(TBB REQUIRED COMPONENTS tbb tbbmalloc REQUIRED)
target_link_libraries(myexec PUBLIC TBB::tbb TBB::tbbmalloc)
包管理器
Linux 可以用系統自帶的包管理器(如 apt)安裝 C++ 包,例如
sudo apt install libfmt-dev
Windows 則沒有自帶的包管理器。因此可以用跨平臺的 vcpkg:https://github.com/microsoft/vcpkg
cd vcpkg
.\bootstrap-vcpkg.bat
.\vcpkg integrate install
.\vcpkg install fmt:x64-windows
cd ..
cmake -B build -DCMAKE_TOOLCHAIN_FILE="%CD%/vcpkg/scripts/buildsystems/vcpkg.cmake"
現代 CMake 高級教程
現代 CMake 主要指 3.x 版本的 CMake,提供了更加方便的命令行指令和更加清晰簡潔的語法。
命令行小技巧
-B 和 --build 選項
現代 CMake 提供了跨平臺的 -B 和 --build 指令,更加方便好用。
cmake -B build
cmake --build build -j4
cmake --build build --target install
CMake 項目的構建分為兩步,
- 第一步是
cmake -B build,稱為配置階段(configure),這時只檢測環境并生成構建規則
會在 build 目錄下生成本地構建系統能識別的項目文件(Makefile 或是 .sln) - 第二步是
cmake --build build,稱為構建階段(build),這時才實際調用編譯器來編譯代碼
-D 選項
在配置階段可以通過 -D 設置緩存變量。第二次配置時,之前的 -D 添加仍然會被保留。
# 設置安裝路徑為 /opt/openvdb-8.0(會安裝到 /opt/openvdb-8.0/lib/libopenvdb.so)
cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/openvdb-8.0
# 設置構建模式為發布模式(開啟全部優化)
cmake -B build -DCMAKE_BUILD_TYPE=Release
# 第二次配置時沒有 -D 參數,但是之前的 -D 設置的變量都會被保留
#(此時緩存 CMakeCache.txt 文件里仍有之前定義的 CMAKE_BUILD_TYPE 和 CMAKE_INSTALL_PREFIX)
cmake -B build
-G 選項
Linux 系統上的 CMake 默認用是 Unix Makefiles 生成器;Windows 系統默認是 Visual Studio 2019 生成器;MacOS 系統默認是 Xcode 生成器。
可以用 -G 參數改用別的生成器,例如 cmake -G Ninja 會生成 Ninja 這個構建系統的構建規則。
Ninja 是一個高性能,跨平臺的構建系統,Linux、Windows、MacOS 上都可以用。
Ninja 是專為性能優化的構建系統(比 MSbuild 和 Makefile 效率要高),他和 CMake 結合都是行業標準了,推薦使用。
添加源文件
-
直接在
add_executable中添加源文件add_executable(myapp main.cpp hello.cpp) -
先創建目標,然后添加源文件
add_executable(myapp) target_sources(myapp PUBLIC main.cpp hello.cpp) -
使用 GLOB 自動查找源文件
file(GLOB sources *.cpp *.h) add_executable(myapp ${sources})這種方法有一些缺點,比如如果添加了新的源文件,需要重新運行 CMake 才能生效。
可以使用 CONFIGURE_DEPENDS 選項來解決這個問題,但是這個選項只有在 CMake 3.12 以上才支持。file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h) add_executable(myapp ${sources})另外,現在這樣沒法遞歸搜索子目錄,如果要遞歸搜索子目錄,可以使用
file(GLOB_RECURSE)命令。file(GLOB_RECURSE sources CONFIGURE_DEPENDS *.cpp *.h) add_executable(myapp ${sources}) -
使用
aux_source_directory命令,可以自動搜集需要的文件后綴名aux_source_directory(. sources) add_executable(myapp ${sources})這種方法和 GLOB 類似,也有一些缺點,比如如果添加了新的源文件,需要重新運行 CMake 才能生效。
另外,這種方法也沒法遞歸搜索子目錄,需要自己指定子目錄
aux_source_directory(. sources) aux_source_directory(./subdir sources) add_executable(myapp ${sources})
值得注意的一點是 .h 文件不添加到源文件中仍然可以正常編譯運行,但是添加之后可以使得 IDE 中可以顯示文件,因此建議是將 .h 文件也添加到源文件中。
另外 GLOB_RECURSE 會把 build 目錄里生成的臨時 .cpp 文件也加進來,一般建議將源碼統一放到 src 目錄中。
項目變量配置
構建模式 CMAKE_BUILD_TYPE
Release、Debug、MinSizeRel、RelWithDebInfo 是 CMake 內置的構建模式,可以通過 cmake -B build -DCMAKE_BUILD_TYPE=Release 來指定構建模式,其具體含義如下
| Build Mode | Compiler Flags | Meaning |
|---|---|---|
| Release | -O3 -DNDEBUG | Optimized, no debug |
| Debug | -O0 -g | Debug symbols enabled |
| MinSizeRel | -Os -DNDEBUG | Optimized, minimal size |
| RelWithDebInfo | -O2 -g -DNDEBUG | Optimized, with debug symbols |
NDEBUG 宏會移除代碼中的 assert 語句。
CMake 中 CMAKE_BUILD_TYPE 的默認值為 "",即不指定構建模式,這時默認使用 Debug 模式。如果想設置默認模式為 Release 模式,可以在 CMakeLists.txt 中添加如下代碼
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
project 函數相關變量
CMake 中的 project 函數會定義一些變量,這些變量可以在 CMakeLists.txt 中使用,也可以在 C++ 代碼中使用。
project(myproject VERSION 1.0.0 LANGUAGES CXX)
message("PROJECT_NAME: ${PROJECT_NAME}")
message("PROJECT_VERSION: ${PROJECT_VERSION}")
message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message("CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message("myproject_SOURCE_DIR: ${myproject_SOURCE_DIR}")
message("myproject_SOURCE_DIR: ${${PROJECT_NAME}_SOURCE_DIR}") # 與上面一樣,展示了嵌套 $ 功能
PROJECT_SOURCE_DIR 代表最近一次調用 project 語句的 CMakeLists.txt 的目錄
CMAKE_CURRENT_SOURCE_DIR 代表當前 CMakelists.txt 所在的目錄。
CMAKE_SOURCE_DIR 代表頂層 CMakeLists.txt 所在的目錄。(不建議使用,無法作為子項目使用)
更多變量和內容可以查看 project 語句的官方文檔
https://cmake.org/cmake/help/latest/command/project.html
C++ 標準變量 CMAKE_CXX_STANDARD
set(CMAKE_CXX_STANDARD 17) # 設置 C++ 標準為 C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 必須使用指定的標準
set(CMAKE_CXX_EXTENSIONS ON) # 啟用 GCC 特有的一些擴展功能
project(myproject VERSION 1.0.0 LANGUAGES CXX)
上面幾條命令比較容易理解,值得注意的一點是 project 函數放在設置之后,這樣 CMake 可以在 project 函數里對編譯器進行一些檢測,看看他能不能支持 C++17 的特性。
標準 C++ 項目模板
cmake_minimum_required(VERSION 3.15)
set(CMake_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project(myproject LANGUAGES CXX)
if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR)
message(WARNING "In-source build detected! Please build out-of-source!")
endif()
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
if (WIN32)
add_definitions(-DNOMINMAX -D_USE_MATH_DEFINES)
endif()
if (NOT MSVC)
find_program(CCACHE_PROGRAM ccache)
if (CCACHE_PROGRAM)
message(STATUS "Found ccache: ${CCACHE_PROGRAM}")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CCACHE_PROGRAM}")
endif()
endif()
鏈接庫文件
對象庫
參考鏈接https://www.scivision.dev/cmake-object-libraries
對象庫是 CMake 自創的,繞開了編譯器和操作系統的各種繁瑣規則,保證了跨平臺統一性,類似于靜態庫,但不生成 .a 文件,只由 CMake 本身記住該庫生成了哪些對象文件。在自己的項目中,推薦全部用對象庫來組織代碼。
add_library(mylib OBJECT lib.cpp)
add_executable(myapp main.cpp $<TARGET_OBJECTS:mylib>)
另外使用靜態庫時 GCC 編譯器會自動剔除沒有引用符號的對象,如果使用靜態庫下面的程序會僅輸出 world 而沒有 hello,對象庫則不會有這種問題。
// lib.cpp
#include <cstdio>
static int unused = printf("hello\n");
// main.cpp
#include <cstdio>
int main() {
printf("world\n");
}
add_library 的默認參數
當不填寫 add_library 的靜態庫/動態庫參數時,CMake 會根據 BUILD_SHARED_LIBS 變量來決定是生成靜態庫還是動態庫,未指定 BUILD_SHARED_LIBS 時默認生成靜態庫。
可以通過命令行參數或者 CMake 語句來指定 BUILD_SHARED_LIBS 變量:
cmake -B build -DBUILD_SHARED_LIBS:BOOL=ON
if (NOT DEFINED BUILD_SHARED_LIBS)
set(BUILD_SHARED_LIBS OFF)
endif()
動態庫無法鏈接靜態庫
當我們要編譯一個 so 提供給外部使用,這個 so 本身依賴一些第三方庫。但是我們卻希望 so 的使用者不用關心該 so 對其他庫的依賴。很自然的是會想到在編譯 so 的時候把依賴到的第三方庫靜態鏈接進來。
然而靜態庫中的代碼位置都是確定的,而動態庫中的代碼位置是不確定的,因此動態庫無法鏈接靜態庫。通過將靜態庫也編譯成位置無關的代碼(Position Independent Code,PIC),就可以解決這個問題,實現這一點有兩種方式,一種是設置全局變量,另一種是設置目標變量。
# 設置全局變量
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
add_library(mylib STATIC lib.cpp)
add_library(mylib_shared SHARED lib.cpp)
target_link_libraries(mylib_shared PRIVATE mylib)
# 設置目標變量
add_library(mylib STATIC lib.cpp)
set_target_properties(mylib_shared PROPERTIES POSITION_INDEPENDENT_CODE ON)
add_library(mylib_shared SHARED lib.cpp)
target_link_libraries(mylib_shared PRIVATE mylib)
Windows 下的動態鏈接庫
需要在需要導出的內容前面加上 __declspec(dllexport),在需要導入的內容前面加上 __declspec(dllimport)。
// lib.h
# pragma once
#ifdef _MSC_VER
#ifdef BUILD_SHARED_LIBS
#define LIB_API __declspec(dllexport)
#else
#define LIB_API __declspec(dllimport)
#endif
#else
#define LIB_API
#endif
LIB_API void hello();
// lib.cpp
#include "lib.h"
#include <cstdio>
LIB_API void hello() { printf("hello\n"); }
另外由于 Windows 不支持 RPATH,因此動態庫的 dll 文件需要放在可執行文件的同一目錄下,或者放在系統目錄下,在子模塊的情況下就需要將子模塊的輸出目錄設置為可執行文件的同一目錄下。
add_library(mylib SHARED lib.cpp)
set_target_properties(TARGET mylib PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES LIBRARY_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES LIBRARY_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
set_target_properties(TARGET mylib PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
對象的屬性
set_property 命令可以設置對象的屬性,例如
add_executable(myapp main.cpp)
set_property(TARGET myapp PROPERTY CXX_STANDARD 17) # 采用 C++ 標準為 C++17 (默認 11)
set_property(TARGET myapp PROPERTY CXX_STANDARD_REQUIRED ON) # 必須使用指定的標準 (默認 OFF)
set_property(TARGET myapp PROPERTY WIN32_EXECUTABLE ON) # 生成 Windows 窗口程序,不啟動控制臺,只有 GUI 界面(默認 OFF)
set_property(TARGET myapp PROPERTY LINK_WHAT_YOU_USE ON) # 告訴編譯器不要自動剔除沒有使用的符號(默認 OFF)
set_property(TARGET myapp PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 設置動態庫文件輸出目錄
set_property(TARGET myapp PROPERTY ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 設置靜態庫文件輸出目錄
set_property(TARGET myapp PROPERTY RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # 設置可執行文件輸出目錄
也可以使用 set_target_properties 命令來一次設置多條對象的屬性,例如
add_executable(myapp main.cpp)
set_target_properties(myapp PROPERTIES
CXX_STANDARD 17 # 采用 C++ 標準為 C++17 (默認 11)
CXX_STANDARD_REQUIRED ON # 必須使用指定的標準 (默認 OFF)
WIN32_EXECUTABLE ON # 生成 Windows 窗口程序,不啟動控制臺,只有 GUI 界面(默認 OFF)
LINK_WHAT_YOU_USE ON # 告訴編譯器不要自動剔除沒有使用的符號(默認 OFF)
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib # 設置動態庫文件輸出目錄
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib # 設置靜態庫文件輸出目錄
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin # 設置可執行文件輸出目錄
)
也可以設置全局的屬性
set(CMAKE_CXX_STANDARD 17) # 設置 C++ 標準為 C++17 (默認 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 必須使用指定的標準 (默認 OFF)
set(CMAKE_WIN32_EXECUTABLE ON) # 生成 Windows 窗口程序,不啟動控制臺,只有 GUI 界面(默認 OFF)
set(CMAKE_LINK_WHAT_YOU_USE ON) # 告訴編譯器不要自動剔除沒有使用的符號(默認 OFF)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 設置動態庫文件輸出目錄
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 設置靜態庫文件輸出目錄
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # 設置可執行文件輸出目錄
add_executable(myapp main.cpp)
鏈接第三方庫
以鏈接 tbb 為例
例如需要鏈接 tbb 庫,直接鏈接的話 CMake 會在系統的庫目錄中查找 tbb,但是在 Windows 上沒有系統的庫目錄,因此需要指定 tbb 的位置,而且頭文件也需要指定,非常麻煩。
有一個優雅的做法是使用現代的 find_package 命令,這個命令會查找 /usr/lib/cmake/TBB/TBBConfig.cmake 這個配置文件(地址不一定一樣,大概就是查找庫的文件的目錄),并根據里面的配置信息創建 TBB::tbb 這個偽對象(他實際指向真正的 tbb 庫文件路徑 /usr/lib/libtbb.so)。
find_package(TBB CONFIG REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE TBB::tbb)
其中 CONFIG 的作用是只查找 TBBConfig.cmake 這個配置文件,不查找 FindTBB.cmake 這個腳本文件(項目作者常把他塞在 cmake/ 目錄里并添加到 CMAKE_MODULE_PATH),建議加,加上能保證項目尋找包的 .cmake 腳本與系統自帶的版本是適配的,而不是項目作者當年下載的版本的 .cmake 腳本。
- 不加 CONFIG:會先查找 TBBConfig.cmake,如果找不到再查找 FindTBB.cmake
- 加了 CONFIG:只查找 TBBConfig.cmake,不查找 FindTBB.cmake
這個配置文件一般由庫的作者提供,通過包管理器安裝包后也會自動安裝這個文件,其內容大概如下
# Create imported target TBB::tbb
add_library(TBB::tbb SHARED IMPORTED)
set_target_properties(TBB::tbb PROPERTIES
INTERFACE_COMPILE_DEFINITIONS "\$<\$<CONFIG:DEBUG>:TBB_USE_DEBUG>"
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
# Create imported target TBB::tbbmalloc
add_library(TBB::tbbmalloc SHARED IMPORTED)
set_target_properties(TBB::tbbmalloc PROPERTIES
INTERFACE_COMPILE_DEFINITIONS "\$<\$<CONFIG:DEBUG>:TBB_USE_DEBUG>"
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
# Create imported target TBB::tbbmalloc_proxy
add_library(TBB::tbbmalloc_proxy SHARED IMPORTED)
set_target_properties(TBB::tbbmalloc_proxy PROPERTIES
INTERFACE_COMPILE_DEFINITIONS "\$<\$<CONFIG:DEBUG>:TBB_USE_DEBUG>"
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
Windows 使用 find_package 查找第三方庫
Windows 上并沒有庫文件目錄,因此需要手動指定 TBBConfig.cmake 文件的位置,有許多方式可以做到這一點,例如設置 CMAKE_MODULE_PATH 變量,也可以設置 TBB_DIR 變量,可以在 CMakeLists.txt 中設置,也可以在命令行中設置(推薦,因為是用戶相關的內容,每個人的安裝路徑都不一樣),例如
cmake -B build -DTBB_DIR="C:/Program Files/TBB/cmake"
鏈接 Qt5
直接用以下方式鏈接 Qt5 會報錯
find_package(Qt5 REQUIRED)
add_executable(myapp main.cpp)
報錯信息為
CMake Error at CMakeLists.txt:6 (find_package):
Found package configuration file:
/usr/lib/cmake/Qt5/Qt5Config.cmake
but it set Qt5_FOUND to FALSE so package "Qt5" is considered to be NOT
FOUND. Reason given by package:
The Qt5 package requires at least one component
原因是 Qt5 具有多個組件,需要指定鏈接哪些組件,find_package 生成的偽對象 (imported target) 都按照“包名:: 組件名”的格式命名。例如
find_package(Qt5 COMPONENTS Widgets Gui REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE Qt5::Widgets Qt5::Gui)
可選依賴
如果希望某個庫是可選的,可以不使用 find_package 的 REQUIRED 參數,然后如下定義相應的宏
find_package(TBB CONFIG)
if (TBB_FOUND) # 也可以用 if (TARGET TBB::tbb)
message(STATUS "TBB found")
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE TBB::tbb)
target_compile_definitions(myapp PRIVATE WITH_TBB)
else()
message(WARNING "TBB not found! using serial version")
于是在 cpp 文件中可以這樣寫
#include <cstdio>
#ifdef WITH_TBB
#include <tbb/parallel_for.h>
#endif
int main() {
#ifdef WITH_TBB
tbb::parallel_for(0, 100, [](int i) { printf("%d\n", i); });
#else
for (int i = 0; i < 100; i++) printf("%d\n", i);
#endif
輸出和變量
message 命令可以輸出信息,例如
message(STATUS "STATUS means status info with -- prefix")
message(WARNING "WARNING means warning info")
message(AUTHOR_WARNING "AUTHOR_WARNING is only useful for project authors and can be closed by -Wno-dev")
message(SEND_ERROR "SEND_ERROR means error info and continue to run")
message(FATAL_ERROR "FATAL_ERROR means error info and stop running")
message 也可以打印變量的值,例如
set(myvar "hello world")
message(STATUS "myvar = ${myvar}") # result is "myvar = hello world"
set(myvar hello world)
message(STATUS "myvar = ${myvar}") # result is "myvar = hello;world"
變量與緩存
CMake 緩存
CMake 會自動將一些編譯器和 C++ 特性等內容檢測完后緩存到 CMakeCache.txt 文件中,這樣下次運行 CMake 時就不用再檢測了,直接讀取緩存文件即可,這樣可以加快 CMake 的運行速度。
前面用到的 find_package 命令就是一個例子,他會將檢測到的庫的路徑緩存到 CMakeCache.txt 文件中,這樣下次運行 CMake 時就不用再檢測了。
緩存雖好,但是很多時候情況有變需要更新緩存,會導致很多 CMake 出錯的情況,這時很多人會經典的刪 build 大法,但是這樣需要完全重新構建,非常耗時,可以嘗試只刪除 CMakeCache.txt 文件,然后重新運行 CMake -B build,這樣就可以更新緩存了。
緩存變量
可以用如下方式設置緩存變量
set(myvar "hello world" CACHE STRING "this is the docstring of myvar") # The last string is a docstring
message(STATUS "myvar = ${myvar}") # result is "myvar = hello world"
這里有一個坑點,更新 CMakeCache.txt 文件后,CMakeLists.txt 中的變量并不會自動更新,需要重新運行 CMake 才會更新,這時可以使用 FORCE 參數來強制更新變量
sett(myvar "hello world" CACHE STRING "this is the docstring of myvar" FORCE) # The last string is a docstring
message(STATUS "myvar = ${myvar}") # result is "myvar = hello world"
其實也可以通過命令行參數來更新
cmake -B build -Dmyvar="goodbye world"
另外還可以通過圖形界面來編輯緩存變量,Linux 上可以使用 ccmake 命令,Windows 上可以使用 cmake-gui 命令。
最后還可以通過直接編輯 CMakeCache.txt 文件來更新緩存變量,該文件被設置為文本文件就是可供用戶手工編輯或者被第三方軟件打開并解析的。
緩存變量類型
緩存變量由如下類型
- STRING 字符串,例如 “hello, world”
- FILEPATH 文件路徑,例如 “C:/vcpkg/scripts/buildsystems/vcpkg.cmake”
- PATH 目錄路徑,例如 “C:/Qt/Qt5.14.2/msvc2019_64/lib/cmake/”
- BOOL 布爾值,只有兩個取值:ON 或 OFF。
注意:TRUE 和 ON 等價,FALSE 和 OFF 等價;YES 和 ON 等價,NO 和 OFF 等價
CMake 對 BOOL 類型的緩存變量的 set 指令提供了一個簡寫 option,例如
add_executable(myapp main.cpp)
option(WITH_TBB "set to ON to enable TBB, OFF disale TBB" ON)
if (WITH_TBB)
find_package(TBB CONFIG REQUIRED)
target_link_libraries(myapp PRIVATE TBB::tbb)
target_compile_definitions(myapp PRIVATE WITH_TBB)
endif()
跟前面一樣,容易犯的一個經典錯誤就是直接改 CMakeLists.txt 文件,然后重新運行 CMake,這樣是不會更新緩存變量的。官方推薦做法是使用 -DTBB:BOOL=ON/OFF 命令行參數來更新緩存變量。
繞開緩存
既然緩存有這么多坑,那么我們就可以繞開緩存,直接使用帶普通變量的默認值來達到相同的效果,例如
if (NOT DEFINED WITH_TBB)
set(WITH_TBB ON)
endif()
一般來講,CMake 自帶的變量都會用這個方式來設置,但是就是會在 ccmake 中看不到。
跨平臺與編譯器
宏變量跨平臺
可以根據操作系統不同定義不同的宏變量,例如
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_compile_definitions(main PUBLIC MY_NAME="Bill Gates")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_compile_definitions(main PUBLIC MY_NAME="Linus Torvalds")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_compile_definitions(main PUBLIC MY_NAME="Steve Jobs")
endif()
這樣代碼中就可以
#include <cstdio>
int main() {
#ifdef MY_NAME
printf("Hello, %s!\n", MY_NAME);
#else
printf("Hello, world!\n");
#endif
}
前面的 CMake 代碼使用簡寫變量可讀性會稍微強一點
if (WIN32)
target_compile_definitions(main PUBLIC MY_NAME="Bill Gates")
elseif (UNIX AND NOT APPLE)
target_compile_definitions(main PUBLIC MY_NAME="Linus Torvalds")
elseif (APPLE)
target_compile_definitions(main PUBLIC MY_NAME="Steve Jobs")
endif()
還可以使用 生成器表達式 簡化為一條語句
target_compile_definitions(main PUBLIC
$<$<PLATFORM_ID:Windows>:MY_NAME="Bill Gates">
$<$<PLATFORM_ID:Linux>:MY_NAME="Linus Torvalds">
$<$<PLATFORM_ID:Darwin>:MY_NAME="Steve Jobs">)
# using comma to separate
target_compile_definitions(main PUBLIC
$<$<PLATFORM_ID:Windows>:MY_NAME="DOS-like">,
$<$<PLATFORM_ID:Linux,Darwin,FreeBSD>:MY_NAME="Unix-like">
編譯器
判斷所使用的編譯器,如下
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message(STATUS "using gcc")
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
message(STATUS "using MSVC")
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(STATUS "using clang")
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "NVIDIA")
message(STATUS "using nvcc")
endif()
從命令行指定編譯器
cmake -B build -DCMAKE_CXX_COMPILER="/usr/bin/clang++"
也可以通過環境變量指定編譯器
export CC=/usr/bin/clang
export CXX=/usr/bin/clang++
cmake -B build
條件與判斷
if 語句中的變量會自動展開,無需添加 ${},例如
set(myvar "hello world")
if (myvar STREQUAL "hello world")
message(STATUS "myvar is hello world")
endif()
如果要加 ${},則需要用引號括起來(否則因為其特殊的規則會有一些其奇奇怪怪的問題),例如
set(myvar "hello world")
if ("${myvar}" STREQUAL "hello world")
message(STATUS "myvar is hello world")
endif()
變量與作用域
變量的傳播
變量的傳播規則:父會傳遞給子,子不會傳遞給父,兄弟之間不會傳遞。
如果子模塊需要向父模塊傳遞變量,可以使用 set 命令的 PARENT_SCOPE 參數,例如
set(myvar "hello world" PARENT_SCOPE)
緩存變量全局可見,會傳播到整個項目中。
獨立作用域
- include 的 XXX.cmake 沒有獨立作用域
- add_subdirectory 的 CMakeLists.txt 有獨立作用域
- macro 沒有獨立作用域
- function 有獨立作用域(因此 PARENT_SCORE 也可以用于 function 的返回值)
更多內容可以參考以下鏈接
https://cmake.org/cmake/help/v3.16/command/set.html
https://blog.csdn.net/Calvin_zhou/article/details/104060927
變量的訪問
用 ${xx} 訪問的是局部變量,局部變量服從剛剛所說的父子模塊傳播規則。
用 $ENV{xx} 訪問的是環境變量,環境變量是全局的,不服從父子模塊傳播規則。
message(STATUS "PATH = $ENV{PATH}")
用 $CACHE{xx} 訪問的是緩存變量,緩存變量是全局的,不服從父子模塊傳播規則。
message(STATUS "CMAKE_CXX_COMPILER_ID = $CACHE{CMAKE_CXX_COMPILER_ID}")
${XX} 找不到局部變量時會去找緩存變量,如果緩存變量也找不到,就會報錯。
if (DEFINED myvar) 會判斷 myvar 是否被定義,而 if (myvar) 會判斷 myvar 的值是否為空,前者即使 myvar 的值為空也會返回 true。
if (DEFINED ENV{XX}) 判斷環境變量是否被定義,if (DEFINED CACHE{XX}) 判斷緩存變量是否被定義。
其他小建議
CCache:編譯加速緩存
CCache 官方網站:https://ccache.dev/
將所有的編譯鏈接指令前加上 ccache 即可,例如
cmake_minimum_required(VERSION 3.15)
project(hellocmake)
find_program(CCACHE_PROGRAM ccache)
if (CCACHE_PROGRAM)
message(STATUS "Found ccache: ${CCACHE_PROGRAM}")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CCACHE_PROGRAM}")
endif()
添加 run 偽目標
創建一個 run 偽目標,執行 main 的可執行文件,例如
add_executable(main main.cpp)
add_custom_target(run
COMMAND $<TARGET_FILE:main>
WORKING_DIRECTORY ${CMAKE_PROJECT_DIR}
)
然后就可以通過以下命令運行程序了
cmake --build build --target run
其最大的好處是跨平臺,不用考慮在 Windows 上使用 build\main.exe 還是在 Linux 上使用 build/main,CMake 會自動處理這個問題。
添加 configure 偽目標
創建一個 configure 偽目標,可視化修改緩存,好處同樣是跨平臺。
if (CMAKE_EDIT_COMMAND)
add_custom_target(configure
COMMAND ${CMAKE_EDIT_COMMAND}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
endif()
總結
以上是生活随笔為你收集整理的CMake 学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: B612咔叽怎么开启镜像模式
- 下一篇: 《最终幻想 14》国际服免费试玩版内容扩