将现有系统转换为CMake

对于许多人来说,他们使用 CMake 的第一步就是将一个现有的项目从使用旧的构建系统转换为使用 CMake。这可能是一个相当简单的过程,但有一些问题需要考虑。本节将解决这些问题,并提供一些关于如何有效地将项目转换为 CMake 的建议。在转换为 CMake 时要考虑的第一个问题是项目的目录结构。

源代码目录结构

大多数小型项目将其源代码放在顶级目录中,或者放在名为 srcsource 的目录中。即使所有源代码都在子目录中,我们也强烈建议为顶级目录创建一个 CMakeLists 文件。有两个原因。首先,对于某些人来说,他们必须在项目的子目录而不是主目录上运行 CMake 可能会令人困惑。其次,您可能希望从其他目录安装文档或其他支持文件。通过在项目顶部拥有一个 CMakeLists 文件,您可以使用 add_subdirectory 命令向下进入文档目录,其 CMakeLists 文件可以在其中安装文档(您可以为没有目标或源代码的文档目录创建 CMakeLists 文件)。

对于源代码位于多个目录中的项目,有几种选择。许多基于 Makefile 的项目使用的一种选择是在顶级目录中拥有一个单独的 Makefile,其中列出了要在其子目录中编译的所有源文件。例如

SOURCES=\
  subdir1/foo.cxx \
  subdir1/foo2.cxx \
  subdir2/gah.cxx \
  subdir2/bar.cxx

这种方法与使用类似语法的 CMake 一样有效

set(SOURCES
  subdir1/foo.cxx
  subdir1/foo2.cxx
  subdir1/gah.cxx
  subdir2/bar.cxx
  )

另一种选择是让每个子目录构建一个或多个库,然后可以将这些库链接到可执行文件中。在这些情况下,每个子目录都会定义自己的源文件列表并添加相应的目标。第三种选择是前两种的混合;每个子目录都可以有一个 CMakeLists 文件列出其源代码,但顶级 CMakeLists 文件不会使用 add_subdirectory 命令进入子目录。相反,顶级 CMakeLists 文件将使用 include 命令包含每个子目录的 CMakeLists 文件。例如,顶级 CMakeLists 文件可能包含以下代码

# collect the files for subdir1
include(subdir1/CMakeLists.txt)
foreach(FILE ${FILES})
  set(subdir1Files ${subdir1Files} subdir1/${FILE})
endforeach()

# collect the files for subdir2
include(subdir2/CMakeLists.txt)
foreach(FILE ${FILES})
  set(subdir2Files ${subdir2Files} subdir2/${FILE})
endforeach()

# add the source files to the executable
add_executable(foo ${subdir1Files} ${subdir2Files})

而子目录中的 CMakeLists 文件可能如下所示

# list the source files for this directory
set(FILES
  foo1.cxx
  foo2.cxx
  )

您使用的方案完全取决于您自己。对于大型项目,拥有多个共享库在进行更改时肯定可以提高构建时间。对于小型项目,其他两种方法也各有优势。这里的主要建议是选择一种策略并坚持下去。

构建目录

接下来要考虑的问题是将生成的 .o 文件、库和可执行文件放在哪里。有几种不同的常用方法,其中一些方法比其他方法更适合 CMake。建议的策略是将二进制文件放入与源树具有相同结构的单独树中。例如,如果源树如下所示

foo/
  subdir1
  subdir2

二进制树可能如下所示

foobin/
  subdir1
  subdir2

对于某些 Windows 生成器(如 Visual Studio),构建文件实际上保存在与所选配置匹配的子目录中;例如 debug、release 等。

如果您需要从一个源树支持多个体系结构,我们强烈建议使用以下目录结构

projectfoo/
  foo/
    subdir1
    subdir2
  foo-linux/
    subdir1
    subdir2
  foo-osx/
    subdir1
    subdir2
  foo-solaris/
    subdir1
    subdir2

这样,每个体系结构都有自己的构建目录,并且不会干扰任何其他体系结构。请记住,不仅 .o 文件保存在二进制目录中,而且通常写入二进制树的任何已配置文件也保存在其中。另一个主要在 UNIX 项目中发现的树结构是在源树的子目录中保留不同体系结构的二进制文件(见下文)。CMake 不太适合这种结构,因此我们建议切换到上面显示的单独构建树结构。

