# 15.1 如何开始迁移项目

我们将首先说明，在哪里可以找到我们的示例，然后对移植，进行逐步的讨论。

## 复制要移植的示例

我们将从Vim源代码库的v8.1.0290发行标记开始(<https://github.com/vim/vim>) ，我们的工作基于Git提交哈希值b476cb7进行。 通过克隆Vim的源代码库并检出特定版本的代码，可以复制以下步骤:

```
$ git clone --single-branch -b v8.1.0290 https://github.com/vim/vim.git
```

或者，我们的解决方案可以在`cmake-support`分支上找到，网址是 <https://github.com/dev-cafe/vim> ，并使用以下方法克隆下来:

```
$ 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_minimum_required(VERSION
3.5 FATAL_ERROR)
project(vim LANGUAGES C)
```

添加任何目标或源之前，可以设置默认的构建类型。本例中，我们默认为Release配置，这将打开某些编译器优化选项:

```
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
```

我们也使用可移植的安装目录变量：

```
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})
```

作为一个完整性检查，我们可以尝试配置和构建项目，但到目前为止还没有目标，所以构建步骤的输出是空的:

```
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
```

我们一会儿就要开始添加目标了。

## 如何让常规和CMake配置共存

CMake的一个特性是在源代码之外构建，构建目录可以是任何目录，而不必是项目目录的子目录。这意味着，我们可以将一个项目移植到CMake，而不影响以前/现在的配置和构建机制。对于一个重要项目的迁移，CMake文件可以与其他构建框架共存，从而允许一个渐进的迁移，包括选项、特性和可移植性，并允许开发社区人员适应新的框架。为了允许传统配置和CMake配置共存一段时间，一个典型的策略是收集`CMakeLists.txt`文件中的所有CMake代码，以及CMake子目录下的所有辅助CMake源文件的示例中，我们不会引入CMake子目录，而是保持辅助文件要求他们接近目标和来源，但会顾及使用的传统Autotools构建修改的所有文件，但有一个例外：我们将一些修改自动生成文件构建目录下，而不是在源代码树中。

```
$ ./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示例，可以使用以下方法实现:

```
$ ./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`命令打印变量的值就非常有用了:

```
message(STATUS "for debugging printing the value of ${some_variable}")
```

在使用消息进行调试时，添加选项、目标、源和依赖项，我们将逐步实现一个可工作的构建。

## 实现选项

找出传统配置为用户提供的选项(例如，通过`./configure --help`)。Vim项目提供了一个非常长的选项和标志列表，为了使本章的讨论保持简单，我们只在CMake端实现四个选项:

```
--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中添加以下选项(有默认值)：

```
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":

```
if(NOT FEATURES)
    set(FEATURES "huge" CACHE STRING
"FEATURES chosen by the user at CMake configure time")
endif()
```

我们为使用者提供了一个值`FEATURES`:

```
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`中，从而可以在自己的目录范围内处理该文件:

```
add_subdirectory(src)
```

在`src/CMakeLists.txt`中，可以定义可执行目标，并列出从`build.log`中获取所有源码:

```
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结构重组，构成如下的树结构：

```
.
├── CMakeLists.txt
└── src
    ├── CMakeLists.txt
    └── libvterm
        └── CMakeLists.txt
```

顶层文件使用`add_subdirectory(src)`添加`src/CMakeLists.txt`。`src/CMakeLists.txt`文件包含三个目标(一个可执行文件和两个库)，每个目标都带有编译定义和包含目录。首先定义可执行文件：

```
add_executable(vim
  main.c
  )

target_compile_definitions(vim
  PRIVATE
      "HAVE_CONFIG_H"
  )
```

然后，定义一些需要源码文件的目标:

```
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
  )
```

然后，定义一些可选源码文件的目标:

```
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/`子目录进行选择:

```
if(ENABLE_TERMINAL)
  add_subdirectory(libvterm)

  target_link_libraries(vim
    PUBLIC
        libvterm
    )
endif()
```

对应的`src/libvterm/CMakeLists.txt`包含以下内容:

```
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构建之后尝试配置)，现在来试试:

```
$ 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`(和其他文件)，我们将在下一节中考虑这些文件。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chenxiaowei.gitbook.io/cmake-cookbook/15.0-chinese/15.1-chinese.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
