如何在CMake生成的Makefile中编译时运行命令?

35

我想向编译器传递一些选项。 这个选项需要在编译时计算 - 每次调用'make'时计算,而不是在'cmake'时计算,因此execute_process命令无法胜任。 (它可以吗?)

例如,在g ++编译器中传递日期:

g++ prog.cpp -o prog -DDATETIME="17:09:2009,14:25"

有没有办法在CMake中使用编译时计算的DATETIME?

赏金修改:

最好是一个简单实用的方案。

请注意,我希望能够在编译时执行任意命令,而不仅仅是"date"命令。

编辑2:

它必须适用于Linux、Windows(VS)、Mingw、Cygwin和OS X。 不能假设Ruby、Perl或Python,因为它们在Windows上是非标准的。 可以假设BOOST,但我猜这没有用。

目标是强制cmake生成Makefile(在Linux上),当执行make命令时,会自动完成操作。

创建自定义*.h文件是可以接受的,但必须由Makefile(或其他等价的工具)发起。 *.h文件的创建不需要(也不应该)使用cmake。

4个回答

46

你忽略了一些信息,比如你需要在哪些平台上运行代码以及是否有可用的其他工具。如果你可以使用Ruby、Perl或Python,那么情况会简单得多。我假设你想在Unix和Windows平台上运行代码,并且没有额外的可用工具。

如果你希望从命令中获取输出并将其存储到预处理器符号中,最简单的方法是生成一个头文件,而不是在命令行参数上进行调整。请记住,CMake有一个脚本模式(-P),它仅处理文件中的脚本命令,因此你可以这样做:

CMakeLists.txt:

project(foo)  
cmake_minimum_required(VERSION 2.6)
add_executable(foo main.c custom.h)
include_directories(${CMAKE_CURRENT_BINARY_DIR})  
add_custom_command(OUTPUT custom.h 
    COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/custom.cmake)

"custom.h"文件是通过编译时执行命令"cmake-P custom.cmake"生成的。custom.cmake文件的内容如下:

execute_process(COMMAND uname -a 
    OUTPUT_VARIABLE _output OUTPUT_STRIP_TRAILING_WHITESPACE)
file(WRITE custom.h "#define COMPILE_TIME_VALUE \"${_output}\"")

它执行指定的命令(在本例中为“uname -a”,您将替换为想要的任何命令),并将输出放到变量_output中,然后将其写入custom.h。请注意,这只适用于输出单行命令。(如果需要多行输出,则必须编写更复杂的custom.cmake文件,具体取决于您如何将多行数据插入程序中。)

主要程序如下:

#include <stdio.h>
#include "custom.h"
int main()
{
  printf("COMPILE_TIME_VALUE: %s\n", COMPILE_TIME_VALUE);
  return 0;
}

如果你想在编译时计算编译器选项,那么事情就变得更加棘手。对于 Bourne-shell 生成器,你只需要将命令插入反引号中。如果在确定引用方式时感到困惑,请将命令的所有逻辑放置到一个 shell 脚本中,这样你只需在 add_definitions() 中放置 mycommand.sh 即可。

if(UNIX)
  add_definitions(`${CMAKE_CURRENT_SOURCE_DIR}/custom-options.sh`)
endif()
对于基于Windows批处理文件的生成器,情况要复杂得多,我没有一个好的解决方案。问题在于PRE_BUILD命令不会作为与实际编译器调用相同的批处理文件的一部分执行(请查看BuildLog.htm以获取详细信息),因此我的初始想法无法实现(在PRE_BUILD步骤中生成custom.bat,然后对其执行“call custom.bat”,以设置一个可在编译器命令行中引用的变量)。如果批处理文件中有反引号的等效物,那么这个问题将得到解决。
希望这能给予一些思路和起点。
(现在不可避免地会有一个反问:你真正想做什么?)
编辑:我不确定为什么你不想让CMake用于生成头文件。使用${CMAKE_COMMAND}将扩展为用于生成Makefiles/.vcproj文件的CMake,由于CMake实际上不支持便携式Makefiles/.vcproj文件,因此您需要在目标机器上重新运行CMake。
CMake还有一堆实用命令(运行“cmake -E”获取列表)用于此明确原因。例如,您可以执行以下操作:
add_custom_command(OUTPUT custom.h COMMAND ${CMAKE_COMMAND} -E copy file1.h file2.h)

将file1.h复制到file2.h。

如果您不想使用CMake生成头文件,那么您需要调用单独的.bat/.sh脚本来生成头文件,或者使用echo进行生成:

