CMake 學習記錄


小記錄

參考:
https://www.zhihu.com/search?type=content&q=premake
https://www.zhihu.com/question/58949190/answer/999701073

口訣:

  1. Declare a target
  2. Declare target's traits
  3. It's all about targets

image
add_definitions 已經被 add_compile_definitions 取代

Windows下是 .dll 和 .lib,Linux下是 .so 和 .a

cmake區分大小寫,但是cmake指令不分(比如set和SET)還有函數啥的也不分

cmake中永遠用正斜杠 / ,cmake會自動將反斜杠轉成正斜杠

image

學習資料

cmake-examples中文版:https://sfumecjf.github.io/cmake-examples-Chinese/
cmake-examples:https://github.com/ttroy50/cmake-examples
CMake “菜譜”:https://www.bookstack.cn/read/CMake-Cookbook/README.md
CMake 官方文檔:https://cmake.org/cmake/help/latest/
CMake 中文文檔:https://runebook.dev/zh-CN/docs/cmake/-index-
高性能並行編程與優化 - 課件:https://github.com/parallel101/course
More Modern CMake:
https://hsf-training.github.io/hsf-training-cmake-webpage/aio/index.html
https://github.com/hsf-training/hsf-training-cmake-webpage/
Modern CMake:https://cliutils.gitlab.io/modern-cmake/chapters/basics/structure.html

configuration stage / build stage

https://cmake.org/cmake/help/v3.25/manual/cmake.1.html#generate-a-project-buildsystem

cmake-examples

https://sfumecjf.github.io/cmake-examples-Chinese/
https://github.com/ttroy50/cmake-examples

1. basic

1.1 hello-cmake

最基礎的當然是

cmake_minimum_required(VERSION 3.5) #設置CMake最小版本
project (hello_cmake) #設置工程名
add_executable(hello_cmake main.cpp) #生成可執行文件

細節就是,在這一行語句中project (hello_cmake) #設置工程名
CMake構建包含一個項目名稱,上面的命令會自動生成一些變量,在使用多個項目時引用某些變量會更加容易。
比如生成了: PROJECT_NAME 這個變量。PROJECT_NAME是變量名,${PROJECT_NAME}是變量值,值為hello_cmake

因此上面的代碼和下面的一樣:

cmake_minimum_required(VERSION 3.5) #設置CMake最小版本
project (hello_cmake) #設置工程名
add_executable(${PROJECT_NAME} main.cpp) #生成可執行文件

1.2 頭文件

image
對於頭文件的情況,我們這樣寫:

cmake_minimum_required(VERSION 3.5)#最低CMake版本

project (hello_headers)# 工程名

set(SOURCES
    src/Hello.cpp
    src/main.cpp
)#創建一個變量,名字叫SOURCE。它包含了所有的cpp文件。

add_executable(hello_headers ${SOURCES})#用所有的源文件生成一個可執行文件,因為這里定義了SOURCE變量,所以就不需要羅列cpp文件了
#等價於命令:     add_executable(hello_headers src/Hello.cpp src/main.cpp)

target_include_directories(hello_headers
    PRIVATE 
        ${PROJECT_SOURCE_DIR}/include
)#設置這個可執行文件hello_headers需要包含的庫的路徑


#PROJECT_SOURCE_DIR指工程頂層目錄
#PROJECT_Binary_DIR指編譯目錄
#PRIVATE指定了庫的范圍,下一節講

我們可以用 message 函數來查看,比如我在上面的最后一行加一句:message(${CMAKE_SOURCE_DIR})

我們在 cmake .. 的時候會打印輸出:
image
我們在 make 的時候可以這樣來詳細列出(否則僅顯示構建狀態):make VERBOSE=1

1.3 Static Library

將源文件直接傳遞給add_library調用,這是modern CMake的建議。(而不是先把Hello.cpp賦給一個變量)
image

cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
#庫的源文件Hello.cpp生成靜態庫hello_library
add_library(hello_library STATIC 
    src/Hello.cpp
)
target_include_directories(hello_library
    PUBLIC 
        ${PROJECT_SOURCE_DIR}/include
)
# target_include_directories為一個目標(可能是一個庫library也可能是可執行文件)添加頭文件路徑。
############################################################
# Create an executable
############################################################
# Add an executable with the above sources
#指定用哪個源文件生成可執行文件
add_executable(hello_binary 
    src/main.cpp
)
#鏈接可執行文件和靜態庫
target_link_libraries( hello_binary
    PRIVATE 
        hello_library
)
#鏈接庫和包含頭文件都有關於scope這三個關鍵字的用法。

