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