先放一个现成的CMakeLists.txt模板

## 只需修改前三行
set(_PROJECT_NAME_ "project_name_x") # 设置project的名字
set(_EXE_NAME_ "exe_name_x") # 设置target的名字
set(_SRC_FILE_NAME_ "file_path_x") # 设置target源代码所在的位置

cmake_minimum_required(VERSION 3.15)
project(${_PROJECT_NAME_})
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -O3")
add_executable(${_EXE_NAME_})
aux_source_directory(${_SRC_FILE_NAME_} _SOURCE_)
target_sources(${_EXE_NAME_} PUBLIC ${_SOURCE_})
target_include_directories(${_EXE_NAME_} PUBLIC ${_SRC_FILE_NAME_}/include)

## 如果需要添加库,用该方法添加:
# set(_LIB_NAME_ "lib_name_x")
# set(_LIB_FILE_NAME_ "lib_file_path_x")
# add_library(${_LIB_NAME_})
# aux_source_directory(${_LIB_FILE_NAME_} _LIB_SOURCE_)
# target_sources(${_LIB_NAME_} PUBLIC ${_LIB_SOURCE_})
# target_link_libraries(${_EXE_NAME_} PUBLIC ${_LIB_NAME_})

## 如果子目录里有CMakeLists,用该方法添加
# add_subdirectory(...)

之前

一直用Visual Studio和DevC++这类IDE,没怎么接触过Makefile和cmake。但是有时候配置环境的时候,需要自己用make编译,可能就会出现环境出错误的情况。CSDN这类东西还不靠谱,所以还是需要至少了解makefile和cmake大致在干嘛的。

不愿意看(看不懂)英文文档,所以网上找了个教程学习了一下。教程来源于上交iPADS新人培训个人感觉这个教程讲的很细致,很少有这样直奔主题的视频,代码分支也很容易操作。 我把一些感觉会用得上的内容内化,记下来。

Makefile

语法

总体上和Shell的语法比较像,一般来说都长这样:

var := somevar # 定义变量
# 定义Target
target: dependencefiles
command -a -b target file.o/cpp # 命令
echo "Something"

举个例子

target: helloworld.cpp
$(CXX) -o target helloworld.cpp
echo "Use ${CXX} compile helloworld.cpp to $@"
# 这个@类似于JavaScript的this

使用的时候,直接:

$ make target # 他就开始自己构造了
$ ./target # 执行这个构造出来的文件

实际使用

实际使用中,会有一种套娃写法,也就是构造一个主目标,需要很多子目标。这时候就把该子目标的名字作为主目标的依赖。
比如说:

# 这部分是配置编译器部分
CC := clang
CXX := clang++ # 可通过 make CXX=g++ 形式覆盖
# 目标的dependence,有往里加的直接写在后面
objects := main.o answer.o
# 这个就是主目标
hello: $(objects)
$(CXX) -o $@ $(objects) # $@ 是自动变量,表示 target 名
# 次级目标。
main.o: main.cpp
$(CXX) -c main.cpp
# 一般来讲makefile能自动识别.o文件,所以一般来说不必写上面这行。比如下面这个
answer.o: answer.hpp
# 但是makefile不能识别头文件,所以要加上头文件

至于为何还要再target后面写上依赖,是因为makefile会检测,如果检测到依赖变了,就能方便makefile重新生成这个目标。如果依赖没改变,make就不会重新构建,这样就方便不必重复生成相同的东西。

另外:

除了使用make+target直接构建目标外,make还提供了“伪目标”来为特定任务提供简单的实现方法。具体用法:

.PHONY: clean
# 声明clean是一个伪目标
clean:
rm -f answer $(object)
# 清理目标文件和项目文件

Cmake

相比于makefile,Cmake显然高级一些,是一个生成makefile的项目管理工具。

基本语法

打开CmakeList.txt,一般都是长这个样子:

cmake_minimum_required(VERSION 3.9)
# 指定Cmake最低版本
project(answer)
# 指定Project名字
set(CMAKE_CXX_STANDARD 11)
# 根目录里面设置,使用CXX的C11标准
add_executable(answer main.cpp answer.cpp)
# 添加一个可执行文件,叫answer,依赖两个cpp文件

cmake会自动根据依赖,构建我们需要的makefile文件。这里的answer也是target。在cmake中这样构建项目:

$ cmake -B build      # 生成构建目录
$ cmake --build build # 执行构建程序
$ ./build/answer # 运行生成的目标

库分离

如果很多项目都用到了一系列文件代码,就需要构建一个“库目标”。想添加了一个静态库target,使用

add_library(libname STATIC dependence_files)

如果一个主target要使用到这个库,使用target_link_libraries链接库和该target:

add_library(a STATIC a.cpp)
# 定义一个静态库
add_executable(tar main.cpp)
# 定义一个Target
target_link_libraries(tar a)
# 链接库和target

如此便可以实现lib的复用。

文件隔离

一个大的项目可能会用到几百个文件,这时候需要将他们放到不同文件夹里面才能方便维护。我们将一个个库放到独立的文件夹中,另外在根目录的CMakeList.txt中添加子目录:

add_subdirectory(math)
# 写在根目录文件中,其中math是包含所需要库的子目录

如果math子目录里面还有其他文件夹,其中的文件需要被调用,就在math下的CMakeList中写add_subdirectory。其他的写法不变。