關於 private pubic interface 的范圍

官方文檔講解:https://cmake.org/cmake/help/v3.0/command/target_include_directories.html

  • PRIVATE - 目錄被添加到目標(庫)的包含路徑中。
  • INTERFACE - 目錄沒有被添加到目標(庫)的包含路徑中,而是鏈接了這個庫的其他目標(庫或者可執行程序)包含路徑中
  • PUBLIC - 目錄既被添加到目標(庫)的包含路徑中,同時添加到了鏈接了這個庫的其他目標(庫或者可執行程序)的包含路徑中

也就是說,根據庫是否包含這個路徑,以及調用了這個庫的其他目標是否包含這個路徑,可以分為三種scope。

拿剛才的項目為例,結構如下:
image
那么對於我們的 CMake 命令:

target_include_directories(hello_library
    PUBLIC 
        ${PROJECT_SOURCE_DIR}/include
)

我們這里 Hello.cpp 和 main.cpp 都是這樣包含頭文件的:#include "static/Hello.h"

而這里我們要加入 ${PROJECT_SOURCE_DIR}/include,為 PUBLIC 的時候,就會是正確的;為 PRIVATE 的時候,那么此時只有靜態庫能找到這個路徑,對於鏈接這個靜態庫的最終可執行文件而言是沒有添加這個路徑的,也就是 main.cpp 沒有添加這個路徑,所以在 main.cpp 中會報錯
image
而若是換成 INTERFACE ,那么就只有要鏈接這個靜態庫的東西添加了這個路徑,因此會在 Hello.cpp 中報錯:
image
同樣,在代碼:

target_link_libraries( hello_binary
    PRIVATE 
        hello_library
)

中,若是把 PRIVATE 換成 INTERFACE 也會在 main.cpp 報錯,因為 INTERFACE 會讓 main.cpp 找不到這個路徑。(這告訴CMake在鏈接期間將hello_library鏈接到hello_binary可執行文件。 同時,這個被鏈接的庫如果有INTERFACE或者PUBLIC屬性的包含目錄,那么,這個包含目錄也會被傳遞( propagate )給這個可執行文件。)

這里對於hello_binary,它不是庫,所以不會被鏈接。直接private自己用這個庫就行。

建議:

對於公共的頭文件,最好在include文件夾下建立子目錄。

傳遞給函數target_include_directories()的目錄,應該是所有包含目錄的根目錄,然后在這個根目錄下建立不同的文件夾,分別寫頭文件。

這樣使用的時候,不需要寫${PROJECT_SOURCE_DIR}/include,而是直接選擇對應的文件夾里對應頭文件。下面是例子:#include "static/Hello.h"而不是#include "Hello.h"使用此方法意味着在項目中使用多個庫時,頭文件名沖突的可能性較小。

1.4 Shared Library

image
CMakeLists:

cmake_minimum_required(VERSION 3.5)
project(hello_library)
############################################################
# Create a library
############################################################
#根據Hello.cpp生成動態庫
add_library(hello_library SHARED 
    src/Hello.cpp
)
#給動態庫hello_library起一個別的名字hello::library
add_library(hello::library ALIAS hello_library)
#為這個庫目標,添加頭文件路徑,PUBLIC表示包含了這個庫的目標也會包含這個路徑
target_include_directories(hello_library
    PUBLIC 
        ${PROJECT_SOURCE_DIR}/include
)
############################################################
# Create an executable
############################################################
#根據main.cpp生成可執行文件
add_executable(hello_binary
    src/main.cpp
)
#鏈接庫和可執行文件,使用的是這個庫的別名。PRIVATE 表示
target_link_libraries( hello_binary
    PRIVATE 
        hello::library
)

1.5 build-type、set、CACHE變量

強烈建議看:https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.5 build-type.html

image

cmake_minimum_required(VERSION 3.5)
#如果沒有指定則設置默認編譯方式
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  #在命令行中輸出message里的信息
  message("Setting build type to 'RelWithDebInfo' as none was specified.")
  #不管CACHE里有沒有設置過CMAKE_BUILD_TYPE這個變量,都強制賦值這個值為RelWithDebInfo
  set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)

  # 當使用cmake-gui的時候,設置構建級別的四個可選項
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
    "MinSizeRel" "RelWithDebInfo")
