CMake中执行shell命令之execute_process、add_custom_target和add_custom_command

背景

以下情况可能需要在CMake中执行shell脚本:

  • cmake未提供的功能而实际构建中又需要时,如获取Linux发行版本
  • 项目构建时需要执行脚本才能完成,如boost构建过程

有的需要shell脚本的返回值,而有的不需要,这个关系不大。本文主要关注的是在cmake中执行shell脚本的方法。

主要涉及三个命令:execute_process、add_custom_target和add_custom_command。

execute_process

通过execute_process方法可以执行多个子进程。

原型如下:

execute_process(COMMAND  []
                [COMMAND  []]...
                [WORKING_DIRECTORY ]
                [TIMEOUT ]
                [RESULT_VARIABLE ]
                [RESULTS_VARIABLE ]
                [OUTPUT_VARIABLE ]
                [ERROR_VARIABLE ]
                [INPUT_FILE ]
                [OUTPUT_FILE ]
                [ERROR_FILE ]
                [OUTPUT_QUIET]
                [ERROR_QUIET]
                [COMMAND_ECHO ]
                [OUTPUT_STRIP_TRAILING_WHITESPACE]
                [ERROR_STRIP_TRAILING_WHITESPACE]
                [ENCODING ]
                [ECHO_OUTPUT_VARIABLE]
                [ECHO_ERROR_VARIABLE]
                [COMMAND_ERROR_IS_FATAL ])

命令COMMAND会并行执行,每个子进程的标准输出映射到下一个进程的标准输入上,所有进程共用standard error管道。

各选项说明如下:

  • COMMAND: 子进程的命令行,直接使用操作系统api执行。可以提供多个command,它们会并行执行。如果需要多个命令顺序执行,可以调用execute_process多次
  • WORKING_DIRECTORY:在该目录下执行COMMAND命令
  • TIMEOUT:超时时间,过了这个时间,所有子进程会被终止,RESULT_VARIABLE会被设置为“timeout”
  • RESULT_VARIABLE:最后一个子进程的返回值(正常是0,异常是其他整数),或者描述发生错误的字符串
  • RESULTS_VARIABLE:对应于每个子进程的返回值,使用分号分割的列表
  • OUTPUT_VARIABLE:对应于standard output的内容
  • ERROR_VARIABLE:对应于standard error的内容
  • INPUT_FILE:第一个子进程的standard input
  • OUTPUT_FILE:最后一个子进程的standard output
  • ERROR_FILE:所有子进程的standard error
  • OUTPUT_QUIET/ERROR_QUIET:忽略standard output 和 standard error
  • COMMAND_ECHO:重显命令到指定的标准设备,如STDERR、STDOUT、NONE。使用CMAKE_EXECUTE_PROCESS_COMMAND_ECHO变量来修改它的行为
  • OUTPUT_STRIP_TRAILING_WHITESPACE/ERROR_STRIP_TRAILING_WHITESPACE:删除空白字符
  • ENCODING:在windows系统上指定进程输出时的解码方式,默认是utf-8,其他平台会忽略该参数
  • ECHO_OUTPUT_VARIABLE/ECHO_ERROR_VARIABLE:输出将被复制,它将被发送到配置的变量中,也会在标准输出或标准错误中,3.18版本支持
  • COMMAND_ERROR_IS_FATAL:触发致命错误并终止进程执行,方式取决于参数。ANY表示任意命令执行失败都触发,LAST表示最后一个进程执行失败才触发,3.19版本支持

示例如下:

cmake_minimum_required(VERSION 3.2)

project(cmake_test)

execute_process(COMMAND echo "hello world"
                WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
                TIMEOUT 3
                RESULT_VARIABLE result_var
                OUTPUT_VARIABLE output_var
                ERROR_VARIABLE error_var
                OUTPUT_STRIP_TRAILING_WHITESPACE
                ERROR_STRIP_TRAILING_WHITESPACE)

message(STATUS "result: ${result_var}")
message(STATUS "output: ${output_var}")
message(STATUS "error: ${error_var}")

输出如下:

-- result: 0
-- output: hello world
-- error: 

如果要执行一个shell脚本,只需要把echo命令替换为如:bash a.sh 即可。

add_custom_target

添加自定义的没有输出的目标,它总会被构建。

原型如下:

add_custom_target(Name [ALL] [command1 [args1...]]
                  [COMMAND command2 [args2...] ...]
                  [DEPENDS depend depend depend ... ]
                  [BYPRODUCTS [files...]]
                  [WORKING_DIRECTORY dir]
                  [COMMENT comment]
                  [JOB_POOL job_pool]
                  [VERBATIM] [USES_TERMINAL]
                  [COMMAND_EXPAND_LISTS]
                  [SOURCES src1 [src2...]])

目标没有输出文件,总是被认为是过时的,可以使用 add_custom_command() 命令生成依赖的文件供 DEPENDS 参数使用。

常用参数说明如下:

  • Name:目标名称
  • ALL:说明该目标需要添加到默认目标的构建中,所以命令每次都会被执行。注意Name不能是ALL
  • COMMAND:构建时执行的命令,如果指定了多个COMMAND,它们将按顺序执行,但不一定组成有状态shell或批处理脚本。(要运行完整的脚本,可以使用configure_file命令或GENERATE命令来创建它,然后指定一个command来启动它。)
  • COMMENT:注释信息,会在命令执行前打印出来
  • DEPENDS:通常以同一CMakeLists.txt文件中的add_custom_command()命令生成的文件作为依赖,目标构建后依赖会被更为最新
  • SOURCES:生成目标所需要的额外的源文件,它们会被添加到IDE项目文件中
  • WORKING_DIRECTORY:执行命令的目标,如果是相对目录,则以当前源文件目录为基准

