65.9K
CodeProject 正在变化。 阅读更多。
Home

面向 Visual C++ 开发人员的 CMake 教程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (22投票s)

2017 年 4 月 10 日

CPOL

12分钟阅读

viewsIcon

105530

downloadIcon

1930

本文通过一系列示例,旨在为 VC++ 开发人员介绍 CMake,帮助您创建和维护 CMake 项目。

引言

Visual Studio 2017 对原生开发最重要的变化之一就是支持 CMake。CMake 是一个跨平台开源工具,用于独立于编译器和环境定义原生应用程序的构建过程。CMake 使用脚本(称为 CMakeLists.txt)来生成特定于环境的构建文件,例如 Visual Studio 项目、XCode 项目、make 脚本等。尽管 CMake 从 6.0 版本起就支持所有 Visual Studio 版本,但 Visual Studio 2017 是第一个支持 CMake 的版本。这意味着在 Visual Studio 2017 中,您无需显式生成 VC++ 项目即可创建、编辑、构建、运行和调试 C++ 代码,因为系统会在后台自动处理这些操作。

本文通过一系列示例,旨在为 VC++ 开发人员介绍 CMake,帮助您创建和维护 CMake 项目。

Visual Studio 2017 支持概览

虽然本文是 CMake 的入门介绍,但我们还是有必要先简要介绍一下 Visual Studio 中对 CMake 的支持。关于支持的详细讨论,请参阅 CMake 支持在 Visual Studio 中

在 Visual Studio 2017 中,您可以使用 打开文件夹 功能来打开 C++ 代码。为此,您需要在源代码所在的同一位置拥有一个 CMake 脚本,该脚本始终命名为 CMakeLists.txt。如果您的代码分布在多个项目和子目录中,则每个项目目录中都需要有一个。要从此类目录中打开代码,请转到 **文件** > **打开** > **文件夹**。

打开文件夹后,您可以在解决方案资源管理器中看到该文件夹的内容,包括 CMakeLists.txt 文件。

主菜单中会有一个名为 CMake 的附加项,其中包含不同的命令,例如构建脚本、管理 CMake 缓存、更改 CMake 设置等。构建和调试代码等不同命令也可通过 CMakeLists.txt 文件的上下文菜单获得。

当您构建 CMake 脚本时,VC++ 项目文件会被创建并存储在临时位置,然后 Visual Studio 使用这些文件来构建、运行和调试代码。

定义示例

在本文的其余部分,我们将讨论如何构建分组在多个项目中的 C++ 代码:一个静态库、一个动态库和一个应用程序。静态库名为 libutil,包含实用函数(random.hrandom.cpp)。它与名为 sharedmod 的动态链接库(pisym.hpisym.cpp)静态链接,而 sharedmod 又与名为 theapp 的主应用程序(main.cpp)链接。

初始文件夹结构如下所示

cmakedemo/
├── include/
|   └── libdef.h
└── src/
    ├── libutil/
    |   ├── random.h
    |   └── random.cpp
    ├── sharedmod/
    |   ├── pisym.h
    |   └── pisym.cpp
    └── theapp/
        └── main.cpp

源代码位于 src 目录下方,每个项目都有自己的子目录。还有一个 include 目录,与 src 处于同一级别。需要在每个 src 子文件夹以及根目录中创建 CMakeLists.txt 脚本。

cmakedemo/
├── include/
|   └── libdef.h
└── src/
    ├── libutil/
    |   ├── random.h
    |   ├── random.cpp
    |   └── CMakeLists.txt
    ├── sharedmod/
    |   ├── pisym.h
    |   ├── pisym.cpp
    |   └── CMakeLists.txt
    ├── theapp/
    |   ├── main.cpp
    |   └── CMakeLists.txt
    └── CMakeLists.txt

CMake 版本

您可以使用 CMakeLists.txt 脚本通过 cmake_minimum_required 命令(位于脚本开头)来指定最低 CMake 版本。

cmake_minimum_required(VERSION major.minor[.patch[.tweak]] [FATAL_ERROR])

如果安装的 CMake 版本低于指定的版本,脚本的解析将以错误停止。此命令是可选的,但如果存在,则应放在顶层脚本的开头,在任何其他命令之前。

cmake_minimum_required(VERSION 3.6.2)

Visual Studio 2017 自带自己的 CMake 分发版。它位于文件夹 c:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO\2017\ENTERPRISE\COMMON7\IDE\COMMONEXTENSIONS\MICROSOFT\CMAKE\CMake\ (假设 Visual Studio 的默认安装位置)。Visual Studio 2017 RTM 随附的版本是 3.6.20160606-g0391f-MSVC,这基本上是支持 Visual Studio 2017 的 3.6.2 版本。您也可以有其他 CMake 安装。例如,如果您从 cmake.org 下载并安装在默认位置,那么它也位于 c:\Program Files\CMake\。当从 Visual Studio 运行时,始终使用的是 VS 分发版。因此,您必须将最低版本指定为 3.6.2,而不是更高的版本。否则,构建将因错误而失败。