endif()


project (build_type)
add_executable(cmake_examples_build_type main.cpp)

image

關於編譯器參數:
參考:http://cn.voidcc.com/question/p-fvbzitto-bhq.html
-DNDEBUG 由兩部分組成,標記 -D 和參數 NDEBUG 。該標志被用於創建預處理器定義,因此這將創建一個新的預處理器 #define 稱為 NDEBUG
-g 是一個編譯器開關,以產生調試信息。它與 -D 完全分開創建定義。

在命令行運行CMake的時候, 使用cmake命令行的-D選項配置編譯類型:
cmake .. -DCMAKE_BUILD_TYPE=Release
image

1.6 Compile Flags

cmake_minimum_required(VERSION 3.5)
#強制設置默認C++編譯標志變量為緩存變量,如CMake(五) build type所說,該緩存變量被定義在文件中,相當於全局變量,源文件中也可以使用這個變量
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DEX2" CACHE STRING "Set C++ Compiler Flags" FORCE)

project (compile_flags)

add_executable(cmake_examples_compile_flags main.cpp)
#為可執行文件添加私有編譯定義
target_compile_definitions(cmake_examples_compile_flags 
    PRIVATE EX3
)

在現代CMake中設置C ++標志的推薦方法是專門針對某個目標(target)設置標志,可以通過target_compile_definitions()函數設置某個目標的編譯標志。

target_compile_definitions(cmake_examples_compile_flags
    PRIVATE EX3
)

如果這個目標是一個庫(cmake_examples_compile_flags),編譯器在編譯目標時添加定義-DEX3 ,並且選擇了范圍PUBLIC或INTERFACE,該定義-DEX3也將包含在鏈接此目標(cmake_examples_compile_flags)的所有可執行文件中。 注意,本語句使用了PRIVATE,所以編譯選項不會傳遞。

image

1.7 Including Third Party Library

https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.7 Including Third Party Library.html
https://www.cnblogs.com/hebohang/p/15990146.html

1.8 設置 C++ 標准

# Set the minimum version of CMake that can be used
# To find the cmake version run
# $ cmake --version
cmake_minimum_required(VERSION 3.1)

# Set the project name
project (hello_cpp11)

# set the C++ standard to C++ 11
set(CMAKE_CXX_STANDARD 11)

# Add an executable
add_executable(hello_cpp11 main.cpp)

1.9 installing

https://github.com/ttroy50/cmake-examples/tree/master/01-basic/E-installing

2. sub projects

https://sfumecjf.github.io/cmake-examples-Chinese/02-sub-projects/A-basic/
image

小彭老師 CMake 教學

1

https://www.bilibili.com/video/BV1fa411r7zp/?spm_id_from=333.788

關於構建系統、CMake

首先是為什么出現了構建系統(比如 Makefile(GNU Make)、Ninja 之類的)

參考
https://sfumecjf.github.io/cmake-examples-Chinese/01-basic/1.9 J-building-with-ninja.html
https://github.com/ttroy50/cmake-examples/tree/master/01-basic/J-building-with-ninja
image

比如老師這個例子:
image
有 hello.cpp main.cpp Makefile,我們make一下,會編譯出 hello.o 和 main.o,要是我們改變 hello.cpp,我們就知道 main.o 不需要改變,只要再編譯一次 hello.cpp 到 hello.o 即可,這便是增量編譯。我想,檢測可以由操作系統打時間戳,GNU Make 再去檢測。
image

為了應對不同平台的差異,於是出現了 CMake,一個構建系統的構建系統,生成的 makefile 之類的再有對應的編譯器編譯成可執行文件。

想切換成 clang++ 去編譯,可以這樣寫:
cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++
測試:
image
這里的 -B 就是指定編譯的位置,我們這里相當於指定為文件夾 build 了。-D 則是 define 的意思,在前面有講過。

還能直接指定C++版本:
cmake -Bbuild -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_STANDARD=17

image

關於 library(動態庫、靜態庫)

比如之前寫的一個 printf 的代碼,被優化成了 puts 函數,用 objdump 查看(反匯編了)如下:
image
這里的
image
就是一個插樁函數,會去 libc.so 找到對應的位置。
image

