跳转到内容

cmake全流程使用

介绍

从一个简单的程序开始,逐步扩充,展开 CMake 的各项功能的讲解。

用户运行程序时将一个数字作为参数,程序计算该参数的平方根并在屏幕上显示出来。

准备

  • 系统:win10
  • 编辑器:VS Code
  • 编译器:Visual Studio 17 2022 MSVC 19.39.33523.0 或者 MinGW, g++ 13.2.0
  • CMake:3.28.2

打开 CMake 官方文档,遇到想详细了解的命令,随时到这里搜索。

创建一个空文件夹 sqrt,用 VSCode 打开。

基础

1. 最简单的项目

用一个单文件程序作为例子,先手动编译,后再用 CMake 编译,演示最基础的部分。

创建文件sqrt.cpp,内容如下:

#include <cmath>
#include <iostream>
#include <string>
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cout << "Usage: " << argv[0] << " num" << std::endl;
return 1;
}
const double inputValue{std::stod(argv[1])};
const double result{std::sqrt(inputValue)};
std::cout << "The square root of " << inputValue << " is " << result
<< std::endl;
return 0;
}

先不用 CMake,用 g++ 手动编译看看:

> g++ sqrt.cpp -o sqrt
> sqrt.exe 20
The square root of 20 is 4.47214
> sqrt.exe 9
The square root of 9 is 3

上述输出证明程序运行正常,下面我们在用 CMake 编译这段代码的时候,如果编译失败,从我们写得 CMake 配置找原因即可,就不用怀疑代码有问题了。

创建文件CMakeLists.txt,这是 CMake 的配置文件。配置文件写好后,用 CMake 构建项目有两个步骤,配置(Configure)和生成(Generate)。下面来写配置文件CMakeLists.txt

首先指定项目需要的最低的 CMake 版本,意思是,如果把项目放到另外一台电脑上构建,如果那台电脑上 CMake 的版本低于配置文件这里指定的版本,则无法构建,CMake 版本必须要高于这里指定的版本号。使用命令 cmake_mini_required()

cmake_minimum_required(VERSION 3.20)

接下来需要设置项目名称,使用命令project()

project(sqrt)

之后,需要告诉 CMake 要生成的可执行文件的名称和源文件名称,CMake将根据这些源文件生成该可执行文件,使用命令 add_executable()

add_executable(sqrt sqrt.cpp)

经过上述配置,就可以构建、运行一个最简单的CMake项目了。现在的文件CMakeLists.txt是这样的:

cmake_minimum_required(VERSION 3.20)
project(sqrt)
add_executable(sqrt sqrt.cpp)

打开终端(Ctrl + Shift + 反引号),输入如下命令构建、运行项目:

> cmake -B build
> cmake --build build
> build\Debug\sqrt.exe 9
The square root of 9 is 3

第一句 cmake -B buildcmake -S . -B build 的简写,意思是,从当前文件夹(.)寻找源代码文件,把生成的构建项目所需的文件放在文件夹build中。如果用的不是 MSVC,而是 MinGW,则可能需要像这样指定需要使用的生成器:cmake -G "MinGW Makefiles" -B build

第二句 cmake --build build 用于实际构建项目,告诉 CMake 从文件夹 build 中寻找所需的文件。

第三句 build\Debug\sqrt.exe 9 运行程序,如果不是用 MSVC 构建项目,而是用 MinGW,则可执行程序可能直接放在 build 目录下,而不是在 build\Debug 里。

2. 使用指定C++版本,std::format()

从 C++20 开始,可以使用 std::format() 格式化字符串,相比手动拼接字符串更加优雅。修改文件代码sqrt.cpp如下:

#include <cmath>
#include <format>
#include <iostream>
#include <string>
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cout << std::format("Usage: {} num", argv[0]) << std::endl;
return 1;
}
const double inputValue{std::stof(argv[1])};
const double result{std::sqrt(inputValue)};
std::cout << std::format("The square root of {} is {}", inputValue, result) << std::endl;
return 0;
}

手动编译运行项目:g++ sqrt.cpp -o sqrt -std=c++20,一切正常。注意-std=c++20用于指定C++版本为20。

用 CMake 构建项目会怎样样呢?再次运行命令构建项目,将得到一大堆报错,报错信息很晦涩。报错的原因是,std::format() 是 C++20 新引入的特性,而 CMake 构建项目时使用的却不是 C++20,而是比较低的标准。那么怎样让 CMake 用 C++20 标准构建项目呢?

修改文件CMakeLists.txt,设置变量CMAKE_CXX_STANDARDCMAKE_CXX_STANDARD_REQUIRED

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)

第一句 set(CMAKE_CXX_STANDARD 20) 指定 CMake 使用 C++20 标准;第二句 set(CMAKE_CXX_STANDARD_REQUIRED True) 要求编译器强制使用上一句指定的 C++ 标准。

现在的文件CMakeLists.txt是这样的:

cmake_minimum_required(VERSION 3.20)
project(sqrt)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_executable(sqrt sqrt.cpp)

再次构建项目,一切正常。

3. 添加版本号

我想要一个这样的效果,不带参数运行程序时,打印程序当前的版本号,如下(假想):

> sqrt.exe
sqrt Version 1.0
Usage: sqrt.exe num

这固然可以通过直接在源代码中添加字符串“sqrt Version 1.0”实现,但后续修改不方便。现在这个程序只有一个源代码文件还好说,如果是一个大型的项目,在很多处显示程序当前的版本号,每当程序更新一次就一个个去源代码里更改版本号很不现实。

我的想法是,在 文件CMakeLists.txt 里指定一个版本号,然后在源代码里使用这个版本号。随着程序的更新,我每次只需要更改这一处的版本号数值即可,很方便,很优雅。

修改文件CMakeLists.txt中的命令project(),改为:

project(sqrt VERSION 1.0)

通过上述命令设置了版本号后,CMake 会在幕后定义变量sqrt_VERSION_MAJORsqrt_VERSION_MINOR,在这里,两个变量的值分别为10。如果能够在程序中使用这两个变量,就可以得到版本号了。

接下来,创建一个新文件SqrtConfig.h.in(input header file,输入头文件),内容如下:

#define SQRT_VERSION_MAJOR @sqrt_VERSION_MAJOR@
#define SQRT_VERSION_MINOR @sqrt_VERSION_MINOR@

我们设想的是,在 CMake 构建项目时,把这个文件替换为 SqrtConfig.h(这是一个头文件),把文件的内容替换为:

#define SQRT_VERSION_MAJOR 1
#define SQRT_VERSION_MINOR 0

然后在源代码文件sqrt.cpp中引用这个头文件SqrtConfig.h,就相当于定义了两个宏SQRT_VERSION_MAJORSQRT_VERSION_MINOR,通过这两个宏就可以得到程序版本号。

怎么实现呢?

修改文件CMakeLists.txt,使用命令configure_file()

configure_file(SqrtConfig.h.in SqrtConfig.h)

通过这句命令,SqrtConfig.h.in 将替换为 SqrtConfig.h,替换后的文件 SqrtConfig.h 将被放在项目存放最后生成的二进制文件的文件夹(project binary directory,项目二进制目录)中。

现在可执行文件sqrt还不知道在哪里寻找头文件 SqrtConfig.h,所以也就没办法包含(include)它,所以需要使用命令target_include_directories()

add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")

这样,编译器就知道要去项目的二进制目录中去寻找头文件SqrtConfig.h了。

现在程序可以访问到在 CMake 配置文件中设定的程序版本号了,为了使用它,编辑源代码文件sqrt.cpp

// ...
#include "SqrtConfig.h"
// ...
if (argc != 2) {
std::cout << std::format("{} Version {}.{}", argv[0], SQRT_VERSION_MAJOR, SQRT_VERSION_MINOR) << std::endl;
std::cout << std::format("Usage: {} num", argv[0]) << std::endl;
return 1;
}
// ...

源代码文件sqrt.cpp 完整代码如下:

#include <cmath>
#include <format>
#include <iostream>
#include <string>
#include "SqrtConfig.h"
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cout << std::format("{} Version {}.{}", argv[0], SQRT_VERSION_MAJOR, SQRT_VERSION_MINOR) << std::endl;
std::cout << std::format("Usage: {} num", argv[0]) << std::endl;
return 1;
}
const double inputValue{std::stof(argv[1])};
const double result{std::sqrt(inputValue)};
std::cout << std::format("The square root of {} is {}", inputValue, result) << std::endl;
return 0;
}

此时,文件CMakeLists.txt完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")

构建项目,运行程序,和我们在这一小节开头设想的效果相同。

添加库(library)

4. 库的基础使用

前面的程序,在计算平方根的时候,调用的是<cmath>里的函数std::sqrt()。接下来我们计划改用自己编写的函数来计算平方根。我们自己编写一个“数学函数库”,计算平方根的函数是库里的一个函数。下面来编写该库。

在项目根目录下创建文件夹MathFunctions,并在其中创建如下几个文件:

\---MathFunctions
CMakeLists.txt
MathFunctions.cpp
MathFunctions.h
mysqrt.cpp
mysqrt.h

文件 MathFunctions.h 代码为:

#pragma once
namespace mathfunctions {
double sqrt(double x);
}

文件 MathFunctions.cpp 代码为:

