Makefile & Cmake 使用
先放一个现成的CMakeLists.txt模板
## 只需修改前三行 |
添加一个cuda项目的例子:
set(_PROJECT_NAME_ "my_cuda_program") # project name |
之前
一直用Visual Studio和DevC++这类IDE,没怎么接触过Makefile和cmake。但是有时候配置环境的时候,需要自己用make编译,可能就会出现环境出错误的情况。CSDN这类东西还不靠谱,所以还是需要至少了解makefile和cmake大致在干嘛的。
不愿意看(看不懂)英文文档,所以网上找了个教程学习了一下。教程来源于上交iPADS新人培训。 个人感觉这个教程讲的很细致,很少有这样直奔主题的视频,代码分支也很容易操作。 我把一些感觉会用得上的内容内化,记下来。
Makefile
语法
总体上和Shell的语法比较像,一般来说都长这样:
var := somevar # 定义变量 |
举个例子
target: helloworld.cpp |
使用的时候,直接:
make target # 他就开始自己构造了 |
实际使用
实际使用中,会有一种套娃写法,也就是构造一个主目标,需要很多子目标。这时候就把该子目标的名字作为主目标的依赖。
比如说:
# 这部分是配置编译器部分 |
至于为何还要再target后面写上依赖,是因为makefile会检测,如果检测到依赖变了,就能方便makefile重新生成这个目标。如果依赖没改变,make就不会重新构建,这样就方便不必重复生成相同的东西。
另外
除了使用make+target直接构建目标外,make还提供了“伪目标”来为特定任务提供简单的实现方法。具体用法:
|
Cmake
相比于makefile,Cmake显然高级一些,是一个生成makefile的项目管理工具。
基本语法
打开CmakeList.txt,一般都是长这个样子:
cmake_minimum_required(VERSION 3.9) |
cmake会自动根据依赖,构建我们需要的makefile文件。这里的answer也是target。在cmake中这样构建项目:
cmake -B build # 生成构建目录 |
库分离
如果很多项目都用到了一系列文件代码,就需要构建一个“库目标”。想添加了一个静态库target,使用
add_library(libname STATIC dependence_files) |
如果一个主target要使用到这个库,使用target_link_libraries链接库和该target:
add_library(a STATIC a.cpp) |
如此便可以实现lib的复用。
文件隔离
一个大的项目可能会用到几百个文件,这时候需要将他们放到不同文件夹里面才能方便维护。我们将一个个库放到独立的文件夹中,另外在根目录的CMakeList.txt中添加子目录:
add_subdirectory(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) |
这里的 PRIVATE 区别于之前的 PUBLIC 之处在于:链接库的文件只能在构建tar目标的时候被用到,但是不能被访问。
Cache变量
私密的 App ID、API Key 等不应该直接放在代码里,应该做成可配置的项,从外部传入。除此之外还可通过可配置的变量来控制程序的特性、行为等。在 CMake 中,使用 set 通过 cache 变量实现:
set(APPID "" CACHE STRING "My secret APPID") |
默认值指的是这个变量的默认值
条件输出
接在这里顺便说一下条件输出,举一个例子
set(API_KEY "" CACHE STRING "My secret API_KEY") |
条件判断部分,顾名思义了就。message语句中SEND_ERROR
是表示输出的一种类型。
另外,就是在这里,如果你没有对API_KEY进行赋值就会报错。怎么赋值就在下面。
BOOL
类型的变量还有另外一种写法:
set(ENABLE_CACHE OFF CACHE BOOL "Enable request cache") |
接下来就是“怎么赋值”+“怎么让代码得到这个变量的值”
构造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_xxx
给 INTERFACE
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
只应用于单个 targettarget_compile_features
可以指定更细粒度的 C++ 特性,例如cxx_auto_type
、cxx_lambda
等。
总的来说就是更灵活、自由度更高。
模块化测试
Ctest
CTest是CMake的自带脚本。要使用 CTest 运行 CMake 项目的测试程序,需要在 CMakeLists.txt 添加一些内容:
# in /CMakeLists.txt |
如果是你自己写的Test脚本,要加上自己的路径。
TBD
写到这里先不写了。等到需要用单元测试的时候再来学习。
下面是Cmake的一些其他特性:
FetchContent
TBD
Macro & Function
TBD
回到Makefile
调用 CMake 命令往往需要传很多参数,并且 CMake 生成、CMake 构建、CTest 的命令都不太相同,要获得比较统一的使用体验,可以在外面包一层 Make:
WOLFRAM_APPID := |
就是套娃,用make的伪指令执行cmake,再用cmake生成并执行makefile。从而方便在命令行调用:
make build WOLFRAM_APPID=xxx |
最后,那个视频我没学完,因为要打Dota了。不过我觉得这个课程讲的确实不错,给99分。