1> Working directory: C:\Users\M\AppData\Local\CMakeBuild\
   bfe38435-a2ed-1738-925f-b2cbf11bf883\build\x86-Debug
1> CMake Error at C:\Work\Learn\cmakedemo\src\CMakeLists.txt:1 (cmake_minimum_required):
1>   CMake 3.7 or higher is required.  You are running version
1>   3.6.20160606-g0391f-MSVC

项目名称

您可以使用 project 命令指定整个项目的名称,以及可选的版本和语言。

project(<PROJECT-NAME> [LANGUAGES] [<language-name>...])
project(<PROJECT-NAME>
        [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
        [LANGUAGES <language-name>...])

构建脚本时,该名称将成为 Visual Studio 解决方案的名称。

project(cmakedemo)

构建消息

可以使用 message 命令在构建脚本期间向用户显示消息。

message([<mode>] "message to display" ...)

这需要一个可选的 mode 参数,指示消息的类型,以及要输出的文本。可能的模式如上表所示。请注意,这些模式区分大小写,必须用大写字母书写。

(无) 重要信息
STATUS 附带信息
警告 CMake 警告,继续处理
AUTHOR_WARNING CMake 警告(dev),继续处理
SEND_ERROR CMake 错误,继续处理,但跳过生成
FATAL_ERROR CMake 错误,停止处理和生成
DEPRECATION 如果分别启用了 CMAKE_ERROR_DEPRECATEDCMAKE_WARN_DEPRECATED 变量,则会显示 CMake 弃用错误或警告,否则不显示任何消息。
   
   

命令行 cmake 工具将 STATUS 消息显示在 stdout 上,而所有其他消息都显示在 stderr 上。

message(STATUS "Setting MSVC flags")

设置变量

构建变量、缓存变量和环境变量都可以使用同一个命令设置,该命令称为 set。这三种变量的语法相似。首先是变量名,然后是值,最后是可能的附加参数。要引用变量,必须将其用 ${} 包围,例如 ${VARIABLE_NAME}。对于环境变量,必须将其用 ENV{} 包围,例如 ENV{PATH}

缓存变量用于用户可设置的值,但此命令不会覆盖现有缓存变量,除非您指定 FORCE 选项。设置缓存变量时,您还需要指定一个类型,该类型可以是 BOOLFILEPATH(文件路径)、PATH(目录路径)、STRINGINTERNAL 之一,以及一个描述变量的 string,该描述将显示在工具的 GUI 版本中。

set(<variable> <value>... [PARENT_SCOPE])
set(<variable> <value>... CACHE <type> <docstring> [FORCE])
set(ENV{<variable>} <value>...)

有一些预定义的变量用于各种目的。在我们的顶层脚本中,我们将使用其中一些

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHc /std:c++latest")

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../bin")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../bin")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../bin")

注意:CMake 会在指定的输出目录中隐式添加配置的名称,例如 Debug 或 Release。因此,如果您指定的输出为 C:\Work\Learn\cmakedemo\bin\,则 Debug 配置的实际输出文件夹将是 C:\Work\Learn\cmakedemo\bin\Debug\

可以通过命令行设置变量。您需要使用 -D 前缀来引入它们,然后提供一个值。

cmake -DCMAKE_RUNTIME_OUTPUT_DIRECTORY c:\demo\bin

添加子目录

当您的代码组织在多个目录中时,您可以使用 add_subdirectory 命令将子目录添加到构建中。

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

唯一强制参数是目录名称或路径。它可以是绝对路径,也可以是相对路径,在这种情况下,它会从当前输出目录开始解析。此目录必须包含一个 CMakeLists.txt 脚本,CMake 将在继续处理当前输入脚本之前立即处理该脚本。

add_subdirectory(libutil)
add_subdirectory(sharedmod)
add_subdirectory(theapp)

添加库和可执行文件

您可以使用 add_libraryadd_executable 命令将库和可执行文件添加到构建中。这两个命令的语法相似

add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 ...])

<name> 是要从指定源列表中构建的目标的名称。这是一个逻辑目标名称,在项目中必须是唯一的。库或可执行文件的实际文件名基于此名称,但取决于本地平台。对于库文件,在 Windows 上是 <name>.lib,在其他系统上是 lib<name>.a;对于可执行文件名,在 Windows 上是 <name>.exe,在其他系统上是 <name>