#include "MathFunctions.h"
#include "mysqrt.h"
namespace mathfunctions {
double sqrt(double x) { return detail::mysqrt(x); }
} // namespace mathfunctions

文件 mysqrt.h 代码为:

#pragma once
namespace mathfunctions::detail {
double mysqrt(double x);
}

文件 mysqrt.cpp 代码为:

#include "mysqrt.h"
#include <format>
#include <iostream>
namespace mathfunctions::detail {
double mysqrt(double x) {
if (x <= 0) {
return 0;
}
double result{x};
for (int i{0}; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
double delta{x - (result * result)};
result = result + 0.5 * delta / result;
std::cout << std::format("Computing sqrt of {} to be {}", x, result)
<< std::endl;
}
return result;
}
} // namespace mathfunctions::detail

接下来要做的,是让 CMake 认为文件夹 MathFunctions 是一个库,以及在代码sqrt.cpp中调用该库。

在文件MathFunctions/CMakeLists.txt中需要告诉 CMake 要生成的库的名称和源文件名称,CMake将根据这些源文件生成该库,使用命令 add_library()

add_library(MathFunctions MathFunctions.cpp mysqrt.cpp)

在项目根目录下的CMakeLists.txt文件(以后就用/CMakeLists.txt表示项目根目录下的CMakeLists.txt文件)中,告诉 CMake 在构建项目时需要构建MathFunctions库,使用 add_subdirectory() 命令:

add_subdirectory(MathFunctions)

如果要在可执行文件 sqrt 中使用库 MathFunctions 中的函数,需要将 MathFunctions 链接到sqrt,使用target_link_libraries()命令:

target_link_libraries(sqrt PUBLIC MathFunctions)

现在可执行文件sqrt还不知道在哪里寻找库MathFunctions的头文件,所以需要再次使用命令target_include_directories()

target_include_directories(sqrt PUBLIC "${PROJECT_SOURCE_DIR}/MathFunctions")

表示sqrt可以从源代码根目录下的MathFunctions文件夹中寻找库MathFunctions的头文件。

对于可执行文件sqrt,现在在 CMakeLists.txt文件中就用了两次target_include_directories()命令:

target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_include_directories(sqrt PUBLIC "${PROJECT_SOURCE_DIR}/MathFunctions")

可以把它们合并为一句:

target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")

现在可执行目标sqrt可以调用库目标MathFunctions中的函数了。接下来修改sqrt的源文件sqrt.cpp

// ...
#include "MathFunctions.h"
//...
// const double result{std::sqrt(inputValue)};
const double result{mathfunctions::sqrt(inputValue)};
// ...

源文件sqrt.cpp完整代码如下:

#include <cmath>
#include <format>
#include <iostream>
#include <string>
#include "SqrtConfig.h"
#include "MathFunctions.h"
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cout << std::format("{} Version {}.{}", argv[0], SQRT_VERSION_MAJOR, SQRT_VERSION_MINOR) << std::endl;
std::cout << std::format("Usage: {} num", argv[0]) << std::endl;
return 1;
}
const double inputValue{std::stof(argv[1])};
// const double result{std::sqrt(inputValue)};
const double result{mathfunctions::sqrt(inputValue)};
std::cout << std::format("The square root of {} is {}", inputValue, result) << std::endl;
return 0;
}

此时,文件/CMakeLists.txt完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_subdirectory(MathFunctions)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")
target_link_libraries(sqrt PUBLIC MathFunctions)

文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp mysqrt.cpp)

构建项目,运行程序,一切正常。

5. 添加选项,option()

现在我想在库MathFunctions中添加一个选项,允许用户选择使用自定义的实现还是使用标准库的实现来计算平方根。

我的思路是,用户在构建 CMake 项目时,可以通过指定一个变量的值来选择使用不同的实现。CMake 接收到这个变量的值后,为一个宏设定不同的值。在代码里,根据宏的值的不同,调用不同的代码。

在文件MathFunctions/CMakeLists.txt中添加一个选项,使用命令option()

option(USE_MYMATH "Use our own math implementation" ON)

用户在配置 CMake 构建时,可以通过参数(-DUSE_MYMATH=ON-DUSE_MYMATH=OFF)更改该变量的值(开/关,ON/OFF)来控制程序使用的不同实现。用户只需要设定该变量一次,之后该变量的值会储存在缓存中,后续用户在每次配置时可自动从缓存中读取值,无需手动设置。

接下来继续修改文件 MathFunctions/CMakeLists.txt,当选项USE_MYMATHON时添加宏USE_MYMATH,使用命令 if()target_compile_definitions

if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif()

配置好选项和宏定义后,接下来在源代码中使用宏。修改文件MathFunctions/MathFunctions.cpp后完整代码如下:

#include "MathFunctions.h"
#ifdef USE_MYMATH
#include "mysqrt.h"
#endif
#include <cmath>
namespace mathfunctions {
double sqrt(double x) {
#ifdef USE_MYMATH
return detail::mysqrt(x);
#else
return std::sqrt(x);
#endif
}
} // namespace mathfunctions

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp mysqrt.cpp)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif()

文件 /CMakeLists.txt 未改变。

按照如下方式构建、运行程序,可以选择使用不同的实现:

> cmake -B build -DUSE_MYMATH=OFF # 或者 -DUSE_MYMATH=ON
> cmake --build build
> build\Debug\sqrt.exe 22

虽然看起来一切正常,但是这样仍然不够好,原因是,当USE_MYMATH=OFF时虽然 mysqrt.cpp 没有被使用,但该文件仍然会被编译。

怎样实现“若一个源文件没有被使用,就不编译它”?有两种方法。

不编译未使用的源文件 方法1

将库 MathFunctions 添加文件 mysqrt.cpp 的命令放在条件判断里,如果USE_MYMATH=OFF,就不将文件 mysqrt.cpp 添加到库MathFunctions。使用命令 target_sources()

# add_library(MathFunctions MathFunctions.cpp mysqrt.cpp)
add_library(MathFunctions MathFunctions.cpp)
if (USE_MYMATH)
target_sources(MathFunctions mysqrt.cpp)
# ...
endif()

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

# add_library(MathFunctions MathFunctions.cpp mysqrt.cpp)
add_library(MathFunctions MathFunctions.cpp)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_sources(MathFunctions mysqrt.cpp)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
endif()

文件 /CMakeLists.txt 未改变。

构建过程和之前一样,一切正常。

不编译未使用的源文件 方法2

将源文件 mysqrt.cpp 放到一个单独的库里,将添加这个库的命令(add_library())放到条件判断里。如果USE_MYMATH=OFF,就不在库MathFunctions中链接这个库;如果USE_MYMATH=ON,就在库MathFunctions中链接这个库。

修改文件MathFunctions/CMakeLists.txt如下,使用命令add_library()target_link_libraries()

# add_library(MathFunctions MathFunctions.cpp mysqrt.cpp)
add_library(MathFunctions MathFunctions.cpp)
# ...
if (USE_MYMATH)
# ...
add_library(SqrtLibrary STATIC mysqrt.cpp)
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()

USE_MYMATH=ON,条件判断成立,就添加库SqrtLibrary,并将其链接到库MathFunctions。当 USE_MYMATH=OFF,条件判断不成立,CMake 就不会把文件 mysqrt.cpp 添加到构建文件里,编译器就不会看到文件mysqrt.cpp,也就不会编译它了。

命令 add_library(SqrtLibrary STATIC mysqrt.cpp) 中的 STATIC 告诉 CMake 创建一个静态库。静态库会在链接时被完整地复制到目标(target,这里是指库MathFunctions)中。

命令 target_link_libraries(MathFunctions PRIVATE SqrtLibrary) 中的 PRIVATE 是指SqrtLibrary只会被链接到目标MathFunctions中,并不会传递到依赖于该目标(指MathFunctions)的其他目标中。

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

# add_library(MathFunctions MathFunctions.cpp mysqrt.cpp)
add_library(MathFunctions MathFunctions.cpp)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
add_library(SqrtLibrary STATIC mysqrt.cpp)
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()

文件 /CMakeLists.txt 未改变。

构建过程和之前一样,一切正常。

优雅地使用库

6. 添加使用要求(Usage Requirements)

前面,可执行目标sqrt在使用库MathFunctions时,不仅要建立二者的链接(命令target_link_libraries()),还要指出sqrt应该到哪里寻找MathFunctions的头文件(命令target_include_directories()),这样很不优雅。文件/CMakeLists.txt相关内容如下:

add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")
target_link_libraries(sqrt PUBLIC MathFunctions)

我想要的是,任何链接到库MathFunctions的目标,都自动包含库MathFunctions的头文件。

编辑文件MathFunctions/CMakeLists.txt,像下面这样使用命令 target_include_directories()

target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

上述命令中的INTERFACE表示,目录${CMAKE_CURRENT_SOURCE_DIR}将会被传递给任何链接到库MathFunctions的目标,而不会被传递给库MathFunctions自身(因为MathFunctions自己原本就知道自己应该到哪里寻找自己需要的头文件)。INTERFACE表示消费者(consumers)需要但生产者(producer)不需要的东西,这正好与PRIVATE的作用相反。

接下来从文件 /CMakeLists.txt 中删去为目标sqrt包含库MathFunctions头文件所在目录的命令:

# target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")

也就是说,目标 sqrt 链接到库MathFunctions 只需要这一句命令就可以了:

