# 15.1 如何開始遷移項目
我們將首先說明,在哪里可以找到我們的示例,然后對移植,進行逐步的討論。
## 復制要移植的示例
我們將從Vim源代碼庫的v8.1.0290發行標記開始(https://github.com/vim/vim) ,我們的工作基于Git提交哈希值b476cb7進行。
通過克隆Vim的源代碼庫并檢出特定版本的代碼,可以復制以下步驟:
```shell
$ git clone --single-branch -b v8.1.0290 https://github.com/vim/vim.git
```
或者,我們的解決方案可以在`cmake-support`分支上找到,網址是 https://github.com/dev-cafe/vim ,并使用以下方法克隆下來:
```shell
$ git clone --single-branch -b cmake-support https://github.com/dev-cafe/vim
```
在本例中,我們將使用CMake模擬` ./configure --enable-gui=no`的配置方式。
為了與后面的解決方案進行比較,建議讀者也可以研究以下Neovim項目(https://github.com/neovim/neovim ),這是傳統Vi編輯器的一個分支,提供了一個CMake構建系統。
## 創建一個主CMakeLists.txt
首先,我們在源代碼存儲庫的根目錄中創建主`CMakeLists.txt`,在這里我們設置了最低CMake版本、項目名稱和支持的語言,在本例中是C:
```cmake
cmake_minimum_required(VERSION
3.5 FATAL_ERROR)
project(vim LANGUAGES C)
```
添加任何目標或源之前,可以設置默認的構建類型。本例中,我們默認為Release配置,這將打開某些編譯器優化選項:
```cmake
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
```
我們也使用可移植的安裝目錄變量:
```cmake
include(GNUInstallDirs)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
```
作為一個完整性檢查,我們可以嘗試配置和構建項目,但到目前為止還沒有目標,所以構建步驟的輸出是空的:
```shell
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
```
我們一會兒就要開始添加目標了。
## 如何讓常規和CMake配置共存
CMake的一個特性是在源代碼之外構建,構建目錄可以是任何目錄,而不必是項目目錄的子目錄。這意味著,我們可以將一個項目移植到CMake,而不影響以前/現在的配置和構建機制。對于一個重要項目的遷移,CMake文件可以與其他構建框架共存,從而允許一個漸進的遷移,包括選項、特性和可移植性,并允許開發社區人員適應新的框架。為了允許傳統配置和CMake配置共存一段時間,一個典型的策略是收集`CMakeLists.txt`文件中的所有CMake代碼,以及CMake子目錄下的所有輔助CMake源文件的示例中,我們不會引入CMake子目錄,而是保持輔助文件要求他們接近目標和來源,但會顧及使用的傳統Autotools構建修改的所有文件,但有一個例外:我們將一些修改自動生成文件構建目錄下,而不是在源代碼樹中。
```shell
$ ./configure --enable-gui=no
... lot of output ...
$ make > build.log
```
我們的示例中(這里沒有顯示build.log的內容),我們能夠驗證編譯了哪些源文件以及使用了哪些編譯標志(`-I. -Iproto -DHAVE_CONFIG_H -g -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1`)。日志文件中,我們可以做如下推斷:
* 所有對象文件都鏈接到二進制文件中
* 不生成庫
* 可執行目標與下列庫進行連接:`-lSM -lICE -lXpm -lXt -lX11 -lXdmcp -lSM -lICE -lm -ltinfo -lelf -lnsl -lacl -lattr -lgpm -ldl`
通過在使用`message`對工程進行調試時,選擇添加選項、目標、源和依賴項,我們將逐步實現一個可工作的構建。
## 獲取傳統構建的記錄
向配置添加任何目標之前,通常有必要看看傳統構建的行為,并將配置和構建步驟的輸出保存到日志文件中。對于我們的Vim示例,可以使用以下方法實現:
```shell
$ ./configure --enable-gui=no
... lot of output ...
$ make > build.log
```
示例中(這里沒有顯示build.log的完整內容),我們能夠驗證編譯了哪些源文件以及使用了哪些編譯標志(`-I.-Iproto -DHAVE_CONFIG_H -g -O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1`)。從日志文件中,推斷如下:
* 所有對象文件都鏈接到一個二進制文件中
* 沒有生成庫
* 可執行目標鏈接到以下庫:`-lSM -lXpm -lXt -lX11 -lXdmcp -lSM -lSM - linfo -lelf -lnsl -lacl -lattr -lgpm -ldl`
## 調試遷移項目
當目標和命令逐漸移動到CMake端時,使用`message`命令打印變量的值就非常有用了:
```cmake
message(STATUS "for debugging printing the value of ${some_variable}")
```
在使用消息進行調試時,添加選項、目標、源和依賴項,我們將逐步實現一個可工作的構建。
## 實現選項
找出傳統配置為用戶提供的選項(例如,通過` ./configure --help`)。Vim項目提供了一個非常長的選項和標志列表,為了使本章的討論保持簡單,我們只在CMake端實現四個選項:
```shell
--disable-netbeans Disable NetBeans integration support.
--disable-channel Disable process communication support.
--enable-terminal Enable terminal emulation support.
--with-features=TYPE tiny, small, normal, big or huge (default: huge)
```
我們還將忽略任何GUI支持和模擬`--enable-gui=no`,因為它將使示例復雜化。
我們將在CMakeLists.txt中添加以下選項(有默認值):
```cmake
option(ENABLE_NETBEANS "Enable netbeans" ON)
option(ENABLE_CHANNEL "Enable channel" ON)
option(ENABLE_TERMINAL "Enable terminal" ON)
```
我們可以用`cmake -D FEATURES=value`定義的變量`FEATURES`來模擬`--with-features `標志。如果不進行設置,它默認值為"huge":
```cmake
if(NOT FEATURES)
set(FEATURES "huge" CACHE STRING
"FEATURES chosen by the user at CMake configure time")
endif()
```
我們為使用者提供了一個值`FEATURES`:
```cmake
list(APPEND _available_features "tiny" "small" "normal" "big" "huge")
if(NOT FEATURES IN_LIST _available_features)
message(FATAL_ERROR "Unknown features: \"${FEATURES}\". Allowed values are: ${_available_features}.")
endif()
set_property(CACHE FEATURES PROPERTY STRINGS ${_available_features})
```
最后一行`set_property(CACHE FEATURES PROPERTY STRINGS ${_available_features})`,當使用`cmake-gui`配置項目,則有有不錯的效果,用戶可根據選擇字段清單,選擇已經定義了的`FEATURES`(參見https://blog.kitware.com/constraining-values-with-comboboxes-in-cmake-cmake-gui/ )。
選項可以放在主`CMakeLists.txt`中,也可以在查詢`ENABLE_NETBEANS`、`ENABLE_CHANNEL`、`ENABLE_TERMINAL`和`FEATURES`的定義附近。前一種策略的優點是,選項列在一個地方,不需要遍歷`CMakeLists.txt`文件來查找選項的定義。因為我們還沒有定義任何目標,所以可以先將選項保存在一個文件中,但是稍后會將選項移到離目標更近的地方,通過本地化作用域,得到可重用的CMake構建塊。
## 從可執行的目標開始,進行本地化
讓我們添加一些源碼。在Vim示例中,源文件位于`src`下,為了保持主`CMakeLists.txt`的可讀性和可維持性,我們將創建一個新文件`src/CMakeLists.txt`,并將其添加到主`CMakeLists.txt`中,從而可以在自己的目錄范圍內處理該文件:
```cmake
add_subdirectory(src)
```
在`src/CMakeLists.txt`中,可以定義可執行目標,并列出從`build.log`中獲取所有源碼:
```cmake
add_executable(vim
arabic.c beval.c buffer.c blowfish.c crypt.c crypt_zip.c dict.c diff.c digraph.c edit.c eval.c evalfunc.c ex_cmds.c ex_cmds2.c ex_docmd.c ex_eval.c ex_getln.c farsi.c fileio.c fold.c getchar.c hardcopy.c hashtab.c if_cscope.c if_xcmdsrv.c list.c mark.c memline.c menu.c misc1.c misc2.c move.c mbyte.c normal.c ops.c option.c os_unix.c auto/pathdef.c popupmnu.c pty.c quickfix.c regexp.c screen.c search.c sha256.c spell.c spellfile.c syntax.c tag.c term.c terminal.c ui.c undo.c userfunc.c window.c libvterm/src/encoding.c libvterm/src/keyboard.c libvterm/src/mouse.c libvterm/src/parser.c libvterm/src/pen.c libvterm/src/screen.c libvterm/src/state.c libvterm/src/unicode.c libvterm/src/vterm.c netbeans.c channel.c charset.c json.c main.c memfile.c message.c version.c
)
```
這是一個開始。這種情況下,代碼甚至不會配置,因為源列表包含生成的文件。討論生成文件和鏈接依賴項之前,我們把這一長列表拆分一下,以限制目標依賴項的范圍,并使項目更易于管理。如果我們將它們分組到目標,這將使CMake更容易地找到源文件依賴項,并避免很長的鏈接行。
對于Vim示例,我們可以進一步了解來自`src/Makefile`和`src/configure.ac`的源碼文件進行分組。這些文件中,大多數源文件都是必需的。有些源文件是可選的(`netbeans.c`應該只在`ENABLE_NETBEANS`打開時構建,而`channel.c`應該只在`ENABLE_CHANNEL`打開時構建)。此外,我們可以將所有源代碼分組到`src/libvterm/`下,并使用`ENABLE_TERMINAL`可選地編譯它們。
這樣,我們將CMake結構重組,構成如下的樹結構:
```shell
.
├── CMakeLists.txt
└── src
├── CMakeLists.txt
└── libvterm
└── CMakeLists.txt
```
頂層文件使用`add_subdirectory(src)`添加`src/CMakeLists.txt`。`src/CMakeLists.txt`文件包含三個目標(一個可執行文件和兩個庫),每個目標都帶有編譯定義和包含目錄。首先定義可執行文件:
```cmake
add_executable(vim
main.c
)
target_compile_definitions(vim
PRIVATE
"HAVE_CONFIG_H"
)
```
然后,定義一些需要源碼文件的目標:
```cmake
add_library(basic_sources "")
target_sources(basic_sources
PRIVATE
arabic.c beval.c blowfish.c buffer.c charset.c
crypt.c crypt_zip.c dict.c diff.c digraph.c
edit.c eval.c evalfunc.c ex_cmds.c ex_cmds2.c
ex_docmd.c ex_eval.c ex_getln.c farsi.c fileio.c
fold.c getchar.c hardcopy.c hashtab.c if_cscope.c
if_xcmdsrv.c json.c list.c main.c mark.c
memfile.c memline.c menu.c message.c misc1.c
misc2.c move.c mbyte.c normal.c ops.c
option.c os_unix.c auto/pathdef.c popupmnu.c pty.c
quickfix.c regexp.c screen.c search.c sha256.c
spell.c spellfile.c syntax.c tag.c term.c
terminal.c ui.c undo.c userfunc.c version.c
window.c
)
target_include_directories(basic_sources
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/proto
${CMAKE_CURRENT_LIST_DIR}
${CMAKE_CURRENT_BINARY_DIR}
)
target_compile_definitions(basic_sources
PRIVATE
"HAVE_CONFIG_H"
)
target_link_libraries(vim
PUBLIC
basic_sources
)
```
然后,定義一些可選源碼文件的目標:
```cmake
add_library(extra_sources "")
if(ENABLE_NETBEANS)
target_sources(extra_sources
PRIVATE
netbeans.c
)
endif()
if(ENABLE_CHANNEL)
target_sources(extra_sources
PRIVATE
channel.c
)
endif()
target_include_directories(extra_sources
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/proto
${CMAKE_CURRENT_BINARY_DIR}
)
target_compile_definitions(extra_sources
PRIVATE
"HAVE_CONFIG_H"
)
target_link_libraries(vim
PUBLIC
extra_sources
)
```
使用以下代碼,對連接`src/libvterm/`子目錄進行選擇:
```cmake
if(ENABLE_TERMINAL)
add_subdirectory(libvterm)
target_link_libraries(vim
PUBLIC
libvterm
)
endif()
```
對應的`src/libvterm/CMakeLists.txt`包含以下內容:
```cmake
add_library(libvterm "")
target_sources(libvterm
PRIVATE
src/encoding.c
src/keyboard.c
src/mouse.c
src/parser.c
src/pen.c
src/screen.c
src/state.c
src/unicode.c
src/vterm.c
)
target_include_directories(libvterm
PUBLIC
${CMAKE_CURRENT_LIST_DIR}/include
)
target_compile_definitions(libvterm
PRIVATE
"HAVE_CONFIG_H"
"INLINE="
"VSNPRINTF=vim_vsnprintf"
"IS_COMBINING_FUNCTION=utf_iscomposing_uint"
"WCWIDTH_FUNCTION=utf_uint2cells"
)
```
我們已經從`build.log`中獲取了編譯信息。樹結構的優點是,目標的定義靠近源的位置。如果我們決定重構代碼并重命名或移動目錄,描述目標的CMake文件就會隨著源文件一起移動。
我們的示例代碼還沒有配置(除非在成功的Autotools構建之后嘗試配置),現在來試試:
```shell
$ mkdir -p build
$ cd build
$ cmake ..
-- The C compiler identification is GNU 8.2.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done
CMake Error at src/CMakeLists.txt:12 (add_library):
Cannot find source file:
auto/pathdef.c
Tried extensions .c .C .c++ .cc .cpp .cxx .cu .m .M .mm .h .hh .h++ .hm
.hpp .hxx .in .txx
```
這里需要生成`auto/pathdef.c`(和其他文件),我們將在下一節中考慮這些文件。
- Introduction
- 前言
- 第0章 配置環境
- 0.1 獲取代碼
- 0.2 Docker鏡像
- 0.3 安裝必要的軟件
- 0.4 測試環境
- 0.5 上報問題并提出改進建議
- 第1章 從可執行文件到庫
- 1.1 將單個源文件編譯為可執行文件
- 1.2 切換生成器
- 1.3 構建和鏈接靜態庫和動態庫
- 1.4 用條件句控制編譯
- 1.5 向用戶顯示選項
- 1.6 指定編譯器
- 1.7 切換構建類型
- 1.8 設置編譯器選項
- 1.9 為語言設定標準
- 1.10 使用控制流
- 第2章 檢測環境
- 2.1 檢測操作系統
- 2.2 處理與平臺相關的源代碼
- 2.3 處理與編譯器相關的源代碼
- 2.4 檢測處理器體系結構
- 2.5 檢測處理器指令集
- 2.6 為Eigen庫使能向量化
- 第3章 檢測外部庫和程序
- 3.1 檢測Python解釋器
- 3.2 檢測Python庫
- 3.3 檢測Python模塊和包
- 3.4 檢測BLAS和LAPACK數學庫
- 3.5 檢測OpenMP的并行環境
- 3.6 檢測MPI的并行環境
- 3.7 檢測Eigen庫
- 3.8 檢測Boost庫
- 3.9 檢測外部庫:Ⅰ. 使用pkg-config
- 3.10 檢測外部庫:Ⅱ. 自定義find模塊
- 第4章 創建和運行測試
- 4.1 創建一個簡單的單元測試
- 4.2 使用Catch2庫進行單元測試
- 4.3 使用Google Test庫進行單元測試
- 4.4 使用Boost Test進行單元測試
- 4.5 使用動態分析來檢測內存缺陷
- 4.6 預期測試失敗
- 4.7 使用超時測試運行時間過長的測試
- 4.8 并行測試
- 4.9 運行測試子集
- 4.10 使用測試固件
- 第5章 配置時和構建時的操作
- 5.1 使用平臺無關的文件操作
- 5.2 配置時運行自定義命令
- 5.3 構建時運行自定義命令:Ⅰ. 使用add_custom_command
- 5.4 構建時運行自定義命令:Ⅱ. 使用add_custom_target
- 5.5 構建時為特定目標運行自定義命令
- 5.6 探究編譯和鏈接命令
- 5.7 探究編譯器標志命令
- 5.8 探究可執行命令
- 5.9 使用生成器表達式微調配置和編譯
- 第6章 生成源碼
- 6.1 配置時生成源碼
- 6.2 使用Python在配置時生成源碼
- 6.3 構建時使用Python生成源碼
- 6.4 記錄項目版本信息以便報告
- 6.5 從文件中記錄項目版本
- 6.6 配置時記錄Git Hash值
- 6.7 構建時記錄Git Hash值
- 第7章 構建項目
- 7.1 使用函數和宏重用代碼
- 7.2 將CMake源代碼分成模塊
- 7.3 編寫函數來測試和設置編譯器標志
- 7.4 用指定參數定義函數或宏
- 7.5 重新定義函數和宏
- 7.6 使用廢棄函數、宏和變量
- 7.7 add_subdirectory的限定范圍
- 7.8 使用target_sources避免全局變量
- 7.9 組織Fortran項目
- 第8章 超級構建模式
- 8.1 使用超級構建模式
- 8.2 使用超級構建管理依賴項:Ⅰ.Boost庫
- 8.3 使用超級構建管理依賴項:Ⅱ.FFTW庫
- 8.4 使用超級構建管理依賴項:Ⅲ.Google Test框架
- 8.5 使用超級構建支持項目
- 第9章 語言混合項目
- 9.1 使用C/C++庫構建Fortran項目
- 9.2 使用Fortran庫構建C/C++項目
- 9.3 使用Cython構建C++和Python項目
- 9.4 使用Boost.Python構建C++和Python項目
- 9.5 使用pybind11構建C++和Python項目
- 9.6 使用Python CFFI混合C,C++,Fortran和Python
- 第10章 編寫安裝程序
- 10.1 安裝項目
- 10.2 生成輸出頭文件
- 10.3 輸出目標
- 10.4 安裝超級構建
- 第11章 打包項目
- 11.1 生成源代碼和二進制包
- 11.2 通過PyPI發布使用CMake/pybind11構建的C++/Python項目
- 11.3 通過PyPI發布使用CMake/CFFI構建C/Fortran/Python項目
- 11.4 以Conda包的形式發布一個簡單的項目
- 11.5 將Conda包作為依賴項發布給項目
- 第12章 構建文檔
- 12.1 使用Doxygen構建文檔
- 12.2 使用Sphinx構建文檔
- 12.3 結合Doxygen和Sphinx
- 第13章 選擇生成器和交叉編譯
- 13.1 使用CMake構建Visual Studio 2017項目
- 13.2 交叉編譯hello world示例
- 13.3 使用OpenMP并行化交叉編譯Windows二進制文件
- 第14章 測試面板
- 14.1 將測試部署到CDash
- 14.2 CDash顯示測試覆蓋率
- 14.3 使用AddressSanifier向CDash報告內存缺陷
- 14.4 使用ThreadSaniiser向CDash報告數據爭用
- 第15章 使用CMake構建已有項目
- 15.1 如何開始遷移項目
- 15.2 生成文件并編寫平臺檢查
- 15.3 檢測所需的鏈接和依賴關系
- 15.4 復制編譯標志
- 15.5 移植測試
- 15.6 移植安裝目標
- 15.7 進一步遷移的措施
- 15.8 項目轉換為CMake的常見問題
- 第16章 可能感興趣的書
- 16.1 留下評論——讓其他讀者知道你的想法