对于库,可以指定 STATIC(用于链接其他目标的对象的存档)、SHARED(动态链接库)或 MODULE(未链接到其他目标但运行时动态加载的插件)来指示库的类型。如果未指定这些类型中的任何一种,则默认为 STATICSHARED,具体取决于变量 BUILD_SHARED_LIBSON 还是 OFF

源文件列表可以包含源文件和头文件。请注意,在为 Visual Studio 生成项目时,只有在命令中列出的文件才会显示在项目中。

add_library(libutil STATIC random.cpp random.h)

如果文件数量太多无法手动指定,或者您想基于模式批量包含它们,可以使用 file 命令生成匹配模式的文件列表,并将其存储在一个变量中,然后您可以在 add_libraryadd_executable 命令中引用该变量。以下示例定义了一个名为 headers 的变量,其中包含当前文件夹中所有扩展名为 .h 的文件。

file(GLOB headers *.h)
add_library(sharedmod SHARED pisym.cpp ${headers})

add_executable(theapp main.cpp)

包含和链接目录

可以使用 include_directories 命令将目录添加到包含目录列表中,并使用 link_directories 命令将其添加到链接目录列表(链接器查找库的位置)中。

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

link_directories(directory1 directory2 ...)

使用 include_directories 命令指定的目录会添加到当前 CMakeLists.txt 脚本的 INCLUDE_DIRECTORY 目录属性中,以及当前 CMakeLists.txt 脚本中每个目标的 INCLUDE_DIRECTORY 目标属性中。当指定 SYSTEM 时,该目录将被指定为编译器的系统包含目录。

include_directories(${CMAKE_SOURCE_DIR}/../include)

使用 link_directories 指定的目录仅应用于在此命令之后创建的目标。因此,在 add_libraryadd_executable 之前使用此命令非常重要。

link_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY})

预处理器定义

可以使用 add_definitions 命令添加预处理器定义。名称必须以 -D 作为前缀。

add_definitions(-DEXPMODULE)

添加库依赖项

您可以使用 target_link_libraries 命令为目标指定库依赖项或链接标志。此命令有多种形式,但在本文中,我们只讨论一种

target_link_libraries(<target> ... <item>... ...)

<target> 必须之前已通过 add_library()add_executable() 创建,并且可以指定多个目标。<item> 可以是库名、库文件的完整路径、纯库名、链接标志或 debug/optimized/general 关键字后跟另一个项。关键字 debug 指的是调试配置,optimized 指的是所有其他配置,general 指的是所有配置,并且是可选的。

使用 debugoptimized 关键字放在项前面,对于为不同配置指定不同目标很有用,例如,为发布配置链接名为 mylibrary.lib 的库,为调试配置链接名为 mylibraryd.lib 的库。

target_link_libraries(sharedmod debug libutil.lib)
target_link_libraries(sharedmod optimized libutil.lib)

设置构建依赖项

目标之间的构建顺序通常很重要,因为某些目标必须在其他目标之前构建。可以通过 add_dependencies 命令指定目标之间的构建顺序依赖项。

add_dependencies(<target> [<target-dependency>]...)

使用此命令指定的所有目标都必须是使用 add_executable()add_library()add_custom_target() 创建的顶层目标。

add_dependencies(sharedmod libutil)
add_dependencies(theapp sharedmod)

设置属性

您可以使用 set_property 命令在给定范围内为命名属性设置值。此命令接受一个可选的范围和一个强制的 PROPERTY 选项,后跟属性的名称及其值或值。

set_property(<GLOBAL                            |
              DIRECTORY [dir]                   |
              TARGET    [target1 [target2 ...]] |
              SOURCE    [src1 [src2 ...]]       |
              INSTALL   [file1 [file2 ...]]     |
              TEST      [test1 [test2 ...]]     |
              CACHE     [entry1 [entry2 ...]]>
             [APPEND] [APPEND_STRING]
             PROPERTY <name> [value1 [value2 ...]])

我们可以使用此命令在 Visual Studio 解决方案中设置启动项目。例如,如果我们希望应用程序 theapp 成为启动项目,我们必须在顶层 CMakeLists.txt 脚本中指定以下命令。

set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT theapp)

整合