target_link_libraries(sqrt PUBLIC MathFunctions)

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_subdirectory(MathFunctions)
add_executable(sqrt sqrt.cpp)
# target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
add_library(SqrtLibrary STATIC mysqrt.cpp)
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

构建过程和之前一样,一切正常。

在本例中,我理解的“使用要求(Usage Requirements)”是,如果你(目标target)要想使用我这个库MathFunctions,(要求)必须要包含这个文件夹${CMAKE_CURRENT_SOURCE_DIR}

7. 使用接口库(Interface Library)设置C++标准

前面我们通过在 target_include_directories() 命令中使用 INTERFACE 关键字,指定了一个”库自己不需要,但是依赖该库的目标(target,本例中指可执行程序sqrt)需要”的文件夹。发散一下思维,是否可以利用这种特性为目标添加别的使用要求(Usage Requirements)?

下面我们利用接口库(INTERFACE library)为目标指定应当使用的 C++ 标准。

修改文件/CMakeLists.txt,首先删去(或注释掉)之前用于指定 C++ 标准的命令:

# set(CMAKE_CXX_STANDARD 20)
# set(CMAKE_CXX_STANDARD_REQUIRED True)

其次,添加一个名字为sqrt_compiler_flagsINTERFACE 库,并为该库添加编译器特征。使用命令add_library()target_compile_features()

add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)

最后,将各个目标sqrtSqrtLibraryMathFunctions都链接到接口库sqrt_compiler_flags,使用命令target_link_libraries()

/CMakeLists.txt

target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)

MathFunctions/CMakeLists.txt

target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
if (USE_MYMATH)
# ...
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
endif()

这样一来,指定C++20标准的编译器特征,将会被传递给那些链接到接口库target_compile_features的目标,也就相当于为这些目标设置了使用C++20标准。一次设置,多处使用。

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
# set(CMAKE_CXX_STANDARD 20)
# set(CMAKE_CXX_STANDARD_REQUIRED True)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
# target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}" "${PROJECT_SOURCE_DIR}/MathFunctions")
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
add_library(SqrtLibrary STATIC mysqrt.cpp)
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

构建过程和之前一样,一切正常。

添加生成器表达式(Generator Expressions)

8. 添加编译器警告标志

在编译程序时添加编译器警告标志,优点很多,有助于提升代码质量。如果直接用编译器(如g++)编译代码,若要添加警告标志,直接在命令后添加相应参数即可。如果用 CMake 构建项目,怎样添加警告标志呢。

CMake可以使用不同的编译器构建项目。警告标志因编译器而异。对于g++,我想启用的警告标志有:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused;对于msvc,我想启用的警告标志是/W3。关于msvc的各个警告标志的详细解释,可以看这个页面:/w, /W0, /W1, /W2, /W3, /W4, /w1, /w2, /w3, /w4, /Wall, /wd, /we, /wo, /Wv, /WX (Warning level) | Microsoft Learn

添加编译器警告标志,可以使用命令 target_compile_options(),这个命令专门用于添加编译选项(compile options,警告标志也是编译选项的一种)。但使用这个命令之前,首先需要判断当前设备上CMake 构建项目时使用的编译器是哪一种,这可以通过使用生成器表达式实现:

/CMakeLists.txt

set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")

gcc_like_cxxmsvc_cxx这两个变量必定不会同时为1,通过这两个变量的取值,就可以判断出当前设备上 CMake 使用的编译器。


$<COMPILE_LANG_AND_ID:language,compiler_ids>是一个生成器表达式,是$<COMPILE_LANGUAGE:language>$<LANG_COMPILER_ID:compiler_ids>两者组合后的缩写形式。

通过例子来理解这几个生成器表达式:

$<COMPILE_LANGUAGE:CXX>,意思是如果编译单元(compilation unit,我理解为“源文件”)使用的语言是CXX,则该生成器表达式的值为1,否则为0;又如$<LANG_COMPILER_ID:Clang,GNU,LCC>,意思是如果CMake使用的编译器能够与Clang,GNU,LCC其中之一匹配上,则该生成器表达式的值为1,否则为0

理解了这两个生成器表达式,那么$<COMPILE_LANG_AND_ID:language,compiler_ids>就好理解了,就是它们两个的组合罢了。如:$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>,意思是如果编译单元使用的语言是CXX,并且 CMake 使用的编译器能够和ARMClang,AppleClang,Clang,GNU,LCC其中之一匹配上,则该生成器表达式的值为为1,否则为0$<COMPILE_LANG_AND_ID: CXX,ARMClang,AppleClang,Clang,GNU,LCC>等价于$<AND:$<COMPILE_LANGUAGE:CXX>,$<LANG_COMPILER_ID:ARMClang,AppleClang,Clang,GNU,LCC>>

参考:cmake-generator-expressions(7)


gcc_like_cxxmsvc_cxx这两个变量必定不会同时为1,通过这两个变量的取值,就可以

现在已判断出当前设备上 CMake 使用的编译器,接下来就可以添加警告标志了。使用命令target_compile_options()

/CMakeLists.txt

target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>"
"$<${msvc_cxx}:-W3>"
)

可以把$<${msvc_cxx}:-W3>看作$<0:-W3>$<1:-W3>,它们称为0,1表达式(0 and 1 expressions)。$<0:-W3>会输出一个空字符串;$<1:-W3>会输出:后面的字符串,也就是-W3$<${gcc_like_cxx}:...>同理。

为接口库sqrt_compiler_flags添加警告标志后,这些警告标志将会被传递给那些链接到接口库target_compile_features的目标,也就相当于为这些库设置了警告标志。一次设置,多处使用。

还有一个问题。我们在构建当前项目时为目标添加了警告标志,后面我们的库MathFunctions会被导出(或者说“安装”),供其他项目使用。而其他的项目在构建时可能并不想添加这些警告标志。

但对于当前CMakeLists.txt的配置,sqrt_compiler_flags把警告标志传递给库MathFunctions,其他项目链接到库MathFunctions,警告标志会继续传递给这些库。因此需要修改文件/CMakeLists.txt,修改命令 target_compile_options()。使用生成器表达式$<BUILD_INTERFACE:...>

target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)

经过上述修改后,警告标志只会在项目构建期间被添加,而在安装期间不会被添加。也就是,安装后的项目不会继承这些警告标志,这些警告标志也就不会被传递给别的项目。

生成器表达式$<BUILD_INTERFACE:...>$<INSTALL_INTERFACE:...>是一组的,一个用于将内容...限定在构建阶段,一个用于将内容...限定在安装阶段。可以在这里详细了解。

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)

文件 MathFunctions/CMakeLists.txt 未变。

构建过程和之前一样,一切正常。如果代码有问题,将显式更丰富的警告信息。

安装和测试

安装

现在项目的功能已经非常完善了。接下来我想将项目的可执行程序和库安装到系统某个位置。这样就不再需要完整路径,而只需要使用程序名称,就可以使用程序sqrt(还需要把程序bin目录放到PATH环境变量里);别的项目也可以调用库MathFunctions

对于库MathFunctions,我想要将库文件和头文件分别安装到libinclude目录中;对于可执行目标sqrt,我想要将可执行文件和配置的头文件(configured header,指SqrtConfig.h,它由SqrtConfig.h.in生成)分别安装到bininclude目录中。指定安装规则可以使用命令install()

MathFunctions/CMakeLists.txt 的末尾,添加以下规则:

set(installable_libs MathFunctions sqrt_compiler_flags)
if (TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

/CMakeLists.txt 的末尾,添加以下规则:

install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)

