---
title: \[Day 9\] 第一個 CMake 專案!
tags: ithome, ithome2023, cmake
---
[TOC]
[Day 9] 第一個 CMake 專案!
# 本日內容
* 設計專案架構
* `CMakeLists.txt`
* `CMAKE_<LANG>_STANDARD`, `CMAKE_<LANG>_STANDARD_REQUIRED`, `CMAKE_<LANG>_EXTENSIONS`
* 決定 Generator
* 來 build code 吧!
* Debug build
* 預告
連結: [Day 9 - Colab](https://colab.research.google.com/drive/1GjMWcf-5qu3E9pkmB0jM4JHwfcJ9v00a?usp=sharing)
就如 [Day 3](https://ithelp.ithome.com.tw/articles/10314617) 所說, CMake 將 build code 的流程分成 configure, build, test, install, package 幾個階段
我們會先著重在 configure 和 build 的部分, 先把最重要的可執行檔建出來
由於範例皆是在 Linux 開發, 對於 Linux 的檔案系統可以額外參考 Filesystem Hierarchy Standard (FHS)
可以大概知道常見的系統資料夾代表什麼意義, 這部分會在 [Day 20]() 介紹
# 設計專案架構
初期的專案架構會長這樣
```shell=
cmake-example/
├─ CMakeLists.txt
├─ src/
│ ├─ CMakeLists.txt
│ ├─ main.cpp
```
非常陽春, 只有
* `CMakeLists.txt`
* 給 CMake 用的 build script
* `src`
* `cmake` 執行時的 source files 的路徑
* 如 [Day 4](https://ithelp.ithome.com.tw/articles/10315042) 所說, CMake 會根據此路徑自動建出一連串的 cache variables
* `src/main.cpp`
* 主程式入口點
Build directory 會在之後讓 CMake 自動幫我們產生
# `CMakeLists.txt`
內容長這樣
```cmake=
cmake_minimum_required(VERSION 3.26)
project(HelloWorld
LANGUAGES C CXX
)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_subdirectory(src)
```
`src/CMakeLists.txt`
```cmake=
add_executable(HelloWorld_Main
main.cpp
)
```
以下簡單說明每行指令的意思
## `cmake_minimum_required()`
```cmake=
cmake_minimum_required(VERSION major.minor[.patch[.tweak]])
```
* **一定要在第一行執行!!**
* 主要是相容性考量, 由於 CMake 3.0 開始有 breaking change
如果太舊會有問題
* 每個小版本可能也會有不同的行為, 不過本系列不會深入討論
有興趣可以參考 [cmake_policy()](https://cmake.org/cmake/help/latest/command/cmake_policy.html)
* major, minor
* 代表有新 feature
* patch
* 代表有修復 bugs
* tweak
* 不太會用到
## `project()`
```cmake=
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
```
* **建議要在 `cmake_minimum_required()` 下一行就執行**
* `PROJECT-NAME`
* 不能有空格
* 通常只會有 letters, 下底線
* 給某些 generator 使用 (ex. Xcode, Visual Studio)
* `VERSION`
* 好習慣是寫清楚 project 的版本, 讓 CMake 其他部分可以 reference
* `LANGUAGES`
* CMake 會幫忙檢查指定語言的 compiler 和 linker 能否正常直接
* project 用的語言
* 預設是 `C`, `CXX`
* 可以給 `none` 就不會檢查語言
* 多語言用空格分隔
* 支援以下語言
* C
* CXX (i.e. C++)
* CSharp (i.e. C#) (CMake >= 3.8)
* CUDA, OBJC (i.e. Objective-C) (CMake >= 3.8)
* OBJCXX (i.e. Objective-C++)
* Fortran
* HIP
* ISPC
* Swift
* ASM
* ASM_NASM
* ASM_MARMASM
* ASM_MASM
* ASM-ATT
# `CMAKE_<LANG>_STANDARD`, `CMAKE_<LANG>_STANDARD_REQUIRED`, `CMAKE_<LANG>_EXTENSIONS`
這三個 project-scope 變數通常是搭配使用的, 所以一起講 (variable scope 可以回去看 [Day 4](https://ithelp.ithome.com.tw/articles/10315042) 介紹)
由於 C/C++ 版本需要有對應的 compiler / linker 版本, 最好在 `CMakeLists.txt` 一開始就設定好
也就是在 `project()` 下
比如以下範例 (Note: `CXX` 可以換成前面提過的其他語言, ex. `C`)
```cmake=
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
```
* `CMAKE_CXX_STANDARD`
* 指定 language standard
* 只是 **建議** , 但該設定沒有強制力
如果 compiler 不支援該 standard, 還是會 fallback 到最近的 standard 版本, 有點恐怖
* 要搭配 `CMAKE_CXX_STANDARD_REQUIRED` 設為 ON 才有強制力
* `CMAKE_CXX_STANDARD_REQUIRED`
* Standard requirements 最小版本, 如果低於指定版本會噴錯給你看
* 需要搭配 `CMAKE_CXX_STANDARD`
* `CMAKE_CXX_EXTENSIONS`
* 決定是否要用 compiler extensions
* 我們會希望讓專案容易被其他人使用, 所以建議不要用 compiler-specific 的 extensions, 故設為 `OFF`
## `add_executable()`
新增 executable target, [Day 8](https://ithelp.ithome.com.tw/articles/10316053) 有介紹過, 這邊就不重複介紹了
## `add_subdirectory()`
```cmake=
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
```
* `source_dir`
* relative path
* source tree 內的話用這種
* 相對於 `source` directory
* absolute path
* source tree 外才用
* 就是絕對路徑
* `binary_dir`
* 預設會在 `binary` directory 建立和 `source_dir` 相同名稱的 folder
* relative path
* 相對於 `binary` directory
* absolute path
* `source_dir` 在 source tree 外才用
* `EXCLUDE_FROM_ALL`
* 用來控制 subdirectory 的 target 是否應該包含在 project 的 `ALL` target
* `SYSTEM`
* 指定 `SYSTEM` 會讓 subdirectory 的 non-imported targets 把該 subdirectory 加入 system header search paths
* 重點: 是給 **consuming targets** 使用, 也就是 link 該 subdirectory 中 non-imported targets 的 targets
* 尷尬的是, 如果該資料夾已經在我們 project 中了, 就不必把他加到 system header search path
需要加到 system header search path 的是 imported targets, 也就是外部資源 (見 [Day 8](https://ithelp.ithome.com.tw/articles/10316053))
但 imported targets 在被我們加進來的時候 CMake 就會幫我們做這件事了
* 所以通常是用在 vendor directory, 不過如果可以, 還是建議用 imported targets 的方式比較好~
# 決定 Generator
我們會先用大家比較熟悉的 Makefile 作為 Generator
由於 Makefile 是 single-config generator, 我們一次只能 build 一種 config 的 binary
關於 single-config generators 和 multi-platform generators 的比較在 [Day 6](https://ithelp.ithome.com.tw/articles/10315505) 有詳細介紹
# 來 build code 吧!
我們先 `cd` 到 `cmake-example` 底下, 執行
```shell=
cmake -S src -B build -G "Unix Makefiles"
```
確認 `build` 底下有 config files 後, 就可以 build 了!
```shell=
cmake --build build
```
# Debug build
為了之後 debug 方便, 我們來 build debug 版本, 關於 build config 請見 [Day 7](https://ithelp.ithome.com.tw/articles/10315729)
我們指定一個新的 build directory `build-Debug` 重新 configure 一次 (因為是 single-config generator, 所以比較麻煩), 然後加上 `CMAKE_BUILD_TYPE:BOOL=Debug`
```shell=
cmake -S src -B build-Debug -G "Unix Makefiles" -DCMAKE_BUILD_TYPE:BOOL=Debug
```
再 build 一次
```shell=
cmake --build build-Debug
```
然後把原本的 `build` directory 改成 `build-Release` 後再做一次
```shell=
cmake -S . -B build-Release -G "Unix Makefiles" -DCMAKE_BUILD_TYPE:BOOL=Release
cmake --build build-Release
```
這樣我們就有 debug 和 release 版本了!
```shell=
cmake-example/
├─ CMakeLists.txt
├─ build-Debug/
├─ build-Release/
├─ src/
│ ├─ CMakeLists.txt
│ ├─ main.cpp
```
## 如何檢查自己 build 的版本是否正確?
對於目前的 single-config generator 來說, 可以先簡單用 `CMAKE_BUILD_TYPE` 確認即可
```cmake=
message(DEBUG "Build type: ${CMAKE_BUILD_TYPE}")
```
```shell=
cmake -S . -B build-Debug -G "Unix Makefiles" -DCMAKE_BUILD_TYPE:BOOL=Debug --log-level=Debug
```
但是, 記得 [Day 6](https://ithelp.ithome.com.tw/articles/10315505) 說過的, multi-config generators 是在 `build` stage 才決定 build type
所以用 cache variable 這招會失敗QQ
那要怎麼辦呢?
本系列會在 [Day 26]() 介紹比較可靠的 generator expression
用法大概如下, 搭配 `add_custom_target()` (見 [Day 8](https://ithelp.ithome.com.tw/articles/10316053))
```cmake=
set(isDebug "$<IF:$<CONFIG:Debug>, is debug, not debug>")
add_custom_target(mytarget
COMMAND ${CMAKE_COMMAND} -E echo "isDebug? ${isDebug}"
VERBATIM
)
```
# 預告
現在, 我們有了第一版簡單的 CMake 專案, 是時候自己寫一些 libraries 了
不過在那之前, 下一篇會先介紹 link libraries 時的一些注意事項