打算简单学一下 CMake,毕竟接下来的实验也好,代码也好 CMake 基本上的躲不掉的。。。
# MakeFile
对于当前文件目录:
可以看到 answer.hpp 和 answer.cpp 都在当下目录下,所以不需要额外连接工作。
Makefile 可以简单写为:
CC := clang | |
CXX := clang++ | |
.PHONY: all | |
all: answer | |
# 在这里添加了 answer.o 目标文件。 | |
objects := main.o answer.o | |
answer: $(objects) | |
$(CXX) -o $@ $(objects) | |
# | |
# Make 可以自动推断 .o 目标文件需要依赖同名的 .cpp 文件, | |
# 所以其实不需要在依赖中指定 main.cpp 和 answer.cpp, | |
# 也不需要写编译 commands,它知道要用 CXX 变量制定的命令 | |
# 作为 C++ 编译器。 | |
# | |
# 这里只需要指定目标文件所依赖的头文件,使头文件变动时可以 | |
# 重新编译对应目标文件。 | |
# | |
main.o: answer.hpp | |
answer.o: answer.hpp | |
.PHONY: clean | |
clean: | |
rm -f answer $(objects) |
注意头文件其实并不会参加链接和编译,编译器做的第一步就是把头文件在源文件中进行展开,这就是为什么需要加 ifdefine。所以即使把 22 和 23 这两行给注释掉,也无所谓,只是头文件更新后不会重新编译。
# CMake 简单三步走
同样的文件目录,同样的 cpp 文件,执行,CmakeLists.txt 可以写为:
# 指定最小 CMake 版本要求 | |
cmake_minimum_required(VERSION 3.9) | |
# 设置项目名称 | |
project(answer) | |
#[[ | |
添加可执行文件 target,类似于原来 Makefile 的: | |
answer: main.o answer.o | |
main.o: main.cpp answer.hpp | |
answer.o: answer.cpp answer.hpp | |
CMake 会自动找到依赖的头文件,因此不需要特别指定, | |
当头文件修改的时候,会重新编译依赖它的目标文件。 | |
#]] | |
add_executable(answer main.cpp answer.cpp) | |
#[[ | |
使用如下命令构建本项目: | |
cmake -B build # 生成构建目录 | |
cmake --build build # 执行构建 | |
./build/answer # 运行 answer 程序 | |
#]] |
# Cmake Split Dir
新的多层目录:
main.cpp:
#include <answer/answer.hpp> |
可以看到 main.cpp includeanswer 的时候只是简单调用了 answer/answer.cpp
其中外部的 CMake 文件:
cmake_minimum_required(VERSION 3.9) | |
project(answer) | |
# 添加 answer 子目录 | |
add_subdirectory(answer) | |
add_executable(answer_app main.cpp) | |
target_link_libraries(answer_app libanswer) | |
#[[ | |
使用如下命令构建本项目: | |
cmake -B build # 生成构建目录 | |
cmake --build build # 执行构建 | |
./build/answer_app # 运行 answer_app 程序 | |
#]] |
它添加了子目录,那么他也会去执行子目录的 CmakeList.txt。
来看子目录的 cmakeList.txt:
add_library(libanswer STATIC answer.cpp) | |
#[[ | |
message 可用于打印调试信息或错误信息,除了 STATUS | |
外还有 DEBUG WARNING SEND_ERROR FATAL_ERROR 等。 | |
#]] | |
message(STATUS "Current source dir: ${CMAKE_CURRENT_SOURCE_DIR}") | |
#[[ | |
给 libanswer 库目标添加 include 目录,PUBLIC 使 | |
这个 include 目录能被外部使用者看到。 | |
当链接 libanswer 库时,这里指定的 include 目录会被 | |
自动添加到使用此库的 target 的 include 路径中。 | |
#]] | |
target_include_directories(libanswer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) |
静态库表示连接的时候会把这个库也加到可执行文件。 target_include_directories(libanswer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
这个语句的作用连接 libanaswer 库的时候,会把当前目录下的 include 目录,添加到 target 的 include 目录。
如果加上一个 DCMAKE_EXPORT_COMPILE_COMMANDS
参数,也就是会自动生成一个 compile 文件:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -B build |
或者在 CMakeList.txt 中加入:
set(CMAKE_EXPORT_COMPILE_COMMANDS onk) |
如果把目录结构改为:
那么 main.cpp 中需要改为:
#include <ans/answer.hpp> |
我现在彻底懂了这个含义了,第一个 CMakeLists.txt 中 add_subdirectory(answer)
表示使用子目录下的 CMakeLists.txt,子目录中 add_library(libanswer STATIC answer.cpp)
表示做一个库 libanswer, target_include_directories(libanswer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
这个 target 应该就是指的是这个 CmakeLists.txt 文件。因此这里做出库之后,那么 target_link_libraries(answer_app libanswer)
表示最终需要 add 这个库。
# 使用系统的 curl 动态库
#[[ | |
find_package 用于在系统中寻找已经安装的第三方库的头文件和库文件 | |
的位置,并创建一个名为 CURL::libcurl 的库目标,以供链接。 | |
#]] | |
find_package(CURL REQUIRED) | |
add_library(libanswer STATIC answer.cpp) | |
target_include_directories(libanswer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) | |
#[[ | |
为 libanswer 库链接 libcurl,这里 PRIVATE 和 PUBLIC 的区别是: | |
CURL::libcurl 库只会被 libanswer 看到,根级别的 main.cpp 中 | |
无法 include curl 的头文件。 | |
#]] | |
target_link_libraries(libanswer PRIVATE CURL::libcurl) |
# 新的依赖关系
依赖关系图:
其中的 CMake 代码:
answer 中:
add_library(libanswer STATIC answer.cpp) | |
target_include_directories(libanswer PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) | |
# libanswer 改成直接使用 wolfram 库提供的 API,无需关心 CURL | |
target_link_libraries(libanswer PRIVATE wolfram) |
wolframe 中:
add_library(wolfram STATIC alpha.cpp) | |
target_include_directories(wolfram PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) | |
# wolfram PRIVATE 地依赖 curl_wrapper | |
target_link_libraries(wolfram PRIVATE curl_wrapper) |
curl_wrapper:
find_package(CURL REQUIRED) | |
add_library(curl_wrapper STATIC curl_wrapper.cpp) | |
target_include_directories(curl_wrapper | |
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) | |
# curl_wrapper PRIVATE 地依赖 CURL::libcurl | |
target_link_libraries(curl_wrapper PRIVATE CURL::libcurl) |
# 把 answer 改成 Header only
set(WOLFRAM_APPID | |
"" | |
CACHE STRING "WolframAlpha APPID") | |
if(WOLFRAM_APPID STREQUAL "") | |
message(SEND_ERROR "WOLFRAM_APPID must not be empty") | |
endif() | |
#[[ | |
INTERFACE 类型的 target 一般用于没有源文件的情况,比如 | |
header-only 库,或者只是为了抽象地提供一组 target_xxx | |
的配置。 | |
INTERFACE target 的后续所有 target_xxx 都必须也使用 | |
INTERFACE,效果将会直接应用到链接此库的 target 上。 | |
本步骤将 libanswer 从 STATIC target 改成 INTERFACE | |
target 不会影响 answer_app 中使用它的代码。 | |
#]] | |
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) |
通过:
if(WOLFRAM_APPID STREQUAL "") | |
message(SEND_ERROR "WOLFRAM_APPID must not be empty") | |
endif() |
检查是否传入了 WOLFRAME_APPID,build 时候需要执行:
cmake -B build -DWOLFRAM_APPID=ssdfk |
注意传参需要加个 -D
。