这样,CMake 就知道该怎样安装项目了。

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)
install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
add_library(SqrtLibrary STATIC mysqrt.cpp)
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
set(installable_libs MathFunctions sqrt_compiler_flags)
if (TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

构建、安装过程如下所示:

> cmake -B build
> cmake --build build
> cmake --install build --prefix "C:\Users\Aoyu\Desktop\sqrt\test"

如果安装命令中不指定--prefix,项目会被默认安装到C:\Program Files (x86),但由于没有管理员权限,可能安装失败。因此,如果你想要将项目安装到C:\Program Files (x86),可以另外打开一个以管理员身份运行的 powershell 窗口。

如果使用的是 MSVC,它支持多个构建配置(如 Debug 和 Release 等),构建、安装命令可能像这样:

cmake -B build
cmake --build build --config Release
cmake --install build --config Release --prefix "C:\Users\Aoyu\Desktop\sqrt\test"

安装后,目录的结构应该像这样:

PS C:\Users\Aoyu\Desktop\sqrt\test> tree /A /F
C:.
+---bin
| sqrt.exe
|
+---include
| MathFunctions.h
| SqrtConfig.h
|
\---lib
MathFunctions.lib
SqrtLibrary.lib

测试 CTest

单元测试的意义和重要性无需多言。接下来为项目添加一些基础测试,使用 CTest 验证可执行程序sqrt能否正常工作。使用命令enable_testing() 启用测试,使用命令add_test() 添加测试:

/CMakeLists.txt 的末尾,添加下述命令:

启用测试:

enable_testing()

测试程序是否运行直到结束、无报错:

add_test(NAME Runs COMMAND sqrt 25)

对于上述测试,我们不关心计算结果,只是验证程序能否平安运行到结束。

测试当提供的参数不正确时程序是否打印帮助信息:

add_test(NAME Usage COMMAND sqrt)
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME Usage2 COMMAND sqrt 6 7)
set_tests_properties(Usage2 PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")

set_tests_properties() 用于设置测试的属性。原型为:

set_tests_properties(<tests>...
[DIRECTORY <dir>]
PROPERTIES <prop1> <value1>
[<prop2> <value2>]...)

命令 set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num") 的含义是,为测试Usage设置属性PASS_REGULAR_EXPRESSION,如果程序的输出不匹配正则表达式Usage:.*num,就表示测试失败。

测试程序的计算结果是否正确:

add_test(NAME StandardUse COMMAND sqrt 4)
set_tests_properties(StandardUse PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2")

为测试程序计算结果的正确性,添加更多的测试。为了方便添加测试,使用函数来帮忙:

function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()
do_test(sqrt 4 "4 is 2")
do_test(sqrt 9 "9 is 3")
do_test(sqrt 5 "5 is 2.236")
do_test(sqrt 7 "7 is 2.645")
do_test(sqrt 25 "25 is 5")
do_test(sqrt -25 "-25 is (-nan|nan|0)")
do_test(sqrt 0.0001 "0.0001 is 0.01")

使用的函数名为do_test,它接收三个参数,分别是要测试的可执行程序名(target)、要传递给程序的参数(arg)、程序计算结果应匹配的正则表达式(result)。

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)
install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)
enable_testing()
add_test(NAME Runs COMMAND sqrt 25)
add_test(NAME Usage COMMAND sqrt)
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME Usage2 COMMAND sqrt 6 7)
set_tests_properties(Usage2 PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME StandardUse COMMAND sqrt 4)
set_tests_properties(StandardUse PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2")
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()
do_test(sqrt 4 "4 is 2")
do_test(sqrt 9 "9 is 3")
do_test(sqrt 5 "5 is 2.236")
do_test(sqrt 7 "7 is 2.645")
do_test(sqrt 25 "25 is 5")
do_test(sqrt -25 "-25 is (-nan|nan|0)")
do_test(sqrt 0.0001 "0.0001 is 0.01")

文件 MathFunctions/CMakeLists.txt 未变。

构建、测试过程如下:

# 构建
cmake -B build
cmake --build build
# 测试
cd build
ctest -N
ctest -VV # 如果用的 MSVC,改为 ctest -C Debug -VV

MSVC 支持多个构建配置(如 Debug 和 Release ),如果要测试Release配置,可能像这样:

# 构建
cmake -B build
cmake --build build --config Release
# 测试
cd build
ctest -N
ctest -C Release -VV # 必须要指定-C

经过测试,我确实在程序中发现一个bug,ctest 的部分输出如下:

11: Test command: C:\Users\Aoyu\Desktop\sqrt\build\Release\sqrt.exe "0.0001"
11: Working Directory: C:/Users/Aoyu/Desktop/sqrt/build
11: Test timeout computed to be: 10000000
11: Computing sqrt of 9.999999747378752e-05 to be 0.5000499999987369
11: Computing sqrt of 9.999999747378752e-05 to be 0.2501249899978426
11: Computing sqrt of 9.999999747378752e-05 to be 0.12526239505184017
11: Computing sqrt of 9.999999747378752e-05 to be 0.06303035961056809
11: Computing sqrt of 9.999999747378752e-05 to be 0.03230844830392213
11: Computing sqrt of 9.999999747378752e-05 to be 0.017701806947227155
11: Computing sqrt of 9.999999747378752e-05 to be 0.011675473806232975
11: Computing sqrt of 9.999999747378752e-05 to be 0.010120218245346998
11: Computing sqrt of 9.999999747378752e-05 to be 0.01000071391248447
11: Computing sqrt of 9.999999747378752e-05 to be 0.009999999899180125
11: The square root of 9.999999747378752e-05 is 0.009999999899180125
11/11 Test #11: Comp0.0001 .......................***Failed Required regular expression not found. Regex=[0.0001 is 0.01
] 0.01 sec
91% tests passed, 1 tests failed out of 11

在将字符串"0.0001"转换为双精度浮点数值后,用std::format()打印该值时,打印的是用科学计数法表示的值9.999999747378752e-05,而不是0.0001。怎样解决?

sqrt.cppMathFunctions\mysqrt.cpp中,为std::format()指定格式化精度,相关语句如下:

sqrt.cpp
std::cout << std::format("The square root of {:.4g} is {:.6g}", inputValue, result) << std::endl;
// MathFunctions\mysqrt.cpp
std::cout << std::format("Computing sqrt of {:.4g} to be {:.6g}", x, result)
<< std::endl;

再次构建、测试,全部通过。

测试仪表盘(Testing Dashboard)

CDash

可以把 CDash 理解成一个管理面板,上面汇总了每一次 CTest 的测试结果,并且可以根据这些测试结果数据生成统计信息。

接下来演示在本地运行测试后,怎样将本次的测试结果自动上传到 CDash。实际使用时,应当在 CDash 上创建一个 CTest 项目,然后从项目的设置页面下载到一个 cmake 脚本CTestConfig.cmake,将这个脚本放到本地项目的根目录。这里只是演示,因此使用演示项目CMakeTutorial,该脚本内容如下:

CTestConfig.cmake

set(CTEST_PROJECT_NAME "CMakeTutorial")
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")
set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=CMakeTutorial")
set(CTEST_DROP_SITE_CDASH TRUE)

然后编辑文件/CMakeLists.txt,将enable_testing()替换为include(CTest)

# enable_testing()
include(CTest)

