打算简单学一下 CMake,毕竟接下来的实验也好,代码也好 CMake 基本上的躲不掉的。。。

# MakeFile

对于当前文件目录:

image-20220425105218653

可以看到 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

新的多层目录:

image-20220425122439766

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)

如果把目录结构改为:

image-20220425142550285

那么 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,这里 PRIVATEPUBLIC 的区别是:
CURL::libcurl 库只会被 libanswer 看到,根级别的 main.cpp 中
无法 include curl 的头文件。
#]]
target_link_libraries(libanswer PRIVATE CURL::libcurl)

# 新的依赖关系

image-20220425155727377

依赖关系图:

image-20220425160052810

其中的 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

更新于

请我喝[茶]~( ̄▽ ̄)~*

Kalice 微信支付

微信支付

Kalice 支付宝

支付宝