假设你现在是一个 IDE 的作者,正在添加对 cmake 项目的支持。你需要从 cmake 的执行结果中,获取所有源代码的信息,比如,使用什么参数编译,都有哪些文件,等等。这时候你会使用什么方法?我相信大多数人第一个想到的是 compilation database ,很多简易版本的 IDE 支持也是使用这个方法。不过,这种方式只能获取很简单的参数信息。如果要获取完整的信息,传统的方法是使用 cmake server 。cmake 3.20 移除了 cmake server ,从此必须使用 cmake-file-api

cmake-file-api 提供了一套仅用文件系统就可以获取各种详细信息的方法。使用的时候,先在 <build>/.cmake/api/v1/ 目录中放入相应的文件,告诉 cmake 需要获取的信息。然后运行 cmake , configure 阶段会根据目录中对应的查询文件,生成查询结果,然后放入对应的文件。下面举一个例子说明。

实例:使用 cmake-file-api 获取链接信息

假设现在我们正在给用户安装自己的编译器的 runtime,我们的 runtime 用 c 编写,自己的编译器编译出来的二进制文件,和 runtime 代码链接后,形成最终的 executable 。在传统编译器实现中,这个过程通常是手写一个链接器参数模板,链接时再带入实际参数。这样做虽然简单,但无法检测系统使用的工具链和实际参数,遇到奇怪点的系统,可能就失效了。这个例子项目演示如何通过 cmake-file-api 获取正确的链接命令行。

目录结构和 CMakeLists.txt

项目的目录结构如下。

1
2
3
4
5
6
7
8
$ tree
.
├── Build.cmake
├── CMakeLists.txt
├── dummy_executable.c
└── runtime.c

0 directories, 4 files

其中, CMakeLists.txt 的内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required(VERSION 3.25)

project(native-runime)

# runtime library
add_library(native_runtime STATIC runtime.c)

# libdl (add link args)
target_link_libraries(native_runtime PUBLIC ${CMAKE_DL_LIBS})

# dummy executabe
add_executable(dummy_executable dummy_executable.c)
target_link_libraries(dummy_executable PRIVATE native_runtime)

这个 cmake 项目中有两个目标,一个是静态链接库 native_runtime ,这个是我们的运行时项目。另一个目标是可执行文件 dummy_executable 。通常,dummy_executable 的链接命令和我们编译器产生的二进制文件的链接命令是一致的。这个目标的作用是为了得到链接成一个 executable 所需的命令行,在实际项目中通常并不会被真正编译。

注:例子中添加了 libdl 作为额外链接参数的例子。

使用 cmake-file-api

Build.cmake 文件为如何使用 cmake-file-api 提供了实例。文件内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
cmake_minimum_required(VERSION 3.25)

function(setup_file_api_query build_dir)
# 确保在 build 目录中,存在文件 .cmake/api/v1/query/codemodel-v2
# 在这个例子中,内容可以为空。
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory "${build_dir}/.cmake/api/v1/query"
COMMAND_ERROR_IS_FATAL ANY)
execute_process(COMMAND ${CMAKE_COMMAND} -E touch "${build_dir}/.cmake/api/v1/query/codemodel-v2"
COMMAND_ERROR_IS_FATAL ANY)
endfunction()

function(init_build build_dir)
# 执行 cmake ,对项目进行 configure
execute_process(
COMMAND ${CMAKE_COMMAND}
-S ${SOURCE_DIR}
-B ${build_dir}
COMMAND_ECHO STDOUT
COMMAND_ERROR_IS_FATAL ANY)
endfunction()

function(extract_link_args build_dir)
# configure 结束后, cmake 会把我们需要的文件放在 ${build_dir}/.cmake/api/v1/reply/target-dummy_executable-*.json
# 由于每次 configure 都可能让 cmake 给出不同的结果,所以每个结果文件的文件名都会带上后缀,确保区分每次会话。
# 在这个例子中,我们假设只执行了一次。
file(GLOB reply_file "${build_dir}/.cmake/api/v1/reply/target-dummy_executable-*.json")
message(STATUS "Found result: ${reply_file}")

# 读取文件内容并打印结果。返回的结果都是 JSON 格式,可以使用 cmake 方便的解析。
file(READ ${reply_file} reply_file_json)
string(JSON link_args GET ${reply_file_json} "link" "commandFragments")
message(STATUS "Link commands: ${link_args}")
endfunction()

setup_file_api_query(${BUILD_PATH})
init_build(${BUILD_PATH})
extract_link_args(${BUILD_PATH})

执行

执行过程如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ mkdir build && cd build
$ cmake -DSOURCE_DIR=.. -DBUILD_PATH=`pwd` -P ../Build.cmake
'/usr/bin/cmake' '-S' '..' '-B' '/home/ubuntu/tmp/cmake-file-api-demo/runtime/build'
-- The C compiler identification is GNU 11.3.0
-- The CXX compiler identification is GNU 11.3.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (1.5s)
-- Generating done (0.0s)
-- Build files have been written to: /home/ubuntu/tmp/cmake-file-api-demo/runtime/build
-- Found result: /home/ubuntu/tmp/cmake-file-api-demo/runtime/build/.cmake/api/v1/reply/target-dummy_executable-8440a387c69b3d168ffa.json
-- Link commands: [
{
"fragment" : "",
"role" : "flags"
},
{
"backtrace" : 2,
"fragment" : "libnative_runtime.a",
"role" : "libraries"
},
{
"backtrace" : 3,
"fragment" : "-ldl",
"role" : "libraries"
}
]

我们可以看到,最后的输出已经是 dummy_executable 的链接参数。至此,只要再从别处获得链接器程序路径[1],就可以得到完整的链接命令行了。


  1. 在这个例子中,通常是 ${CMAKE_C_COMPILER}↩︎