有了include(CTest),在 CTest 启动时,会自动加载 CTestConfig.cmake 文件,并根据其中的配置信息来设置 CTest 的行为。

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)
install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)
# enable_testing()
include(CTest)
add_test(NAME Runs COMMAND sqrt 25)
add_test(NAME Usage COMMAND sqrt)
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME Usage2 COMMAND sqrt 6 7)
set_tests_properties(Usage2 PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME StandardUse COMMAND sqrt 4)
set_tests_properties(StandardUse PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2")
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()
do_test(sqrt 4 "4 is 2")
do_test(sqrt 9 "9 is 3")
do_test(sqrt 5 "5 is 2.236")
do_test(sqrt 7 "7 is 2.645")
do_test(sqrt 25 "25 is 5")
do_test(sqrt -25 "-25 is (-nan|nan|0)")
do_test(sqrt 0.0001 "0.0001 is 0.01")

构建、测试过程如下:

cmake -B build
# 注意不需要 cmake --build build
cd build
ctest [-VV] -D Experimental
# 如果用的是 MSVC,需要添加 -C 选项,指定 Debug 或 Release 等
ctest [-VV] -C Debug -D Experimental

命令ctest [-VV] -D Experimental将构建项目、运行所有的测试,并把结果提交到 CDash。打开my.cdash.org/index.php?project=CMakeTutorial可以看到本次测试的报告和统计信息。

添加系统自省(System Introspection)

检查依赖项可用性(Assessing Dependency Availability)

在库SqrtLibrary的源文件mysqrt.cpp中,我们通过使用循环来计算 x 的平方根,这样做的速度不佳,因此我打算换用另一种计算方式:double result{std::exp(std::log(x) * 0.5)};

std::exp()std::log()定义在头文件<cmath>中,假如在不同的平台上不一定有 std::exp()std::log() 这两个函数(只是假如,为了演示的需要)。因此,在使用它们前需要先做一个判断,如果当前构建项目的平台上可以使用这两个函数,那就用,如果不能用,就继续用之前的方式来计算。怎样实现?

可以使用模块CheckCXXSourceCompiles中的函数check_cxx_source_compiles()来实现。编辑文件MathFunctions/CMakeLists.txt,改动内容如下:

# ...
if (USE_MYMATH)
# ...
add_library(SqrtLibrary STATIC mysqrt.cpp)
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::log(1.0);
return 0;
}
" HAVE_LOG)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::exp(1.0);
return 0;
}
" HAVE_EXP)
if (HAVE_LOG AND HAVE_EXP)
target_compile_definitions(SqrtLibrary PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
# ...

如果作为函数check_cxx_source_compiles()第一个参数的代码片段可以编译通过,那么就将第二个参数设置为True,否则设置为False。上面的命令测试了std::exp()std::log()两个函数,并将结果保存在变量HAVE_LOGHAVE_EXP里。如果两个变量都为True,就表明这两个函数都可用,这时使用命令target_compile_definitions()在库SqrtLibrary中定义两个宏HAVE_LOGHAVE_EXP,这样就把 CMake 的变量传递到了源代码,源代码就能知道资源是否可用。

接下来修改源代码mysqrt.cpp,变动内容如下:

// ...
double mysqrt(double x) {
if (x <= 0) {
return 0;
}
#if defined(HAVE_LOG) && defined(HAVE_EXP)
double result{std::exp(std::log(x) * 0.5)};
std::cout << std::format("Computing sqrt of {:.4g} to be {:.6g} using log and exp", x, result)
<< std::endl;
#else
double result{x};
for (int i{0}; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
double delta{x - (result * result)};
result = result + 0.5 * delta / result;
std::cout << std::format("Computing sqrt of {:.4g} to be {:.6g}", x, result)
<< std::endl;
}
#endif
return result;
}
// ...

如果定义了宏 HAVE_LOGHAVE_EXP#if defined(HAVE_LOG) && defined(HAVE_EXP)),就使用新的计算平方根的方式,如果未定义,就继续使用旧的方式。

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
add_library(SqrtLibrary STATIC mysqrt.cpp)
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::log(1.0);
return 0;
}
" HAVE_LOG)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::exp(1.0);
return 0;
}
" HAVE_EXP)
if (HAVE_LOG AND HAVE_EXP)
target_compile_definitions(SqrtLibrary PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
set(installable_libs MathFunctions sqrt_compiler_flags)
if (TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

文件 /CMakeLists.txt 未变。

构建、运行过程与之前相同,不同的是,在生成构建项目所需文件时(cmake -B build)会测试指定功能的可用性,输出内容节选如下:

> cmake -B build
# ...
-- Performing Test HAVE_LOG
-- Performing Test HAVE_LOG - Success
-- Performing Test HAVE_EXP
-- Performing Test HAVE_EXP - Success
# ...
> cmake --build build
# ...

添加自定义命令(Custom Command)

在程序中使用自定义命令生成的文件(Generated File)

前面,我们自己编写了函数来计算平方根,使用了 for 循环。后面又更改了实现,如果 std::log()std::exp() 可用,就使用 std::log()std::exp()来计算平方根。

我想要制作一个表格,对于常用的数字,比如 0 到 10,直接查表就可以得到平方根的计算结果,而不用再完整走一遍计算流程,这样速度可以更快。对于表里没有的数字,还是和之前一样计算。怎么实现?

我的思路是,首先写一个制作表格的程序,程序运行后可以生成一个头文件,头文件包含一个“表格”(数组)储存了预先计算好的结果;在构建项目时调用这个程序生成头文件;在源代码中引入这个头文件,做一个条件判断,当接收到的数值在表格内的时候,直接返回表格内对应的结果,如果不在表格内,照常计算。

创建源文件MathFunctions/MakeTable.cpp,内容如下:

#include <cmath>
#include <fstream>
#include <iostream>
int main(int argc, char *argv[])
{
if (argc != 2)
{
return 1;
}
std::ofstream outFile{argv[1], std::ios_base::out};
if (!outFile.good())
{
std::cout << "Error while opening output file " << argv[1] << std::endl;
return -1;
}
outFile << "double sqrtTable[] = {" << std::endl;
for (int i{0}; i < 10; ++i)
{
outFile << std::sqrt(static_cast<double>(i)) << "," << std::endl;
}
outFile << "0};" << std::endl;
return 0;
}

创建脚本文件MathFunctions/MakeTable.cmake,内容如下:

add_executable(MakeTable MakeTable.cpp)
target_link_libraries(MakeTable PRIVATE sqrt_compiler_flags)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
DEPENDS MakeTable
)

脚本中的命令,首先添加一个可执行程序MakeTable,并将其链接到接口库sqrt_compiler_flags;然后添加了一个自定义命令,该命令告诉CMake,它将运行命令MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h,运行完后将生成一个文件${CMAKE_CURRENT_BINARY_DIR}/Table.h,这个命令依赖于目标(target)MakeTable,所以得等它(指MakeTable)编译完后才能调用此命令,并且,如果MakeTable发生了改变,这个自定义命令也需要重新执行。

接下来,编辑文件MathFunctions/CMakeLists.txt,变动内容如下:

# ...
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
include(MakeTable.cmake)
add_library(SqrtLibrary STATIC mysqrt.cpp ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
target_include_directories(SqrtLibrary PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
# ...

在文件MathFunctions/CMakeLists.txt顶部包含MakeTable.cmakeMakeTable.cmake里的命令会先于下面添加库SqrtLibrary的命令运行,add_library(SqrtLibrary STATIC mysqrt.cpp ${CMAKE_CURRENT_BINARY_DIR}/Table.h)又指出mysqrt.cpp依赖于${CMAKE_CURRENT_BINARY_DIR}/Table.h,因此 CMake 就知道了要等MakeTable.cmake里的自定义命令运行完毕后才能编译SqrtLibrary。命令target_include_directories()的作用是让mysqrt.cpp能够找到Table.h(用于#include "Table.h")。

最后,修改源代码文件MathFunctions/mysqrt.cpp,修改后的代码如下所示:

#include "mysqrt.h"
#include "Table.h"
#include <format>
#include <iostream>
#include <cmath>
namespace mathfunctions::detail {
double mysqrt(double x) {
if (x <= 0) {
return 0;
}
double result {};
if (x >= 1 && x < 10) {
std::cout << "Use the table to help find an initial value " << std::endl;
result = sqrtTable[static_cast<int>(x)];
}
else {
#if defined(HAVE_LOG) && defined(HAVE_EXP)
result = std::exp(std::log(x) * 0.5);
std::cout << std::format("Computing sqrt of {:.4g} to be {:.6g} using log and exp", x, result)
<< std::endl;
#else
result = x;
for (int i{0}; i < 10; ++i) {
if (result <= 0) {
result = 0.1;
}
double delta{x - (result * result)};
result = result + 0.5 * delta / result;
std::cout << std::format("Computing sqrt of {:.4g} to be {:.6g}", x, result)
<< std::endl;
}
#endif
}
return result;
}
} // namespace mathfunctions::detail

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
include(MakeTable.cmake)
add_library(SqrtLibrary STATIC mysqrt.cpp ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
target_include_directories(SqrtLibrary PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::log(1.0);
return 0;
}
" HAVE_LOG)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::exp(1.0);
return 0;
}
" HAVE_EXP)
if (HAVE_LOG AND HAVE_EXP)
target_compile_definitions(SqrtLibrary PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
set(installable_libs MathFunctions sqrt_compiler_flags)
if (TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

文件 /CMakeLists.txt 未变。

构建、运行过程与之前相同。

打包安装程序

之前,我们在“安装和测试”这一小节中,安装了从源代码构建的二进制文件。这一小节不一样,我们想把程序分发给其他人使用,而他们不需要从源代码开始构建项目,只是把我们预先构建好的二进制文件安装在它们系统上,之后就可以使用了。我们使用 CPack 来实现。

编辑文件/CMakeLists.txt,在末尾添加下述内容:

include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/License.txt)
set(CPACK_PACKAGE_VERSION_MAJOR ${sqrt_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${sqrt_VERSION_MINOR})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)

首先引入模块InstallRequiredSystemLibraries,该模块包括当前平台项目所需的所有运行时库。接下来设置一些变量,告诉 CPack 从哪里找项目许可证文本、项目版本号、使用 TGZ 作为源代码包生成器(创建完整源代码树的存档时压缩包的格式将是.tgz)。最后,引入模块CPack,它会使用上面设置的变量和一些当前系统的其他属性来设置安装程序。

在项目根目录下创建文件License.txt,内容我随便写的”Hello, World!”。

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)
install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)
# enable_testing()
include(CTest)
add_test(NAME Runs COMMAND sqrt 25)
add_test(NAME Usage COMMAND sqrt)
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME Usage2 COMMAND sqrt 6 7)
set_tests_properties(Usage2 PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME StandardUse COMMAND sqrt 4)
set_tests_properties(StandardUse PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2")
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()
do_test(sqrt 4 "4 is 2")
do_test(sqrt 9 "9 is 3")
do_test(sqrt 5 "5 is 2.236")
do_test(sqrt 7 "7 is 2.645")
do_test(sqrt 25 "25 is 5")
do_test(sqrt -25 "-25 is (-nan|nan|0)")
do_test(sqrt 0.0001 "0.0001 is 0.01")
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/License.txt)
set(CPACK_PACKAGE_VERSION_MAJOR ${sqrt_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${sqrt_VERSION_MINOR})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)

文件 MathFunctions/CMakeLists.txt 未变。

构建项目、打包安装程序的过程如下:

# 照常构建项目
> cmake -B build
> cmake --build build
# 打包安装程序1
> cd build
> cpack # 在 windows 下需要安装 NSIS
# 打包安装程序2
> cd build
> cpack -G ZIP -C Debug
# 使用 -G 选项指定生成器(generator)
# 对于多个构建配置(如 Debug 和 Release 等)的情况,使用 -C 选项指定配置
# 创建完整源代码树的存档
> cd build
> cpack --config CPackSourceConfig.cmake

选择静态库或动态库

前面,构建库MathFunctions时,我们一直构建的都是静态库。接下来我想要使用动态库。

在添加库(命令add_library())时,如果不显式指定库类型(STATICSHAREDMODULE 或 OBJECT),CMake 将其默认构建为静态库(STATIC)。如果想要修改这一默认行为,则可以添加选项BUILD_SHARED_LIBS,使用option()命令:

set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

如果选项BUILD_SHARED_LIBSON,则如果在命令add_library()中没有显式指定库类型,则默认构建动态库(SHARED)。更多细节可看:BUILD_SHARED_LIBS

对于三个变量CMAKE_ARCHIVE_OUTPUT_DIRECTORYCMAKE_LIBRARY_OUTPUT_DIRECTORYCMAKE_RUNTIME_OUTPUT_DIRECTORY,我的理解比较粗浅。变量CMAKE_ARCHIVE_OUTPUT_DIRECTORY指生成的静态库(.lib, .a)所在文件夹;变量 CMAKE_LIBRARY_OUTPUT_DIRECTORY指在非Windows平台生成的动态库文件(.so)所在文件夹;变量CMAKE_RUNTIME_OUTPUT_DIRECTORY指可执行程序、在Windows平台生成的动态库文件(动态库.dll和导入库.lib)所在文件夹。

由于现在使用的是动态库,可执行程序sqrt将在运行时(而不是编译时)链接到库MathFunctions,如果库MathFunctions的动态库文件(类似MathFunctions.dll的名字)和可执行文件不在同一个目录,那么就无法找到动态库,所以程序就不能正确运行。所以需要把动态库和可执行文件放在同一个目录中(这里是${PROJECT_BINARY_DIR})。

如果使用 MinGW,按照和以前一样的步骤构建项目,可以构建成功,一切正常。但是 MSVC 将会构建失败,会提示LINK : fatal error LNK1104: 无法打开文件“Debug\MathFunctions.lib” [C:\Users\Aoyu\Desktop\sqrt\build\sqrt.vcxproj],这是由 MSVC 链接动态库的机制导致的:(来自:这里)通过 MSVC 编译DLL项目A时,会生成目标DLL文件和同名的LIB文件。在其他项目B引用该DLL时,在编译时项目B需要把LIB文件链接到项目中,编译完成后,运行时才需要DLL文件。上面我们在构建项目时只是生成了.dll文件,没有同名的.lib文件,找不到文件,所以就报错了。

解决办法有两种。

方法1

一种是传统的方式,修改源代码,使用__declspec(dllexport)__declspec(dllimport),但由于修改了源代码(专门为生成动态库修改了源代码),使用这种方式,就不能方便地在生成动态库和生成静态库之间切换了。

编辑文件MathFunctions\MathFunctions.h,改为:

#pragma once
#if defined(_WIN32)
# if defined(EXPORTING_MYMATH)
# define DECLSPEC __declspec(dllexport)
# else
# define DECLSPEC __declspec(dllimport)
# endif
#else // non windows
# define DECLSPEC
#endif
namespace mathfunctions {
double DECLSPEC sqrt(double x); # 注意这里使用了 DECLSPEC
}

若当前平台为Windows(定义了宏_WIN32),并且正在生成DLL文件(根据宏EXPORTING_MYMATH来区分),就将宏DECLSPEC定义为__declspec(dllexport);若当前平台为Windows并且是在调用该DLL文件,就将宏DECLSPEC定义为__declspec(dllimport)。如果非Windows平台,则宏DECLSPEC定义为空。在函数声明中使用宏DECLSPEC,指示导出(被导出到动态链接库中)还是导入(从动态链接库中导入)该函数,也就是区分是正在生成动态库,还是在调用动态库。

接下来编辑文件MathFunctions\CMakeLists.txt,在编译时定义宏EXPORTING_MYMATH

target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)
install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)
# enable_testing()
include(CTest)
add_test(NAME Runs COMMAND sqrt 25)
add_test(NAME Usage COMMAND sqrt)
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME Usage2 COMMAND sqrt 6 7)
set_tests_properties(Usage2 PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME StandardUse COMMAND sqrt 4)
set_tests_properties(StandardUse PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2")
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()
do_test(sqrt 4 "4 is 2")
do_test(sqrt 9 "9 is 3")
do_test(sqrt 5 "5 is 2.236")
do_test(sqrt 7 "7 is 2.645")
do_test(sqrt 25 "25 is 5")
do_test(sqrt -25 "-25 is (-nan|nan|0)")
do_test(sqrt 0.0001 "0.0001 is 0.01")
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/License.txt)
set(CPACK_PACKAGE_VERSION_MAJOR ${sqrt_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${sqrt_VERSION_MINOR})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
include(MakeTable.cmake)
add_library(SqrtLibrary STATIC mysqrt.cpp ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
target_include_directories(SqrtLibrary PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::log(1.0);
return 0;
}
" HAVE_LOG)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::exp(1.0);
return 0;
}
" HAVE_EXP)
if (HAVE_LOG AND HAVE_EXP)
target_compile_definitions(SqrtLibrary PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")
set(installable_libs MathFunctions sqrt_compiler_flags)
if (TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

文件 /CMakeLists.txt 未变。

构建项目,一切正常。

方法2

另一种方式是使用变量CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS。编辑文件/CMakeLists.txt,设置该变量为ON

set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

这个选项CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS,用于设定目标(可执行程序或库)的属性WINDOWS_EXPORT_ALL_SYMBOLS的默认值。关于属性WINDOWS_EXPORT_ALL_SYMBOLS更深入的信息可看这里

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)
install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)
# enable_testing()
include(CTest)
add_test(NAME Runs COMMAND sqrt 25)
add_test(NAME Usage COMMAND sqrt)
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME Usage2 COMMAND sqrt 6 7)
set_tests_properties(Usage2 PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME StandardUse COMMAND sqrt 4)
set_tests_properties(StandardUse PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2")
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()
do_test(sqrt 4 "4 is 2")
do_test(sqrt 9 "9 is 3")
do_test(sqrt 5 "5 is 2.236")
do_test(sqrt 7 "7 is 2.645")
do_test(sqrt 25 "25 is 5")
do_test(sqrt -25 "-25 is (-nan|nan|0)")
do_test(sqrt 0.0001 "0.0001 is 0.01")
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/License.txt)
set(CPACK_PACKAGE_VERSION_MAJOR ${sqrt_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${sqrt_VERSION_MINOR})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
include(MakeTable.cmake)
add_library(SqrtLibrary STATIC mysqrt.cpp ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
target_include_directories(SqrtLibrary PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
# set_target_properties(SqrtLibrary PROPERTIES POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS})
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::log(1.0);
return 0;
}
" HAVE_LOG)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::exp(1.0);
return 0;
}
" HAVE_EXP)
if (HAVE_LOG AND HAVE_EXP)
target_compile_definitions(SqrtLibrary PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
set(installable_libs MathFunctions sqrt_compiler_flags)
if (TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

构建项目,一切正常。

添加导出配置(Adding Export Configuration)

旧的调用库的方法

前面,我们在“安装和测试”这一小节中,安装了从源代码构建的二进制文件;在“打包安装程序”这一小节,我们把程序打包为二进制文件分发给其他人使用。在别的项目中怎么使用呢。

构建、打包项目的过程如下:

> cmake -G "MinGW Makefiles" -B build
> cmake --build build
> cd build
> cpack -G ZIP -C Debug

将项目二进制文件安装到(解压到)如下位置:C:\Users\wenidc\Desktop\sqrt-1.0-win64\sqrt-1.0-win64。文件列表如下:

C:\Users\wenidc\Desktop\sqrt-1.0-win64>tree /A /F
文件夹 PATH 列表
卷序列号为 7C9D-1250
C:.
\---sqrt-1.0-win64
+---bin
| sqrt.exe
|
+---include
| MathFunctions.h
| SqrtConfig.h
|
\---lib
libMathFunctions.dll
libMathFunctions.dll.a
libSqrtLibrary.a

(此时运行bin目录里的sqrt.exe将失败,因为没有把动态库MathFunctions相关的文件放到bin目录下面,sqrt.exe找不到动态库。后面会修改相关配置,但在此处我们只是测试外部项目调用库MathFunctions,不影响这里的演示。)

有另一个项目项目test1,源文件test1/main.cpp内容如下:

// 计算数字 4 的平方根
#include <iostream>
#include "MathFunctions.h"
int main() {
std::cout << mathfunctions::sqrt(4) << std::endl;
return 0;
}

我们分别看看直接用命令行(g++)编译时,以及用CMake管理项目时,应该怎样调用库MathFunctions

命令行编译

g++ main.cpp -o main -IC:/Users/wenidc/Desktop/sqrt-1.0-win64/sqrt-1.0-win64/include -LC:/Users/wenidc/Desktop/sqrt-1.0-win64/sqrt-1.0-win64/lib -lMathFunctions

选项-I指定头文件路径,-L指定动态库文件路径,-l指定需要链接的动态库名称。

然后需要把sqrt-1.0-win64/sqrt-1.0-win64/lib里面的库文件放到与生成的main.exe相同目录下,这样才能成功运行main.exe。(动态库必须和可执行文件放到同一个目录吗?可以问问GPT)

CMake构建

test1/CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(test1)
add_executable(test1 main.cpp)
# 链接到库 MathFunctions
target_link_libraries(test1 PRIVATE MathFunctions)
set(MathFunctions_DIR C:\\Users\\wenidc\\Desktop\\sqrt-1.0-win64\\sqrt-1.0-win64)
# 指定库 MathFunctions 的头文件的路径
target_include_directories(test1 PRIVATE "${MathFunctions_DIR}/include")
# 指定库 MathFunctions 的动态链接库的路径
target_link_directories(test1 PRIVATE "${MathFunctions_DIR}/lib")

构建过程如下:

> cmake -G "MinGW Makefiles" -B build
> cmake --build build

将生成可执行文件test1.exe。但这时test1.exe还运行不了,还需要将sqrt-1.0-win64/sqrt-1.0-win64/lib里面的库文件放到与生成的test1.exe相同的目录下(也就是build目录中)。

让调用库更方便

从上面可以看到,我们使用库MathFunctions的方式与使用其他普通的、未使用 CMake 构建的动态库没有什么区别。下面为项目sqrt添加必要的信息,其他 CMake 项目只需使用命令find_package()就可以很方便地使用库MathFunctions

首先编辑MathFunctions/CMakeLists.txt,修改install()命令:

# install(TARGETS ${installable_libs} DESTINATION lib)
install(TARGETS ${installable_libs}
EXPORT MathFunctionsTargets
DESTINATION lib
)

新增的EXPORT关键字,用于生成一个 CMake 脚本文件,这个脚本文件描述了(其他项目)怎样从项目安装后的目录中导入目标(列在${installable_libs}里的这些目标)。但注意,EXPORT 关键字本身不会直接生成文件,而是将指定的目标(列在${installable_libs}里的这些目标)导出到一个命名的导出集中(名称为MathFunctionsTargets)。若要生成 CMake 文件,需要使用命令install(EXPORT ...)

编辑文件/CMakeLists.txt,在底部添加:

install(EXPORT MathFunctionsTargets
FILE MathFunctionsTargets.cmake
DESTINATION lib/cmake/MathFunctions
)

上面的命令,用于根据MathFunctionsTargets生成文件MathFunctionsTargets.cmake

构建项目,将遇到以下报错:

> cmake -G "MinGW Makefiles" -B build
-- Configuring done (0.3s)
CMake Error in MathFunctions/CMakeLists.txt:
Target "MathFunctions" INTERFACE_INCLUDE_DIRECTORIES property contains
path:
"C:/Users/Aoyu/Desktop/sqrt/MathFunctions"
which is prefixed in the source directory.
-- Generating done (0.2s)
CMake Generate step failed. Build files cannot be regenerated correctly.

报错原因是在MathFunctions/CMakeLists.txt中有这一句命令:

target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

这句命令的含义是,所有链接到 MathFunctions 的目标(targets),都需要包含文件夹${CMAKE_CURRENT_SOURCE_DIR}里的头文件。但是${CMAKE_CURRENT_SOURCE_DIR}是一个绝对路径,如果项目打包后安装在其他电脑上,还去这个路径去找头文件,肯定会出错。CMake 察觉到了这个问题,所以抛出了一个错误。

如何解决?思路是,在构建时,依然从${CMAKE_CURRENT_SOURCE_DIR}中寻找头文件;在安装后,从安装目录中的include文件夹中寻找头文件。

编辑文件MathFunctions/CMakeLists.txt,进行如下修改:

# target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(MathFunctions INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)

关于生成器表达式$<BUILD_INTERFACE:...>$<INSTALL_INTERFACE:...>,本文前面有介绍。

修改后,构建项目、安装项目:

> cmake -G "MinGW Makefiles" -B build
> cmake --build build
> cmake --install build --prefix "C:\Users\wenidc\Desktop\sqrt\test_install"

test_install\lib\cmake\MathFunctions中可以看到成功生成的文件MathFunctionsTargets.cmake。(构建目录build里面是没有这个文件的)

想让其他项目的find_package()命令能够找到本项目中的库MathFunctions,需要在项目安装目录的lib\cmake\MathFunctions目录下(和文件MathFunctionsTargets.cmake同目录)放一个文件MathFunctionsConfig.cmake,这个文件使用”模板”生成:

在项目根目录下创建新文件Config.cmake.in

@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/MathFunctionsTargets.cmake")

变量@PACKAGE_INIT@在最终生成的文件中将被替换为“获取项目安装目录根目录的绝对路径”的代码。一份生成的文件MathFunctionsConfig.cmake示例如下:

####### Expanded from @PACKAGE_INIT@ by configure_package_config_file() #######
####### Any changes to this file will be overwritten by the next CMake run ####
####### The input file was Config.cmake.in ########
get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE)
####################################################################################
include("${CMAKE_CURRENT_LIST_DIR}/MathFunctionsTargets.cmake")

变量CMAKE_CURRENT_LIST_DIR指当前文件MathFunctionsConfig.cmake所在的文件夹(项目安装目录/lib\cmake\MathFunctions),语句include("${CMAKE_CURRENT_LIST_DIR}/MathFunctionsTargets.cmake")指将相同文件夹下的文件MathFunctionsTargets.cmake中的代码包含进来。

修改文件CMakeLists.txt,在末尾添加如下代码:

include(CMakePackageConfigHelpers)
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
INSTALL_DESTINATION "lib/cmake/MathFunctions"
NO_SET_AND_CHECK_MACRO
NO_CHECK_REQUIRED_COMPONENTS_MACRO
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
DESTINATION lib/cmake/MathFunctions
)

命令configure_package_config_file()获取项目源码根目录下的文件Config.cmake.in,将其作为模板生成文件MathFunctionsConfig.cmake放在构建的二进制目录${CMAKE_CURRENT_BINARY_DIR}里,并指明在安装项目时将该文件安装在安装目录下的文件夹lib/cmake/MathFunctions中。NO_SET_AND_CHECK_MACRONO_CHECK_REQUIRED_COMPONENTS_MACRO两个选项用于控制在生成的配置文件中是否包含特定的 CMake 宏。

构建项目、安装项目:

> cmake -G "MinGW Makefiles" -B build
> cmake --build build
> cmake --install build --prefix "C:\Users\wenidc\Desktop\sqrt\test_install"

一切正常。

可选地,还可以在文件MathFunctionsConfig.cmake同目录下包含一个文件MathFunctionsConfigVersion.cmake,用于指出项目的版本号以及兼容性(如果当前项目的版本号是3.2,而另一个项目在链接当前项目时指明需要版本2.8,CMake就会提示版本不兼容)。

修改文件/CMakeLists.txt,在末尾添加如下代码:

write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
VERSION "${sqrt_VERSION_MAJOR}.${sqrt_VERSION_MINOR}"
COMPATIBILITY AnyNewerVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake
DESTINATION lib/cmake/MathFunctions
)