foo/
  subdir1/
    linux
    solaris
    hpux
  subdir2/
    linux
    solaris
    hpux

CMake 提供了三个变量来控制二进制目标的写入位置。它们是 CMAKE_RUNTIME_OUTPUT_DIRECTORYCMAKE_LIBRARY_OUTPUT_DIRECTORYCMAKE_ARCHIVE_OUTPUT_DIRECTORY 变量。这些变量用于初始化库和可执行文件的属性,以控制它们将写入的位置。设置这些变量使项目能够将所有库和可执行文件放置到单个目录中。对于具有许多子目录的项目,这可以节省大量时间。一个典型的实现如下所示

# Setup output directories.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)

在此示例中,所有“运行时”二进制文件都将写入项目二进制树的 bin 子目录中,包括所有平台上的可执行文件以及 Windows 上的 DLL。其他二进制文件将写入 lib 目录。这种方法对于使用共享库(DLL)的项目非常有用,因为它将所有共享库收集到一个目录中。如果可执行文件放在同一个目录中,那么在 Windows 上运行时,它们可以更容易地找到所需的共享库。

关于目录结构的最后一点说明:使用 CMake,在一个项目中拥有另一个项目是完全可以接受的。例如,在 Visualization Toolkit 的源树中,有一个目录包含 zlib 压缩库的完整副本。在编写该库的 CMakeLists 文件时,我们使用 project 命令创建一个名为 VTKZLIB 的项目,即使它位于 VTK 源树和项目中。这对 VTK 没有什么实际影响,但它确实允许我们独立于 VTK 构建 zlib,而无需修改其 CMakeLists 文件。

转换项目时有用的 CMake 命令

有一些 CMake 命令可以使转换现有项目的作业更容易和更快。带 GLOB 参数的 file 命令允许您快速设置一个包含与 glob 表达式匹配的所有文件的列表的变量。例如

# collect up the source files
file(GLOB SRC_FILES "*.cxx")

# create the executable
add_executable(foo ${SRC_FILES})

SRC_FILES 变量设置为当前源目录中所有 .cxx 文件的列表。然后它将使用这些源文件创建一个可执行文件。Windows 开发人员应该注意,glob 匹配区分大小写。

转换 UNIX Makefile

如果您的项目当前基于标准 UNIX Makefile,那么将其转换为 CMake 应该相当简单。从本质上讲,对于项目中每个具有 Makefile 的目录,您都将创建一个匹配的 CMakeLists 文件。您如何处理目录中的多个 Makefile 取决于它们的功能。如果其他 Makefile(或 Makefile 类型文件)只是包含在主 Makefile 中,您可以创建匹配的 CMake 输入 (.cmake) 文件,并以类似的方式将它们包含到您的主 CMakeLists 文件中。如果不同的 Makefile 旨在针对不同的情况在命令行上调用,请考虑创建一个主 CMakeLists 文件,该文件使用一些逻辑来根据 CMake 选项选择要 include 的文件。

通常 Makefile 会有一个要编译的目标文件列表。这些可以转换为 CMake 变量,如下所示

OBJS= \
  foo1.o \
  foo2.o \
  foo3.o

变成

set(SOURCES
  foo1.c
  foo2.c
  foo3.c
)

虽然目标文件通常列在 Makefile 中,但在 CMake 中,重点是源文件。如果您在 Makefile 中使用了条件语句,则可以将其转换为 CMake if 命令。由于 CMake 处理生成依赖项,因此可以消除大多数依赖项或生成依赖项的规则。在您有构建库或可执行文件的规则的地方,请将其替换为 add_libraryadd_executable 命令。

一些 UNIX 构建系统(以及源代码)大量使用系统架构来确定要编译哪些文件或使用哪些标志。通常,此信息存储在名为 ARCHUNAME 的 Makefile 变量中。在这些情况下,首选方法是用更通用的测试替换特定于架构的代码。对于某些软件包,特定于架构的代码过多,以至于进行此类更改不合理,或者您可能出于其他原因希望根据架构做出决策。在这些情况下,您可以使用变量 CMAKE_SYSTEM_NAMECMAKE_SYSTEM_VERSION。它们提供了有关主机计算机的操作系统和版本的相当详细的信息。