通过讨论的所有这些命令,我们可以将主文件夹和每个子文件夹的脚本组合起来,如下所示。

  • 顶层 CMakeLists.txt 脚本
    cmake_minimum_required(VERSION 3.6.2)
    project(cmakedemo)
    
    message(STATUS "Setting MSVC flags")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHc /std:c++latest")
    
    set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../bin")
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../bin")
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../bin")
    
    add_subdirectory(libutil)
    add_subdirectory(sharedmod)
    add_subdirectory(theapp)
    
    add_dependencies(sharedmod libutil)
    add_dependencies(theapp sharedmod)
    
    set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT theapp)
  • libutil CMakeLists.txt 脚本
    add_library(libutil STATIC random.cpp random.h)
    
    include_directories(${CMAKE_SOURCE_DIR}/../include)
    add_definitions(-DEXPMODULE)
    
  • sharedmod CMakeLists.txt 脚本
    include_directories(${CMAKE_SOURCE_DIR}/libutil)
    include_directories(${CMAKE_SOURCE_DIR}/../include)
    link_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
    
    file(GLOB headers *.h)
    add_library(sharedmod SHARED pisym.cpp ${headers})
    
    add_definitions(-DEXPMODULE)
    
    target_link_libraries(sharedmod debug libutil.lib)
    target_link_libraries(sharedmod optimized libutil.lib)
    
  • theapp CMakeLists.txt 脚本
    include_directories(${CMAKE_SOURCE_DIR}/../include)
    include_directories(${CMAKE_SOURCE_DIR}/libutil)
    include_directories(${CMAKE_SOURCE_DIR}/sharedmod)
    
    link_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
    
    add_executable(theapp main.cpp)
    
    target_link_libraries(theapp debug sharedmod.lib)
    target_link_libraries(theapp optimized sharedmod.lib)

显式构建脚本

尽管 Visual Studio 2017 会自动(或按需)构建 CMake 脚本,但您仍然可以在没有 Visual Studio 的情况下使用 CMake 来生成 Visual C++ 项目和解决方案。以下命令显示了如何为此目的使用 CMake,假设代码位于 src 文件夹下(如本文开头所示)。

cmake -G "Visual Studio 15 2017" src

-- The C compiler identification is MSVC 19.10.25017.0
-- The CXX compiler identification is MSVC 19.10.25017.0
-- Check for working C compiler: C:/Program Files 
 (x86)/Microsoft Visual Studio/2017/Enterprise/VC/Tools/MSVC/14.10.25017/bin/HostX86/x86/cl.exe
-- Check for working C compiler: C:/Program Files 
 (x86)/Microsoft Visual Studio/2017/Enterprise/VC/Tools/MSVC/14.10.25017/bin/HostX86/x86/cl.exe
-- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files 
  (x86)/Microsoft Visual Studio/2017/Enterprise/VC/Tools/MSVC/14.10.25017/bin/HostX86/x86/cl.exe
-- Check for working CXX compiler: C:/Program Files 
 (x86)/Microsoft Visual Studio/2017/Enterprise/VC/Tools/MSVC/14.10.25017/bin/HostX86/x86/cl.exe 
-- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Setting MSVC flags
-- Configuring done
-- Generating done
-- Build files have been written to: C:/Work/Learn/cmakedemo

"Visual Studio 15 2017" 是生成 Visual Studio 2017 项目(针对 x86 平台)的生成器名称。如果您需要针对 x64 或 ARM 平台,则必须使用其他生成器,分别为 "Visual Studio 15 2017 Win64""Visual Studio 15 2017 ARM"

在使用常规 CMake 版本或 Visual Studio 随附的 CMake 版本时,生成的项目存在一些重要的区别。

  • CMake 生成针对单个平台(x86、x64 或 ARM,取决于您指定的生成器)的配置,而 Visual Studio 随附的 CMake 则为 x86 和 x64 生成配置,但不包括 ARM。
  • CMake 生成四种配置:DebugReleaseMinSizeRelRelWithDebInfo(后三种是 Release 配置),而 Visual Studio 随附的 CMake 则为 Debug 和 Release 配置生成,但同时针对这两个目标,因此总共生成四种配置:x86-Debugx86-Releasex64-Debugx64-Release

  • CMake 在当前工作目录中生成项目,而 Visual Studio 随附的版本则在 AppData\Local 文件夹中生成项目,如前所示。但在这两种情况下,输出都位于相同的位置,即使用预定义变量(如 CMAKE_RUNTIME_OUTPUT_DIRECTORY)指定的位置,如上所示。

跨 IDE 和平台构建

尽管本文专注于 Visual Studio,但使用 CMake 的主要原因是构建跨平台和跨编译器应用程序。我们创建的用于创建 Visual Studio 项目的相同脚本也可用于其他 IDE。例如,只需将生成器从 "Visual Studio 15 2017" 替换为 Xcode,CMake 就会创建一个 Xcode 项目。

cmake -G Xcode src

注意:为了成功构建项目,源代码和脚本还需要进行一些更改。例如,示例代码中使用的 std::optional 在撰写本文时最新版本的 Xcode 提供的 LLVM 工具链中不受支持。此外,编译器选项 /EHc /std:c++latest 应替换为 -std=c++1z -fexceptions -g -Wall。还有其他更改是必要的,但这些超出了本文的范围。

历史

  • 2017 年 4 月 10 日:初始版本
© . All rights reserved.