上述命令指出,要在项目构建二进制目录${CMAKE_CURRENT_BINARY_DIR}生成一个文件MathFunctionsConfigVersion.cmakeVERSION指出项目版本号信息,变量sqrt_VERSION_MAJORsqrt_VERSION_MINOR在前面有介绍过;COMPATIBILITY AnyNewerVersion指出当前项目版本兼容于任何比它更新的版本。以及,在安装项目时将该文件安装在安装目录下的文件夹lib/cmake/MathFunctions中。

现在,构建、安装项目,在其他 CMake 项目中就可以使用命令find_package()方便地导入库MathFunctions了。

> cmake -G "MinGW Makefiles" -B build
> cmake --build build
> cmake --install build --prefix "C:\Users\wenidc\Desktop\sqrt\test_install"

以前面出现过的项目test1为例,之前的文件test1/CMakeLists.txt是这样的:

cmake_minimum_required(VERSION 3.20)
project(test1)
add_executable(test1 main.cpp)
# 链接到库 MathFunctions
target_link_libraries(test1 PRIVATE MathFunctions)
set(MathFunctions_DIR C:\\Users\\Aoyu\\Desktop\\sqrt-1.0-win64\\sqrt-1.0-win64)
# 指定库 MathFunctions 的头文件的路径
target_include_directories(test1 PRIVATE "${MathFunctions_DIR}/include")
# 指定库 MathFunctions 的动态链接库的路径
target_link_directories(test1 PRIVATE "${MathFunctions_DIR}/lib")