从以上说明可以看出,可以直接使用add_custom_target执行shell命令,并且使用DEPENDS可以让各个目标之间产生关联。

通常和add_custom_command命令配合使用来产生DEPENDS。

比如我们在编译boost库时,需要执行shell命令,示例如下:

add_custom_target(build_boost_libs
	COMMAND ./bootstrap.sh --prefix=/usr/local/boost
  COMMAND ./b2 link=static runtime-link=static threading=multi --with-system --with-thread --with-filesystem
  WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/boost/" 
  COMMENT "begin build boost libs...")

这样,我们自定义了目标build_boost_libs,它是通过两行命令来构建完成的。

关于add_custom_command,下面介绍。

add_custom_command

为构建系统添加自定义的构建规则。

它有两种形式的原型,下面分别介绍。

生成文件

签名如下:

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])

使用命令生成指定的输出文件。具体参数不再说明,详情可参考后文资料。

使用示例:

add_custom_command(
  OUTPUT out.c
  COMMAND someTool -i ${CMAKE_CURRENT_SOURCE_DIR}/in.txt
                   -o out.c
  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/in.txt
  VERBATIM)
  
add_library(myLib out.c)

这样,在生成myLib库时依赖out.c,而out.c由add_custom_command生成,每次in.txt的变动都会导致add_custom_command中命令的执行。

add_custom_command指定的DEPENDS可以是某个target(通过add_library/add_executable/add_custom_target创建),或者直接是某个文件。

如果add_custom_command命令不指定DEPENDS的话,那么只要没有这个OUTPUT的文件,都会生成自己并执行command。

构建事件

为库、可执行文件等目标添加自定义命令,可以在构建目标前或者构建目标后执行一些命令。

要执行的命令会成为目标的一部分,并且只在目标构建时执行,如果目标已经构建完成,这些命令也不会执行。

原型:

add_custom_command(TARGET 
                   PRE_BUILD | PRE_LINK | POST_BUILD
                   COMMAND command1 [ARGS] [args1...]
                   [COMMAND command2 [ARGS] [args2...] ...]
                   [BYPRODUCTS [files...]]
                   [WORKING_DIRECTORY dir]
                   [COMMENT comment]
                   [VERBATIM] [USES_TERMINAL]
                   [COMMAND_EXPAND_LISTS])

这样就为指定的target关联了要执行的命令,target必须在当前目录里定义。

命令执行的时机:

  • PRE_BUILD:在所有规则执行前执行
  • PRE_LINK:在源文件编译后且链接前执行
  • POST_BUILD:在所有规则执行后执行命令

其他参数不再说明。

示例:

# 在目标构建完成后执行一些操作
add_executable(myExe myExe.c)
add_custom_command(
  TARGET myExe POST_BUILD
  COMMAND someHasher -i "$"
                     -o "$.hash"
  VERBATIM)
add_custom_target vs add_custom_command

add_custom_target有依赖文件时,经常和add_custom_command的生成文件模式搭配使用。

它们之间的关系比较暧昧,这里说明一下。

当add_custom_target所要生成的target依赖add_custom_command所生成的文件时,这个文件就是一个纽带。

add_custom_command命令输出的OUTPUT文件和命令里的command之间的关系是:每当这个文件需要被重新生成时,都会执行这段command。

这个文件会不会被生成,取决于构建的target是否depends这个output文件。

这个文件会不会被重新生成,取决于这个output文件depends的东西变了没。

上面也有点绕,举例说明一下:

add_custom_command(OUTPUT config_bootstrap
  COMMAND ./bootstrap.sh --prefix=/usr/local
  WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/boost/" 
  COMMENT "begin config_bootstrap")

add_custom_target(build_boost_libs
  COMMAND ./b2 link=static runtime-link=static threading=multi --with-system --with-thread --with-filesystem
  WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}/boost/" 
  DEPENDS config_bootstrap
  COMMENT "begin build_boost_libs")

执行流程为:

  1. 开始构建目标build_boost_libs,发现它依赖config_bootstrap,cmake会根据该依赖和缓存决定是否重新构建该目标
  2. config_bootstrap是由add_custom_command生成的,它是否需要重新生成,取决于它自己的depends,该例中它没有depends,则只要没有config_bootstrap,就重新生成
  3. 只要config_bootstrap需要构建,add_custom_command中的命令就会被执行

这个流程与MakeFile决定是否重新编译目标是一个道理,它会自动识别模块间的依赖关系,并自己构建需要构建的模块。

写cmake的过程,也是告诉cmake模块间依赖关系的过程。

小结

cmake提供了对执行自定义命令的支持,可以很方便地使用它们执行shell命令。

但它们使用上有一些区别,比如有的会无条件每次执行,有的则依赖于依赖文件是否被更新。

具体使用哪种模式,就看需求了。

参考资料

execute_process
add_custom_target
add_custom_command

你可能感兴趣的:(工具使用,cmake,execute_process)