可以链接同一项目中其它子目录中定义的 library。

目录路径添加

首先说一下 ${CMAKE_CURRENT_SOURCE_DIR} 这个变量。他所指的是这个CMakeList所在的文件的位置。

在构建程序的过程中,c++引用头文件和源文件需要指明路径,否则构建程序将找不到这些文件。这时候需要使用target_include_directories为target添加路径:

target_include_directories(libname PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 内部参数说明

这里面的 PUBLIC 是告诉cmake这个添加的路径可以认为是一个“全局变量”,任何链接了这个库的target都可以访问这个目录里的文件。

调用系统其他第三方库

使用find_package,调用系统已经安装好的第三方库。比如说,演示项目里面用的是curl这个库:

find_package(CURL REQURED)
# 用find package寻找CURL(一般都是约定俗成的名字)库
target_link_libraries(tar PRIVATE CURL::libcurl)
# 在这里添加所链接的库目标(libcurl)

这里的 PRIVATE 区别于之前的 PUBLIC 之处在于:链接库的文件只能在构建tar目标的时候被用到,但是不能被访问。

Cache变量

私密的 App ID、API Key 等不应该直接放在代码里,应该做成可配置的项,从外部传入。除此之外还可通过可配置的变量来控制程序的特性、行为等。在 CMake 中,使用 set 通过 cache 变量实现:

set(APPID "" CACHE STRING "My secret APPID")
# 变量名 默认值 表示他是CACHE变量 变量类型 变量描述

默认值指的是这个变量的默认值

条件输出

接在这里顺便说一下条件输出,举一个例子

set(API_KEY "" CACHE STRING "My secret API_KEY")
if(API_KEY STREQUAL "")
message(SEND_ERROR "The API_KEY must not be empty")
endif()

条件判断部分,顾名思义了就。message语句中SEND_ERROR是表示输出的一种类型。

另外,就是在这里,如果你没有对API_KEY进行赋值就会报错。怎么赋值就在下面。

BOOL类型的变量还有另外一种写法:

set(ENABLE_CACHE OFF CACHE BOOL "Enable request cache")
option(ENABLE_CACHE "Enable request cache" OFF) # 和上面基本等价

接下来就是“怎么赋值”+“怎么让代码得到这个变量的值”

构造build目录时传给cmake

用-D命令紧跟变量赋值

$ cmake -B build -DAPI_KEY=xxx

xxx是你的API_KEY的值。你也可以用ccmake工具赋值。使用ccmake:

$ ccmake -B build

回车,他会出现一个类似于nano的界面,让你手动改这些值。看起来挺友好的。

让C代码拿到API_KEY

使用 target_compile_definitions 添加编译时宏定义:

target_compile_definitions(libanswer PRIVATE APPID="${API_KEY}")

给目标libanswer提供了一个私有的宏定义APPID,其值就是你所传入的API_KEY。

接下来是我不怎么懂的,很多都是有关于C++的特性
不懂……也不会用……我就先复制粘贴了……

Header-only 的库可以添加为 INTERFACE 类型的 library

add_library(libanswer INTERFACE)
target_include_directories(libanswer INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_definitions(libanswer INTERFACE WOLFRAM_APPID="${WOLFRAM_APPID}")
target_link_libraries(libanswer INTERFACE wolfram)

通过 target_xxxINTERFACE library 添加属性都要用 INTERFACE

指定特性

可以针对 target 要求编译 feature(即指定要使用 C/C++ 的什么特性)。使用target_compile_features

target_compile_features(libanswer INTERFACE cxx_std_20)

格式很简单:库名、INTERFACE属性、特性名。
和直接设置 CMAKE_CXX_STANDARD 的区别:

  • CMAKE_CXX_STANDARD 会应用于所有能看到这个变量的 target,而 target_compile_features 只应用于单个 target
  • target_compile_features 可以指定更细粒度的 C++ 特性,例如 cxx_auto_typecxx_lambda 等。

总的来说就是更灵活、自由度更高。

模块化测试

Ctest

CTest是CMake的自带脚本。要使用 CTest 运行 CMake 项目的测试程序,需要在 CMakeLists.txt 添加一些内容:

# in /CMakeLists.txt
cmake_minimum_required(VERSION 3.14) # 提高了 CMake 版本要求
project(answer)

if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
include(CTest)
endif()

如果是你自己写的Test脚本,要加上自己的路径。

TBD

写到这里先不写了。等到需要用单元测试的时候再来学习。

下面是Cmake的一些其他特性:

FetchContent

TBD

Macro & Function

TBD

回到Makefile

调用 CMake 命令往往需要传很多参数,并且 CMake 生成、CMake 构建、CTest 的命令都不太相同,要获得比较统一的使用体验,可以在外面包一层 Make:

WOLFRAM_APPID :=

.PHONY: build configure run test clean

build: configure
cmake --build build

configure:
cmake -B build -DWOLFRAM_APPID=${WOLFRAM_APPID}

run:
./build/answer_app

test:
ctest --test-dir build -R "^answer."

clean:
rm -rf build

就是套娃,用make的伪指令执行cmake,再用cmake生成并执行makefile。从而方便在命令行调用:

$ make build WOLFRAM_APPID=xxx
$ make test
$ make run
$ make clean

最后,那个视频我没学完,因为要打Dota了。不过我觉得这个课程讲的确实不错,给99分。