---
# System prepended metadata

title: '[Day 9] 第一個 CMake 專案!'

---

---
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 時的一些注意事項