转换基于 Autoconf 的项目

基于 Autoconf 的项目主要由三个关键部分组成。第一个是驱动该过程的 configure.in 文件。第二个是 Makefile.in,它将成为生成的 Makefile,第三个部分是运行 configure 后生成的其余已配置文件。在将基于 autoconf 的项目转换为 CMake 时,请从 configure.in 和 Makefile.in 文件开始。

Makefile.in 文件可以转换为 CMake 语法,如前面关于转换 UNIX Makefile 的部分所述。完成此操作后,将 configure.in 文件转换为 CMake 语法。Autoconf 中的大多数函数(宏)在 CMake 中都有相应的命令。下面列出了某些基本转换的简短表格

AC_ARG_WITH

使用 option 命令。

AC_CHECK_HEADER

使用来自 CheckIncludeFile 模块的 check_include_file 宏。

AC_MSG_CHECKING

使用 message 命令以及 STATUS 参数。

AC_SUBST

使用 configure_file 命令时自动完成。

AC_CHECK_LIB

使用来自 CheckLibraryExists 模块的 check_libary_exists 宏。

AC_CONFIG_SUBDIRS

使用 add_subdirectory 命令。

AC_OUTPUT

使用 configure_file 命令。

AC_TRY_COMPILE

使用 try_compile 命令。

如果您的 configure 脚本使用 AC_TRY_COMPILE 执行测试编译,则可以使用相同的代码用于 CMake。如果代码很短,则可以直接将其放入 CMakeLists 文件中,或者最好将其放入项目的源代码文件中。对于需要此类测试的大型项目,我们通常将此类文件放入 CMake 子目录中。

在您依赖 autoconf 配置文件的情况下,您可以使用 CMake 的 configure_file 命令。基本方法相同,我们通常将要配置的输入文件命名为 .in 扩展名,就像 autoconf 所做的那样。此命令将输入文件中引用为 ${VAR}@VAR@ 的任何变量替换为 CMake 确定的值。如果未定义变量,则将其替换为空。或者,仅替换 @VAR@ 形式的变量,而忽略 ${VAR}。这对于为使用 ${VAR} 作为评估变量语法的语言配置文件很有用。您还可以使用 C 预处理器通过使用 #cmakedefine VAR 有条件地定义变量。如果定义了变量,则 configure_file#cmakedefine 转换为 #define;如果未定义,则将其变为已注释的 #undef。例如

/* what byte order is this system */
#cmakedefine CMAKE_WORDS_BIGENDIAN

/* what size is an INT */
#cmakedefine SIZEOF_INT @SIZEOF_INT@

转换基于 Windows 的工作区

将 Visual Studio 解决方案转换为 CMake 涉及几个步骤。首先,您需要在源代码目录的顶部创建一个 CMakeLists 文件。与往常一样,此文件应以 cmake_minimum_requiredproject 命令开始,该命令定义 CMake 项目的名称。这将成为生成的 Visual Studio 解决方案的名称。接下来,将所有源文件添加到 CMake 变量中。对于具有多个目录的大型项目,请在每个目录中创建一个 CMakeLists 文件,如本章开头有关源目录结构的部分所述。然后,您将使用 add_libraryadd_executable 添加库和可执行文件。默认情况下,add_executable 假设您的可执行文件是控制台应用程序。将 WIN32 参数添加到 add_executable 表示它是一个 Windows 应用程序(使用 WinMain 而不是 main)。

Visual Studio 支持一些不错的功能,CMake 可以利用这些功能。一个是支持类浏览。通常在 CMake 中,只有源文件添加到目标中,而不是头文件。如果将头文件添加到目标,它们将显示在工作区中,然后您可以像往常一样浏览它们。Visual Studio 还支持文件组的概念。默认情况下,CMake 为源文件和头文件创建组。使用 source_group 命令,您可以创建自己的组并将文件分配给它们。如果您的工作区中有任何自定义构建步骤,则可以使用 add_custom_command 命令将其添加到您的 CMakeLists 文件中。Visual Studio 中的自定义目标(实用程序目标)可以使用 add_custom_target 命令添加。