Linux是沒有與 .so 配套的 .a 的,它會自動生成插樁。可以用 ldd a.out 查看可執行文件 a.out 鏈接了誰(.so)。

CMake 的一些指令

image

image
上圖的 -DMY_MACRO=1 只是為了符合某些人的習慣,其實還是相當於 #define MY_MACRO 1

沒有 target 的下面的指令就像是全局的,不推薦使用。

解決菱形依賴的問題:
image

安裝第三方庫-包管理器:
image

2

https://www.bilibili.com/video/BV16P4y1g7MH/?spm_id_from=333.788

image

現代 CMake 命令行調用:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel 4
cmake --build build --target install

這里的 --build 在Linux中就會調用make,在 Windows 中就會調用 visual studio 的那個 msbuild 去構建。--parallel 4 就代表用 4 個進程並行地構建,並且可以用 --target 來指定要構建的目標,和這個 make install 是一樣的。

image

還有就是現代 CMake 中 public 的應用:
image

![image](https://img2022.cnblogs.com/blog/1752640/202203/1752640-20220311192201092-1353477202. png)

一般比較常用的是 Ninja 這個后端,比較快:
image
Ninja 還是跨平台的,Windows和Linux上都能用:
image
但是像 CUDA 在Windows上強制用 MSBuild 而不能用 Ninja。

可以使用 GLOB 來直接加所有的 .cpp 和 .h,但是可能存在刪掉或者新增cpp卻不被識別的問題,小彭老師推薦加關鍵字 CONFIGUE_DEPENDS:
image
不過前面說的現代 CMake 推薦都寫下來。

可以用 aux_source_directory 來自動根據當前語言搜索所需要的文件:
image

同時還有遞歸地去搜索的關鍵詞 GLOB_RECURSE(能自動包含所有子文件夾下的文件):
image
但是會把 CMake 構建時build中的測試的臨時 cpp 也被加進來:
image

注:在CMake看來編譯和鏈接是一起的,

默認是 Debug 構建模式:
image

image

image

image
https://cmake.org/cmake/help/latest/command/project.html

image

image
image

一般會把 CMAKE_CXX_EXTENSIONS 設置為 OFF,表示不需要有那些GCC的特性(夾帶私貨!否則MSVC可能會通不過)。

這些變量最好設置在 project 之前,這樣編譯器在對project的時候啟用這個語言的時候就會檢測一下,就不容易出錯。

image
image
image
image
image

CMAKE常見變量https://blog.csdn.net/fuyajun01/article/details/8891749

小彭老師建議卸載Ubuntu,使用Arch Linux.

一個標准的 CMakeLists.txt 模板

cmake_minimum_required(VERSION 3.15)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

project(MyProjName LANGUAGES C CXX)

if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR)
    message(WARNING "The binary directory of CMake cannot be the same as source directory!")
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()

鏈接庫文件

小彭老師推薦用靜態庫,因為動態庫在Windows上有很多坑點。

對象庫:https://www.scivision.dev/cmake-object-libraries/
image
image

在自己的項目里可以都用對象庫來組織。對象庫的一個優點是,如上面的 mylib 作為一個對象庫可以指定不同的編譯選項,有時候就會做到。還有一點就是可以繞開編譯器和操作系統的各種規則,保證跨平台統一性。

image
image

image
image

常見坑點:
image

原因是動態庫在內存中的地址是會變化的,他在編譯時會指定一個 -fPIC 選項,但是靜態庫卻沒有這個選項。靜態庫不想變換地址而動態庫卻需要變換地址,因此把一個靜態庫鏈到動態庫上就會報錯:cannot relocate x86_ to loc 之類的錯誤。

解決方法:要么把靜態庫變為一個對象庫,要么讓靜態庫編譯時也生成位置無關的代碼(PIC)
image

不過這樣就導致那些本來不需要為 PIC 的那些庫也變為 PIC 了。

前面的set(CMAKE_POSITION_INDEPENDENT_CODE ON)會把那個文件后面所有的靜態庫都生成 PIC ,因此我們可以用 set_property 來只針對某一個庫:
image

對象的屬性

image

注:經嘗試,set_property(TARGET TinyRaytracer PROPERTY WIN32_EXECUTABLE TRUE) 必須要 WinMain 才可以,main 入口是不可以的,會報錯。