现在只需这样写:

cmake_minimum_required(VERSION 3.20)
project(test1)
add_executable(test1 main.cpp)
# 链接到库 MathFunctions
target_link_libraries(test1 PRIVATE MathFunctions)
set(MathFunctions_DIR C:\\Users\\Aoyu\\Desktop\\sqrt\\test_install\\lib\\cmake\\MathFunctions)
find_package(MathFunctions REQUIRED)

注意,变量MathFunctions_DIR不是随意命名的,必须用这个名字,变量MathFunctions_DIR的值必须是MathFunctionsConfig.cmake所在文件夹的路径,而不是项目安装目录的根目录。关于此,官方有介绍(摘录自 不加这句set(MathFunctions_DIR ...)构建项目时的报错信息):

# Add the installation prefix of "MathFunctions" to CMAKE_PREFIX_PATH or set "MathFunctions_DIR" to a directory containing one of the above files. If "MathFunctions" provides a separate development package or SDK, be sure it has been installed.

虽说现在项目sqrt的库MathFunctions可以被其他项目方便地调用了,但我仍然感觉有那么一点不方便。在开发时,我如果修改了项目的代码,其他库如果想使用本项目最新的代码,我除了要构建本项目之外,还要安装本项目。我能不能让其他库直接从本项目的构建目录导入库?构建目录下现在有文件MathFunctionsConfig.cmakeMathFunctionsConfigVersion.cmake,也就是还缺一个MathFunctionsTargets.cmake

编辑文件/CMakeLists.txt,在末尾添加代码:

export(EXPORT MathFunctionsTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)

