CMake 官方教程 Step1

186 阅读5分钟

Tutorial Step 1: A Basic Starting Point

Exercise 1 - Building a Basic Project

一个最简单的 CMake 项目是由单个源文件构建而成的,对于这样的简单项目,一个最基本的 CmakeLists.txt 文件最少需要包含以下三行:

cmake_minimum_required (VERSION 3.10) 
project (Tutorial) 
add_executable(Tutorial tutorial.cxx)

注意:cmake 的语法支持大小、小写和大小写混合上边的代码中我们使用的 cmake 语法是小写的,不过只有系统指令是不区分大小写的,但是变量和字符串是区分大小写的。

任何项目的顶级 CMakeLists.txt 必须从指定最小 CMake 版本开始,即上面第一行,这是为了确保下面的 CMake 指令可以由合适的 CMake 版本执行。我们使用 project() 去设置项目名,每个项目都需要这样,并且应该在 cmake_minimum_required() 命令之后立即设置。最后,使用 add_executable() 命令告诉 CMake 去使用特定的源文件创建一个可执行文件。

Relative Resources

project()

project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
        [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
        [DESCRIPTION <project-description-string>]
        [HOMEPAGE_URL <url-string>]
        [LANGUAGES <language-name>...])

设置项目名,并将其存储到变量 PROJECT_NAME 中。如果是在顶层的 CMakeLists.txt 中调用,则还会将项目名存到变量 CMAKE_PROJECT_NAME 中。

同时还会设置下面的变量:

  • PROJECT_SOURCE_DIR, <PROJECT-NAME>_SOURCE_DIR

    • 项目源目录的绝对地址
  • PROJECT_BINARY_DIR, <PROJECT-NAME>_BINARY_DIR

    • 项目构建目录的绝对地址
  • PROJECT_IS_TOP_LEVEL, <PROJECT-NAME>_IS_TOP_LEVEL

    • New in version 3.21.
    • 指出是否为顶级项目的布尔值

add_executable()

add_executable(<name> [WIN32] [MACOSX_BUNDLE]
               [EXCLUDE_FROM_ALL]
               [source1] [source2 ...])

通过列出的源文件添加一个叫做 <name> 的可执行文件,在一个项目中, <name> 必须唯一。

New in version 3.1 可以使用「生成器表达式」(generator expressions),语法为 $<...>

New in version 3.11 如果在后面使用 target_sources(),源文件可以省略

默认可执行文件会生成在执行该命令的 CMakeLists.txt 所在的目录中。

Solution

修改完成的 CMakeLists.txt 如下:

# TODO 1: Set the minimum required version of CMake to be 3.10
cmake_minimum_required(VERSION 3.10)
# TODO 2: Create a project named Tutorial
project(Tutorial)
# TODO 7: Set the project version number as 1.0 in the above project command

# TODO 6: Set the variable CMAKE_CXX_STANDARD to 11
#         and the variable CMAKE_CXX_STANDARD_REQUIRED to True

# TODO 8: Use configure_file to configure and copy TutorialConfig.h.in to
#         TutorialConfig.h

# TODO 3: Add an executable called Tutorial to the project
# Hint: Be sure to specify the source file as tutorial.cxx
add_executable(Tutorial tutorial.cxx)
# TODO 9: Use target_include_directories to include ${PROJECT_BINARY_DIR}

Build and Run

完成 TODO1TODO3 后,接下来就是构建以及运行了。CMakeLists.txt 以及源文件在 /Step1 中,在当前目录新建 Step1_build 并使用 cmake 命令去生成构建系统文件:

mkdir Step1_build
cd Step1_build
cmake ../Step1

过程如下:

cmake ../Step1/
-- The C compiler identification is AppleClang 15.0.0.15000040
-- The CXX compiler identification is AppleClang 15.0.0.15000040
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Library/Developer/CommandLineTools/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Library/Developer/CommandLineTools/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (1.4s)
-- Generating done (0.0s)
-- Build files have been written to: /Volumes/HY/C++/cmake-3.28.2-tutorial-source/Step1_build

