第十六章:将项目移植到 CMake
在本书的最后一章中,我们将结合前面章节中讨论的多个不同的构建块,并将其应用于一个实际项目。我们的目标将是逐步展示如何将一个非平凡的项目移植到 CMake,并讨论这样的过程中的步骤。我们将为移植您自己的项目或为遗留代码添加 CMake 支持提供建议,无论是来自 Autotools,来自手工编写的配置脚本和 Makefile,还是来自 Visual Studio 项目文件。
为了有一个具体和现实的示例,我们将使用流行的编辑器 Vim(www.vim.org
)背后的源代码,并尝试将配置和编译从 Autotools 移植到 CMake。
为了保持讨论和示例的相对简单性,我们将不尝试为整个 Vim 代码提供完整的 CMake 移植,包括所有选项。相反,我们将挑选并讨论最重要的方面,并且只构建一个核心版本的 Vim,不支持图形用户界面(GUI)。尽管如此,我们将得到一个使用 CMake 和本书中介绍的其他工具配置、构建和测试的 Vim 工作版本。
本章将涵盖以下主题:
- 移植项目时的初始步骤
- 生成文件和编写平台检查
- 检测所需的依赖项并进行链接
- 重现编译器标志
- 移植测试
- 移植安装目标
- 将项目转换为 CMake 时常见的陷阱
从哪里开始
我们将首先展示在哪里可以在线找到我们的示例,然后逐步讨论移植示例。
重现移植示例
我们将从 Vim 源代码仓库的v8.1.0290
发布标签(github.com/vim/vim
)开始,并基于 Git 提交哈希b476cb7
进行工作。以下步骤可以通过克隆 Vim 的源代码仓库并检出该特定版本的代码来重现:
$ git clone --single-branch -b v8.1.0290 https://github.com/vim/vim.git
或者,我们的解决方案可以在github.com/dev-cafe/vim
的cmake-support
分支上找到,并使用以下命令克隆到您的计算机上:
$ git clone --single-branch -b cmake-support https://github.com/dev-cafe/vim
在本示例中,我们将模拟在 CMake 中使用 GNU 编译器集合构建的./configure --enable-gui=no
配置。
为了与我们的解决方案进行比较,并获得额外的灵感,我们鼓励读者也研究 Neovim 项目(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()
我们还使用便携式安装目录变量,如 GNU 软件所定义:
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 配置在一段时间内共存,一个典型的策略是将所有 CMake 代码收集在CMakeLists.txt
文件中,并将所有辅助 CMake 源文件放在cmake
子目录下。在我们的示例中,我们不会引入cmake
子目录,而是将辅助文件更靠近需要它们的目标和源文件,但我们会注意保持几乎所有用于传统 Autotools 构建的文件不变,只有一个例外:我们将对自动生成的文件进行少量修改,以便将它们放置在构建目录下,而不是源代码树中。
记录传统构建过程的记录
在我们向配置中添加任何目标之前,通常首先记录传统构建过程的内容,并将配置和构建步骤的输出保存到日志文件中,这通常很有用。对于我们的 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 -lICE -lXpm -lXt -lX11 -lXdmcp -lSM -lICE -lm -ltinfo -lelf -lnsl -lacl -lattr -lgpm -ldl
调试迁移过程
在逐步将目标和命令迁移到 CMake 侧时,使用message
命令打印变量值将非常有用:
message(STATUS "for debugging printing the value of ${some_variable}")
通过添加选项、目标、源文件和依赖项,同时使用message
进行调试,我们将逐步构建起一个可用的构建系统。
实现选项
找出传统配置向用户提供的选项(例如,通过运行./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)
我们将使用一个变量FEATURES
来模拟--with-features
标志,该变量可以通过cmake -D FEATURES=value
来定义。我们确保如果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
的选择字段,列出了我们已定义的所有可用功能(另请参见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
为 ON
时构建,channel.c
应该只在 ENABLE_CHANNEL
为 ON
时构建)。此外,我们可能可以将所有源文件归类在 src/libvterm/
下,并使用 ENABLE_TERMINAL
使它们的编译成为可选。
通过这种方式,我们将 CMake 结构重新组织为以下树形结构:
. ├── CMakeLists.txt └── src ├── CMakeLists.txt └── libvterm └── CMakeLists.txt
顶级文件添加了 src/CMakeLists.txt
并包含 add_subdirectory(src)
。src/CMakeLists.txt
文件现在包含三个目标(一个可执行文件和两个库),每个目标都带有编译定义和包含目录。我们首先定义可执行文件:
add_executable(vim main.c ) target_compile_definitions(vim PRIVATE "HAVE_CONFIG_H" )
CMake 秘籍(八)(2)https://developer.aliyun.com/article/1525075