add_custom_command(OUTPUT custom.h COMMAND echo #define SOMETHING 1 > custom.h)

根据需要进行引用。


我澄清了我的问题。您能否在这个角度上澄清您的回答? - Łukasz Lew

7
上面的解决方法(使用单独的CMake脚本文件生成头文件)似乎非常灵活,但对于示例中所做的事情来说有些复杂。
另一种选择是在单个源文件和/或目标上设置COMPILE_DEFINITIONS属性,在这种情况下,定义的预处理器变量仅在编译目标中的源文件时设置。
COMPILE_DEFINITIONS属性具有与add_definitions命令中使用的格式不同,并且具有优点,即您无需担心“-D”或“\D”语法,它们可以跨平台工作。
示例代码
-- CMakeLists.txt --
execute_process(COMMAND svnversion
    WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
    OUTPUT_VARIABLE SVN_REV)
string(STRIP ${SVN_REV} SVN_REV)

execute_process(COMMAND date "+%Y-%m-%d-%H:%M"
    OUTPUT_VARIABLE BUILD_TIME)
string(STRIP ${BUILD_TIME} BUILD_TIME)

set_source_files_properties(./VersionInfo.cpp
    PROPERTIES COMPILE_DEFINITIONS SVN_REV=\"${SVN_REV}\";BUILD_TIME=\"${BUILD_TIME}\"")

第一行运行一个Shell命令svnversion,并将结果存入变量SVN_REV中。使用string(STRIP ...)命令可以去除输出的尾随换行符。

请注意,这假定正在运行的命令是跨平台的。如果不是,则可能需要有不同平台的替代方案。例如,我使用Unix date命令的Cygwin实现,并拥有:

if(WIN32)
 execute_process(COMMAND cmd /C win_date.bat
    OUTPUT_VARIABLE BUILD_TIME)
else(WIN32)
  execute_process(COMMAND date "+%Y-%m-%d-%H:%M"
    OUTPUT_VARIABLE BUILD_TIME)
endif(WIN32)
string(STRIP ${BUILD_TIME} BUILD_TIME)

对于日期命令,其中win_date.bat是一个批处理文件,可以按所需格式输出日期。

这两个预处理变量在文件./VersionInfo.cpp中不可用,但在其他任何文件中都没有设置。您可以这样做:

-- VersionInfo.cpp --

std::string version_build_time=BUILD_TIME;
std::string version_svn_rev=SVN_REV;

这种方法在各个平台上都能很好地工作,并且最大限度地减少了特定于平台的代码量。

2
我看到的一个问题是,文件VersionInfo.cpp不会改变,因此不会被重新编译。你会如何解决这个问题?简单地触发手动重新编译会使整个cmake魔法变得不太有用,因为它不会比直接使用__DATE____TIME__宏做更多的事情。 - Gerald Senarclens de Grancy

2
我会采用以下方法:
  1. 创建一个可执行文件,将当前日期打印到stdout(CMake缺少此功能)
  2. 添加一个始终被视为过时的目标
  3. 让目标调用另一个CMake脚本
  4. 让被调用的CMake脚本生成一个头文件

这是示例代码:

--- CMakeLists.txt ---

PROJECT(Foo)
ADD_EXECUTABLE(RetreiveDateTime ${CMAKE_CURRENT_SOURCE_DIR}/datetime.cpp)
ADD_CUSTOM_TARGET(GenerateFooHeader
                  COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_SOURCE_DIR}/Generate.cmake
                  WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                  DEPENDS RetreiveDateTime)
ADD_EXECUTABLE(Foo "test.cpp" "${CMAKE_CURRENT_BINARY_DIR}/generated.h")
ADD_DEPENDENCIES(Foo GenerateFooHeader)

--- Generate.cmake ---

EXECUTE_PROCESS(COMMAND ${CMAKE_BINARY_DIR}/RetreiveDateTime OUTPUT_VARIABLE DATETIMESTRING)
MESSAGE(STATUS "DATETIME=\"${DATETIMESTRING}\"")
CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/generated.h.in ${CMAKE_CURRENT_BINARY_DIR}/generated.h @ONLY)

--- generate.h.in ---

#pragma once

#define DATETIMESTRING "@DATETIMESTRING@"

--- datetime.cpp ---

#include <iostream>
#include <ctime>
#include <cstring>

int main(int, char*[])
{
 time_t now;
 time(&now);
 tm * timeinfo = localtime(&now);

 char * asstring = asctime(timeinfo);
 asstring[strlen(asstring) - 1] = '\0'; // Remove trailing \n
 std::cout << asstring;
 return 0;
}

--- 测试.cpp ---

#include "generated.h"

#include <iostream>

int main(int, char*[])
{
 std::cout << DATETIMESTRING << std::endl;
 return 0;
}

这将导致在每次构建时重新生成一个名为"generated.h"的头文件。如果您不需要DATETIME,那么这个示例可以大大简化,因为CMake缺少此功能,必须构建程序来模拟该功能。
然而,在执行此操作之前,我建议您多次考虑。请记住,每次运行make时都会重新生成头文件,使您的目标始终无效。您永远不会拥有被视为最新的二进制文件。

-1

这个可以工作吗?

d=`perl -e"print qq(Whatever calculated at runtime);"`; g++ prog.cpp -o prog -DDATETIME=$$d

问题是关于CMake,而不是perl/bash。 - Łukasz Lew
@Łukasz Lew:在CMake中执行这行代码不是可能的吗? - Curd
2
是的,但它将在 cmake 时间执行而不是 make 时间。 - Łukasz Lew

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接