执行完成,当前目录(Step1_build)产生了如下文件:

./Step1_build/
├── CMakeCache.txt
├── CMakeFiles
│   ├── 3.27.8
│   │   ├── CMakeCCompiler.cmake
│   │   ├── CMakeCXXCompiler.cmake
│   │   ├── CMakeDetermineCompilerABI_C.bin
│   │   ├── CMakeDetermineCompilerABI_CXX.bin
│   │   ├── CMakeSystem.cmake
│   │   ├── CompilerIdC
│   │   │   ├── CMakeCCompilerId.c
│   │   │   ├── CMakeCCompilerId.o
│   │   │   └── tmp
│   │   └── CompilerIdCXX
│   │       ├── CMakeCXXCompilerId.cpp
│   │       ├── CMakeCXXCompilerId.o
│   │       └── tmp
│   ├── CMakeConfigureLog.yaml
│   ├── CMakeDirectoryInformation.cmake
│   ├── CMakeScratch
│   ├── Makefile.cmake
│   ├── Makefile2
│   ├── TargetDirectories.txt
│   ├── Tutorial.dir
│   │   ├── DependInfo.cmake
│   │   ├── build.make
│   │   ├── cmake_clean.cmake
│   │   ├── compiler_depend.make
│   │   ├── compiler_depend.ts
│   │   ├── depend.make
│   │   ├── flags.make
│   │   ├── link.txt
│   │   └── progress.make
│   ├── cmake.check_cache
│   ├── pkgRedirects
│   └── progress.marks
├── Makefile
└── cmake_install.cmake

之后就是真正的编译以及链接了,使用 cmake --build 执行构建系统生成的构建规则:

cmake --build .

输出:

cmake --build .
[ 50%] Building CXX object CMakeFiles/Tutorial.dir/tutorial.cxx.o
[100%] Linking CXX executable Tutorial
[100%] Built target Tutorial

这样在当前目录就生成了一个名为之前 add_executable() 命令中定义的名称的可执行文件,一个最简单的 CMake 项目就完成了。

Exercise 2 - Specifying the C++ Standard

CMake 有一些特殊的变量,它们大部分都是由 CMAKE_ 开头的,所以在给自己的项目设置变量要注意不要冲突。其中有两个特殊变量是用户可以设置的:

  • CMAKE_CXX_STANDARD
  • CMAKE_CXX_STANDARD_REQUIRED

它们共同指定了构建该项目所需的 C++ 标准。这个 Exercise 的目标就是给 tutorial 代码加上 C++11 的需求,完成 TODO 4TODO 6

Relative Resources

set()

set(<variable> <value>... [PARENT_SCOPE])

这个命令可以在当前的函数或目录范围内设置或重置变量 <variable>

  • 如果至少提供了一个 <value>,那么就将该变量设置为那个值。
  • 如果没有提供 <value>,那么就重置这个变量,相当于 unset(<variable>) 命令。

Solution

首先修改 tutorial.cxx,使其具有 C++11 的特性,如下:

// A simple program that computes the square root of a number
#include <cmath>
// #include <cstdlib> // TODO 5: Remove this line
#include <iostream>
#include <string>

// TODO 11: Include TutorialConfig.h

int main(int argc, char* argv[])
{
  if (argc < 2) {
    // TODO 12: Create a print statement using Tutorial_VERSION_MAJOR
    //          and Tutorial_VERSION_MINOR
    std::cout << "Usage: " << argv[0] << " number" << std::endl;
    return 1;
  }

  // convert input to double
  // TODO 4: Replace atof(argv[1]) with std::stod(argv[1])
  const double inputValue = std::atof(argv[1]);

  // calculate square root
  const double outputValue = sqrt(inputValue);
  std::cout << "The square root of " << inputValue << " is " << outputValue
            << std::endl;
  return 0;
}

然后修改 CMakeLists.txt,需要确保变量 CMAKE_CXX_STANDARD 的设置在 add_executable() 命令之前完成。将 CMAKE_CXX_STANDARD 设置为 11,CMAKE_CXX_STANDARD_REQUIRED 是布尔值,设置为 True。

# TODO 1: Set the minimum required version of CMake to be 3.10
cmake_minimum_required(VERSION 3.10)
# TODO 2: Create a project named Tutorial
project(Tutorial)
# TODO 7: Set the project version number as 1.0 in the above project command

# TODO 6: Set the variable CMAKE_CXX_STANDARD to 11
#         and the variable CMAKE_CXX_STANDARD_REQUIRED to True
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# TODO 8: Use configure_file to configure and copy TutorialConfig.h.in to
#         TutorialConfig.h

# TODO 3: Add an executable called Tutorial to the project
# Hint: Be sure to specify the source file as tutorial.cxx
add_executable(Tutorial tutorial.cxx)
# TODO 9: Use target_include_directories to include ${PROJECT_BINARY_DIR}

Build and Run

由于之前已经创建了构建目录,并且已经为 Exercise 运行了 CMake,所以直接使用 cmake --build . 即可完成构建。

Exercise 3 - Adding a Version Number and Configured Header File

拥有一个在 CMakelists.txt 中定义,并且可以在源代码中获取的变量有时候是很有用的,在这个 Exercise 中将要打印项目版本。

实现上面效果的一种方法是使用配置的头文件。我们创建一个包含一个或多个要替换的变量的输入文件,这些变量有特殊的语法,看起来像 @VAR@。然后,我们使用 configure_file() 命令将输入文件复制到给定的输出文件,并用文件中的当前值 VAR 替换 CMakelists.txt 中的这些变量。

虽然我们可以直接在源代码中编辑版本,但是最好使用这个特性,因为它创建了一个单一的值来源,并且避免了重复。

Relative Resources

  • <PROJECT-NAME>_VERSION_MAJOR
    • 主版本号
  • <PROJECT-NAME>_VERSION_MINOR
    • 第二版本号

这两个变量通过 project() 命令来设置。

configure_file()

configure_file(<input> <output>
               [NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS |
                FILE_PERMISSIONS <permissions>...]
               [COPYONLY] [ESCAPE_QUOTES] [@ONLY]
               [NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])

复制一个文件到另一个位置,并修改它的内容。具体来说,将 <input> 文件复制到 <output> 文件(如果文件不存在就会创建新文件),并且替换输入文件中如同 @VAR@${VAR}$CACHE{VAR}$ENV{VAR} 的变量值。每个变量引用将被替换为变量的当前值,如果没有定义变量,则替换为空字符串。此外,下面格式的输入文件:

#cmakedefine VAR ...

将会被替换为:

#define VAR ...

/* #undef VAR */

这取决于 VAR 这个变量在 CMake 文件中是否被设置为 if 语句取 True 的值。与此不同的格式 #cmakedefine01 VAR 中的 VAR 会被替换为 VAR 0VAR 1,这也取决于 CMake 文件中的定义。

如果修改了输入文件,则生成系统将重新运行 CMake 以重新配置文件并再次生成生成系统。只有当生成的文件的内容发生更改时,才会修改生成的文件,并在后续 cmake 上更新其时间戳。

其他参数:

  • @ONLY
    • 只替换 @VAR@ 格式的变量引用,这对使用了 ${VAR} 语法的脚本很有用。
Example

假设有一个输入文件 foo.h.in

#cmakedefine FOO_ENABLE
#cmakedefine FOO_STRING "@FOO_STRING@"

CMakeLists.txt 如下,它使用了 configure_file 配置了一个头文件 foo.h

option(FOO_ENABLE "Enable Foo" ON)
if(FOO_ENABLE)
  set(FOO_STRING "foo")
endif()
configure_file(foo.h.in foo.h @ONLY)

如果 FOO_ENABLE option 是 on,配置的文件如下:

#define FOO_ENABLE
#define FOO_STRING "foo"

否则:

/* #undef FOO_ENABLE */
/* #undef FOO_STRING */

