0x00. 前言
在网上看别人做一些手工教程视频,经常能看到这样的评论:
脑子:我感觉我会了。
手:你行你来。
之前一直通过编译脚本去寻找代码入口,感觉我已经懂得CMake的语法了,直到今天寄己要写一个脚本去编译一个工程才发现,事情并不简单:脚本并没有按照我期望的去执行。
此工程需要用到Protocol Buffer,因此当代码构建的时候需要使用使用Protocol Buffer编译器去编译.proto
文件获得对应的生成文件。理论上,想要达到这个目的,我们只需要在CMakeLists.txt中使用add_custom_command
命令就可以可以生成对应的构建规则。但出人意料的是,这条命令并没有被执行,也就是说,并没有编译.proto
文件的规则生成,因此当最终使用Make去构建工程的时候,没能通过.proto
文件得到对应的源代码。
0x01. 踩雷
整个命令的使用如下面的代码所示,作用就是将位${REPO_ROOT}/protobuf/onnx-operators-ml.proto
以及${REPO_ROOT}/protobuf/onnx-ml.proto
这两个文件编译成C++头文件以及源文件,并存放到{REPO_ROOT}/src
目录下,其中${REPO_ROOT}
是项目的根目录,例如在我的例子中为/home/sunny/workspace/model-tool/
:
set(PROTOBUF_PROTOC_EXECUTABLE ${REPO_ROOT}/build/third_party/protobuf/cmake/protoc)
list(APPEND PROTO_FILES
"${REPO_ROOT}/protobuf/onnx-operators-ml.proto"
"${REPO_ROOT}/protobuf/onnx-ml.proto")
set(output_dir ${REPO_ROOT}/include)
set(protoc_include ${REPO_ROOT}/protobuf)
foreach(fil ${PROTO_FILES})
get_filename_component(abs_fil ${fil} ABSOLUTE)
get_filename_component(fil_we ${fil} NAME_WE)
list(APPEND ${srcs_var} "${output_dir}/${fil_we}.pb.cc")
list(APPEND ${hdrs_var} "${output_dir}/${fil_we}.pb.h")
add_custom_command(
OUTPUT "${output_dir}/${fil_we}.pb.cc"
"${output_dir}/${fil_we}.pb.h"
COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} --cpp_out ${output_dir} -I${protoc_include} ${abs_fil}
DEPENDS ${abs_file}
COMMENT "Running C++ protocol buffer compiler on ${fil}" VERBATIM )
endforeach()
官方文档中该命令的签名有两个形式,在开源的项目中经常看到的是下面这个形式:
add_custom_command(OUTPUT output1 [output2 ...]
COMMAND command1 [ARGS] [args1...]
[COMMAND command2 [ARGS] [args2...] ...]
[MAIN_DEPENDENCY depend]
[DEPENDS [depends...]]
[BYPRODUCTS [files...]]
[IMPLICIT_DEPENDS depend1
[ depend2] ...]
[WORKING_DIRECTORY dir]
[COMMENT comment]
[DEPFILE depfile]
[JOB_POOL job_pool]
[VERBATIM] [APPEND] [USES_TERMINAL]
[COMMAND_EXPAND_LISTS])
从中可以看到,只有OUTPUT
以及COMMAND
这两个参数是必须的,也就是说,正常情况下只要正确提供了这两个参数的值,在构建的时候肯定会执行这条命令生成的规则去编译.proto
。但是在我确认提供的参数都没问题的情况下,这条命令依旧没有按照预期工作。
这就非常奇怪了,坦白的讲,我的例子中的命令就是从ONNX Runtime中拷贝过来,只不过将一些变量的值修改成了指向我本地机器中的文件而已,它在别人的工程中能执行,为什么到了我这就不好使了呢?把可选的参数尝试了一遍,仍然木有结果。
我终于意识到,这样蛮干是不行的,即便瞎猫碰上死耗子偶然尝试对了一种组合,我依旧不知道它为什么又行了,回头再需要编写其他命令的时候一样抓瞎。还是需要去文档中寻找答案。
好在,最终我还是从文档中悟出了答案。
0x02. 解惑
其实在官方的文档中一开始就说的很明白了,只不过当时着急,并没有认真看对整个命令的综述,而是着急忙慌地去看应该怎么去构造每个参数的值。官方文档中是这么说的:
This defines a command to generate specified OUTPUT file(s). A target created in the same directory (CMakeLists.txt file) that specifies any output of the custom command as a source file is given a rule to generate the file using the command at build time.……In makefile terms this creates a new target in the following form:
OUTPUT: MAIN_DEPENDENCY DEPENDS
COMMAND
看到这一段话,我已经知道在我的项目中为什么这个命令不好使了:只有当构建的目标以add_custome_command
生成的OUTPUT文件为源代码的情况下,add_custome_command
中指定的命令才会才会执行。到目前为止,我并没有在CMakeLists.txt中生成目标文件的时候使用到诸如model-ml.pb.h, model-ml.pb.cc
这些文件,也就是说当构建我的代码的时候,根本就用不到model-ml.pb.h, model-ml.pb.cc
,既然用不到,那生成它们干啥呢?因此“聪明”的构建系统就不去执行编译.proto
的命令了。
我们知道,Makefile文件由一系列规则(rules)构成,规则的形式如下所示:
:
[tab]
根据我的项目里CMakeLists.txt中的内容,会生成一个Makefile文件(Ubuntu中默认情况下),其形式大概如下:
model_tool: main.cpp onnx-ml.pb.cc
C++ -o model_tool main.cpp onnx-ml.pb.cc
onnx-ml.pb.cc: onnx-ml.proto
protoc --cpp_out ./include -I./protobuf/ ./protobuf/onnx-ml.proto
为了生成model_tool
,需要先生成onnx-ml.pb.cc
,因此需要先执行protoc
命令。而如果我再CMakeLists.txt中并没有将onnx-ml.pb.cc
指定为生成model_tool
的源文件之一,所生成的Makefile便会如下面所示 ,此时规则1对规则2就不存在依赖关系,因此protoc
就不会执行了。
model_tool: main.cpp
C++ -o model_tool main.cpp
onnx-ml.pb.cc: onnx-ml.proto
protoc --cpp_out ./include -I./protobuf/ ./protobuf/onnx-ml.proto
想让model_tool
对onnx-ml.pb.cc
形成依赖也很简单,只要在将onnx-ml.pb.cc
作为值传个最终生成model_tool
的命令add_executable
就行,如下所示:
list(APPEND CXX_SRCS ${REPO_ROOT}/src/main.cpp
${REPO_ROOT}/include/onnx-ml.pb.cc)
add_executable(model_tool ${CXX_SRCS}
我一开始就是因为没将onnx-ml.pb.cc
也列为生成model_tool
的源文件,才导致add_custom_command
没有效果。至于main.cpp
中是不是真的引用了onnx-ml.pb.cc
的内容,Who care?
0x03 总结
作为总结,这里展示一个小Demo,文件结构如下:
demo/
CMakeLists.txt
main.cpp
source.txt
utils.h
其中每个文件中的内容如下:
// main.cpp
#include "utils.h"
int main(int argc, char **argv) {
greeting("Sunny");
return 0;
}
// utils.h
#ifndef MY_OWN_DEADER__
#define MY_OWN_HEADER__
#include
#include
void greeting(std::string who);
#endif // #define MY_OWN_HEADER__
// source.txt
#include
#include
#include "utils.h"
void greeting(std::string who) {
std::cout<< "Hello " << who << std::endl;
}
此时,如果CMakeLists.txt的内容如下所示,则会执行cat source.txt > test_file.cpp
这条命令生成test_file.cpp
,编译得以通过:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(demo VERSION 0.1 LANGUAGES C CXX)
add_custom_command(OUTPUT test_file.cpp
COMMAND cat source.txt > test_file.cpp
DEPENDS source.txt
COMMENT "Just copy file contents")
add_executable(demo main.cpp test_file.cpp)
而如果CMakeLists.txt的内容如下所示,则cat source.txt > test_file.cpp
便不会执行,在链接阶段就会因为缺少test_file.cpp
中的函数实现而失败:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.5)
project(demo VERSION 0.1 LANGUAGES C CXX)
add_custom_command(OUTPUT test_file.cpp
COMMAND cat source.txt > test_file.cpp
DEPENDS source.txt
COMMENT "Just copy file contents")
add_executable(demo main.cpp)
唯一的区别就是有没有在add_executable
命令中指明demo
对test_file.cpp
的依赖。
欢1迎2关3注4个5人6微7信8公9众10号:爱码士1024
0x03. References
[1] https://cmake.org/cmake/help/latest/command/add_custom_command.html