这样就将导出集(Export Set)MathFunctionsTargets导出为文件${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake

构建项目:

> cmake -G "MinGW Makefiles" -B build
> cmake --build build

现在文件齐全了。试一试其他项目能不能直接从sqrt的构建目录导入。依然以项目test1为例,更改文件test1/CMakeLists.txt中变量MathFunctions_DIR的值为项目sqrt的构建目录:

cmake_minimum_required(VERSION 3.20)
project(test1)
add_executable(test1 main.cpp)
# 链接到库 MathFunctions
target_link_libraries(test1 PRIVATE MathFunctions)
set(MathFunctions_DIR C:\\Users\\Aoyu\\Desktop\\sqrt\\build)
find_package(MathFunctions REQUIRED)

一切正常。

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)
install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)
# enable_testing()
include(CTest)
add_test(NAME Runs COMMAND sqrt 25)
add_test(NAME Usage COMMAND sqrt)
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME Usage2 COMMAND sqrt 6 7)
set_tests_properties(Usage2 PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME StandardUse COMMAND sqrt 4)
set_tests_properties(StandardUse PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2")
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()
do_test(sqrt 4 "4 is 2")
do_test(sqrt 9 "9 is 3")
do_test(sqrt 5 "5 is 2.236")
do_test(sqrt 7 "7 is 2.645")
do_test(sqrt 25 "25 is 5")
do_test(sqrt -25 "-25 is (-nan|nan|0)")
do_test(sqrt 0.0001 "0.0001 is 0.01")
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/License.txt)
set(CPACK_PACKAGE_VERSION_MAJOR ${sqrt_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${sqrt_VERSION_MINOR})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)
install(EXPORT MathFunctionsTargets
FILE MathFunctionsTargets.cmake
DESTINATION lib/cmake/MathFunctions
)
include(CMakePackageConfigHelpers)
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
INSTALL_DESTINATION "lib/cmake/MathFunctions"
NO_SET_AND_CHECK_MACRO
NO_CHECK_REQUIRED_COMPONENTS_MACRO
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
VERSION "${sqrt_VERSION_MAJOR}.${sqrt_VERSION_MINOR}"
COMPATIBILITY AnyNewerVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake
DESTINATION lib/cmake/MathFunctions
)
export(EXPORT MathFunctionsTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
include(MakeTable.cmake)
add_library(SqrtLibrary STATIC mysqrt.cpp ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
target_include_directories(SqrtLibrary PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
# set_target_properties(SqrtLibrary PROPERTIES POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS})
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::log(1.0);
return 0;
}
" HAVE_LOG)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::exp(1.0);
return 0;
}
" HAVE_EXP)
if (HAVE_LOG AND HAVE_EXP)
target_compile_definitions(SqrtLibrary PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
# target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(MathFunctions INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
set(installable_libs MathFunctions sqrt_compiler_flags)
if (TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
# install(TARGETS ${installable_libs} DESTINATION lib)
install(TARGETS ${installable_libs}
EXPORT MathFunctionsTargets
DESTINATION lib
)
install(FILES MathFunctions.h DESTINATION include)

打包Debug版本和Release版本

这一小节不适用于多配置生成器(如MSVC)。我使用的是 MinGW。

我想要同时构建本项目的两个版本,一个Debug版本,一个Release版本,把它们放在同一个目录下。为以示区分,在Debug版本的目标(target)名称后加一个字符“d”。

编辑文件/CMakeLists.txt,在开头处添加:

set(CMAKE_DEBUG_POSTFIX d)

构建项目,指定构建类型为Debug:

> cmake -G "MinGW Makefiles" -B build -DCMAKE_BUILD_TYPE=Debug
> cmake --build build

可以注意到,生成的库目标文件名称末尾都带一个“d”。但是可执行文件后不带“d”。

编辑文件/CMakeLists.txt,为可执行目标sqrt设置属性DEBUG_POSTFIX

set_target_properties(sqrt PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})

重新构建项目,注意到这次生成的可执行文件后也带“d”了,变成了sqrtd.exe

接下来给库MathFunctions设置版本号。前面通过命令write_basic_package_version_file设置过一次版本号,那个版本号是指“包(Package)的版本”,而这里的版本号是“具体的目标(库MathFunctions)的版本”。

编辑文件MathFunctions/CMakeLists.txt,为MathFunctions设置属性VERSIONSOVERSION

set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")

VERSIONSOVERSION属性分别指“共享库目标的版本号”和“共享库目标的 ABI(Application Binary Interface) 版本号。”当你对库进行重大更改且不兼容旧版本时,你应该增加 SOVERSION,以便系统可以区分新版本和旧版本,并在需要时自动使用正确的版本。

在项目根目录创建子目录debugrelease,使用选项CMAKE_BUILD_TYPE在这两个目录中分别构建 Debug 版和 Release 版:

Terminal window
> cd debug
> cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Debug -B debug
> cmake --build debug
> cmake -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -B release
> cmake --build release

两个版本都已构建完毕,接下来将它们打包到同一个发行版中。在项目根目录创建文件MultiCPackConfig.cmake,写入如下内容:

include("release/CPackConfig.cmake")
set(CPACK_INSTALL_CMAKE_PROJECTS
"debug;sqrt;ALL;/"
"release;sqrt;ALL;/"
)

变量 CPACK_INSTALL_CMAKE_PROJECTS 的值中用分号分隔的4个值分表代表:构建目录(Build Directory),项目名称(Project Name),项目组件(Project Component),相对于安装目录根目录的路径(Directory)。

使用 CPack 及上述自定义配置打包项目:

cpack -G ZIP --config MultiCPackConfig.cmake

生成的项目二进制文件都是双份的,一份是 Debug 版,一份是 Release 版。

此时文件 /CMakeLists.txt 的完整内容如下:

cmake_minimum_required(VERSION 3.20)
project(sqrt VERSION 1.0)
set(CMAKE_DEBUG_POSTFIX d)
add_library(sqrt_compiler_flags INTERFACE)
target_compile_features(sqrt_compiler_flags INTERFACE cxx_std_20)
set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU,LCC>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(sqrt_compiler_flags INTERFACE
"$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
"$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
add_subdirectory(MathFunctions)
configure_file(SqrtConfig.h.in SqrtConfig.h)
add_executable(sqrt sqrt.cpp)
set_target_properties(sqrt PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})
target_include_directories(sqrt PUBLIC "${PROJECT_BINARY_DIR}")
target_link_libraries(sqrt PUBLIC MathFunctions)
target_link_libraries(sqrt PUBLIC sqrt_compiler_flags)
install(TARGETS sqrt DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/SqrtConfig.h" DESTINATION include)
# enable_testing()
include(CTest)
add_test(NAME Runs COMMAND sqrt 25)
add_test(NAME Usage COMMAND sqrt)
set_tests_properties(Usage PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME Usage2 COMMAND sqrt 6 7)
set_tests_properties(Usage2 PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*num")
add_test(NAME StandardUse COMMAND sqrt 4)
set_tests_properties(StandardUse PROPERTIES PASS_REGULAR_EXPRESSION "4 is 2")
function(do_test target arg result)
add_test(NAME Comp${arg} COMMAND ${target} ${arg})
set_tests_properties(Comp${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()
do_test(sqrt 4 "4 is 2")
do_test(sqrt 9 "9 is 3")
do_test(sqrt 5 "5 is 2.236")
do_test(sqrt 7 "7 is 2.645")
do_test(sqrt 25 "25 is 5")
do_test(sqrt -25 "-25 is (-nan|nan|0)")
do_test(sqrt 0.0001 "0.0001 is 0.01")
include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/License.txt)
set(CPACK_PACKAGE_VERSION_MAJOR ${sqrt_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${sqrt_VERSION_MINOR})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)
install(EXPORT MathFunctionsTargets
FILE MathFunctionsTargets.cmake
DESTINATION lib/cmake/MathFunctions
)
include(CMakePackageConfigHelpers)
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
INSTALL_DESTINATION "lib/cmake/MathFunctions"
NO_SET_AND_CHECK_MACRO
NO_CHECK_REQUIRED_COMPONENTS_MACRO
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
VERSION "${sqrt_VERSION_MAJOR}.${sqrt_VERSION_MINOR}"
COMPATIBILITY AnyNewerVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake
DESTINATION lib/cmake/MathFunctions
)
export(EXPORT MathFunctionsTargets
FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)

此时文件 MathFunctions/CMakeLists.txt 的完整内容如下:

add_library(MathFunctions MathFunctions.cpp)
target_link_libraries(MathFunctions PUBLIC sqrt_compiler_flags)
option(USE_MYMATH "Use our own math implementation" ON)
if (USE_MYMATH)
target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")
include(MakeTable.cmake)
add_library(SqrtLibrary STATIC mysqrt.cpp ${CMAKE_CURRENT_BINARY_DIR}/Table.h)
target_include_directories(SqrtLibrary PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
# set_target_properties(SqrtLibrary PROPERTIES POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS})
target_link_libraries(SqrtLibrary PUBLIC sqrt_compiler_flags)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::log(1.0);
return 0;
}
" HAVE_LOG)
check_cxx_source_compiles("
#include <cmath>
int main() {
std::exp(1.0);
return 0;
}
" HAVE_EXP)
if (HAVE_LOG AND HAVE_EXP)
target_compile_definitions(SqrtLibrary PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()
# target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(MathFunctions INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
)
set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")
set(installable_libs MathFunctions sqrt_compiler_flags)
if (TARGET SqrtLibrary)
list(APPEND installable_libs SqrtLibrary)
endif()
# install(TARGETS ${installable_libs} DESTINATION lib)
install(TARGETS ${installable_libs}
EXPORT MathFunctionsTargets
DESTINATION lib
)
install(FILES MathFunctions.h DESTINATION include)