target_include_directories()

target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
  <INTERFACE|PUBLIC|PRIVATE> [items1...]
  [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

在 CMake 中,target_include_directories 用于指定一个目标(通常是可执行文件、共享库或静态库)的头文件搜索路径。这个命令告诉编译器在编译和链接时应该去哪里查找头文件。

  • <target>: 目标的名称,可以是可执行文件、共享库或静态库的名称。必须是由 add_executable()add_library() 生成的。
  • SYSTEM: 可选参数,将目录添加到系统头文件搜索路径,通常用于包含系统提供的头文件。
  • BEFORE: 可选参数,将指定的目录添加到当前目标的头文件搜索路径之前,即在其他路径之前搜索。
  • INTERFACE|PUBLIC|PRIVATE: 指定头文件路径应该对目标的哪些范围可见。这些关键字的含义如下:
    • PRIVATE: 只有目标自身可见。
    • INTERFACE: 可见于目标和依赖于目标的目标。
    • PUBLIC: 可见于目标、依赖于目标的目标以及使用该目标的目标。
# CMakeLists.txt in my_library
add_library(my_library my_source.cpp)
target_include_directories(my_library PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

在这个例子中,my_library 库的头文件路径被设置为 ${CMAKE_CURRENT_SOURCE_DIR}/include,并且通过 PUBLIC 关键字表明这个路径对使用该库的其他项目也是可见的。这样,当其他项目使用 my_library 时,它们就能正确找到 my_source.cpp#include 的头文件。

Solution

完成 TODO 7TODO 12,首先修改 CMakeLists.txt

# TODO 1: Set the minimum required version of CMake to be 3.10
cmake_minimum_required(VERSION 3.10)
# TODO 2: Create a project named Tutorial
project(Tutorial VERSION 1.0)
# TODO 7: Set the project version number as 1.0 in the above project command

# TODO 6: Set the variable CMAKE_CXX_STANDARD to 11
#         and the variable CMAKE_CXX_STANDARD_REQUIRED to True
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# TODO 8: Use configure_file to configure and copy TutorialConfig.h.in to
#         TutorialConfig.h
configure_file(TutorialConfig.h.in TutorialConfig.h)
# TODO 3: Add an executable called Tutorial to the project
# Hint: Be sure to specify the source file as tutorial.cxx
add_executable(Tutorial tutorial.cxx)
# TODO 9: Use target_include_directories to include ${PROJECT_BINARY_DIR}
target_include_directories(Tutorial PUBLIC "${PROJECT_BINARY_DIR}")

接下来修改输入文件 TutorialConfig.h.in

// the configured options and settings for Tutorial
// TODO 10: Define Tutorial_VERSION_MAJOR and Tutorial_VERSION_MINOR
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

最后修改 tutorial.cxx 来包含创建的头文件 TutorialConfig.h

// A simple program that computes the square root of a number
#include <cmath>
// #include <cstdlib> // TODO 5: Remove this line
#include <iostream>
#include <string>
#include "TutorialConfig.h"
// TODO 11: Include TutorialConfig.h

int main(int argc, char* argv[])
{
  if (argc < 2) {
    // TODO 12: Create a print statement using Tutorial_VERSION_MAJOR
    //          and Tutorial_VERSION_MINOR
    // report version
    std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
              << Tutorial_VERSION_MINOR << std::endl;
    std::cout << "Usage: " << argv[0] << " number" << std::endl;
    return 1;
  }

  // convert input to double
  // TODO 4: Replace atof(argv[1]) with std::stod(argv[1])
  const double inputValue = std::atof(argv[1]);

  // calculate square root
  const double outputValue = sqrt(inputValue);
  std::cout << "The square root of " << inputValue << " is " << outputValue
            << std::endl;
  return 0;
}

Build and Run

同样,构建只需要如下步骤:

cd Step1_build
cmake --build .

这样就会在 ${PROJECT_BINARY_DIR}/Step1_build 中创建 TutorialConfig.h 文件了。