image
image
image
image

image
image

手動拷貝 dll 太麻煩了,有如下幾個解決方法:
image
image

鏈接第三方庫

image

image

image
image
image
image

image
image
image
image
image
image
image
image
image

輸出與變量

image
image
image
image
image
image
image
image

set(myvar hello world) 相當於 set(myvar "hello;world"),即視作列表。

image

所以如果不確定的時候最好打上引號。

結論:除非確實需要列表,建議始終在你不確定的地方加上引號,例如: set(sources “main.cpp” “mylib.cpp” “C:/Program Files/a.cpp”) message(“${sources}”)

變量與緩存

CMake 的最大坑點就是緩存了。

image
image
image

清除緩存,其實只需刪除 build/CMakeCache.txt 就可以了rm build/CMakeCache.txt

image
image
image
image

這個和參數的語法是一樣的,就是前面加個 -D 而已。

image
image
image
image
image
image

bool 值還有 TRUE 和 FALSE,但他們是由於歷史原因保留的,目前普遍用的是 ON 和 OFF。

image

如上圖,設置 WITH_TBB 是一個 BOOL 類型的緩存變量,那么用戶使用的時候就可以這樣來設置是否開啟:
cmake -B build -DWITH_TBB:BOOL=OFF

CMake 對 BOOL 類型緩存的 set 指令提供了一個簡寫:option
image

image
image

image
image

但是上圖的問題是,比如 ccmake(或者Windows的CMake GUI?)會查緩存,如果是不帶 CACHE 的這樣 set 就不進緩存,於是 ccmake 就看不到。

跨平台與編譯器

image
相當於 gcc -DMY_MACRO=233

image
image

雖然名字叫 WIN32,實際上對 32 位 Windows 和 64 位 Windows 都適用。

生成器表達式

image
https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#genex:PLATFORM_ID

這個生成器表達式就相當於,當滿足 PLATFORM_ID:Windows 的時候才會定義后面的 MY_NAME="Bill Gates",如果不滿足就會定義為一個空字符串。總之就相當於剛才的那三個指令,且自動做了if判斷。

image

image
image
image
image
image
image
image
image
image

分支與判斷

image
建議同學們始終使用 ON/OFF 避免混淆

image
image
image
image

變量與作用域

image
image
image
image
image
image

image
image
image
image
image
image
image
image

小彭老師小建議

image
image
image

3

https://www.bilibili.com/video/BV1V84y117YU/?spm_id_from=333.999.0.0&vd_source=bd08f0c74da1940eb8682f61aa471b24

推薦的目錄組織方式

多套一層名字,防止頭文件名重復:
image
image
image

.cmake 文件

image

find_package

image
image
image
image
image
image
image
image
image
image
image

科普:語義版本號(semantic versioning)系統

image

其他記錄

怎么判斷64位平台和32位平台

CMAKE_SIZEOF_VOID_P 表示 void* 的大小(例如為 4 或者 8),可以使用其來判斷當前構建為 32 位還是 64 位

if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
    message(FATAL_ERROR "TinyRaytracer only supports 64-bit platforms")
endif ()

經測試,這一行應該包含在 project 指定語言之后,否則不知道 CMAKE_SIZEOF_VOID_P 是多少上面那個代碼就始終會報錯。

add_library是純頭文件或者沒有源文件

此時不能用 STATIC 啥的,發現會報錯:no sources given to target
https://stackoverflow.com/questions/65415872/issue-regarding-cmake-error-no-source-given-to-target

得換成 INTERFACE

CMake 輸出路徑

https://blog.csdn.net/q610098308/article/details/121157418

CMake warining管理

有時候引入第三方庫我不想讓它報warning,很煩人,學習如下鏈接:
https://www.foonathan.net/2018/10/cmake-warnings/

target_compile_options(my_library PRIVATE
     $<$<OR:$<CXX_COMPILER_ID:Clang>,$<CXX_COMPILER_ID:AppleClang>,$<CXX_COMPILER_ID:GNU>>:
          -Wall>
     $<$<CXX_COMPILER_ID:MSVC>:
          /W4>)

add_compile_definitions 似乎和變量一樣遵從“子不影響父”的關系

add_compile_definitions 似乎和變量一樣遵從“子不影響父”的關系?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM