C++ CMake:在默认情况下,如何使add_custom_命令输出保持最新?
我有一个基于CMake 3.11.4和Python 3.7的软件环境 我的库/程序有一个C++ CMake:在默认情况下,如何使add_custom_命令输出保持最新?,c++,visual-studio-2015,cmake,C++,Visual Studio 2015,Cmake,我有一个基于CMake 3.11.4和Python 3.7的软件环境 我的库/程序有一个config.txt文件,以我指定的格式描述它们的依赖关系。然后,我有一个Python脚本(scripts/configure.py),它动态生成CMakeLists.txt,然后调用CMake生成一个可由Visual Studio 2015构建的解决方案 我希望当用户编辑config.txt时,Python会自动再次运行 因此,我让我的Python脚本在生成的CMakeLists.txt中添加一个自定义命令
config.txt
文件,以我指定的格式描述它们的依赖关系。然后,我有一个Python脚本(scripts/configure.py
),它动态生成CMakeLists.txt
,然后调用CMake生成一个可由Visual Studio 2015构建的解决方案
我希望当用户编辑config.txt
时,Python会自动再次运行
因此,我让我的Python脚本在生成的CMakeLists.txt
中添加一个自定义命令语句。下面是一个名为“myproject”的项目的外观,该项目包括两个库“lib1”和“lib2”
${SDE\u ROOT\u DIR}/build/myproject/CMakeLists.txt
包含:
# Automatically re-run configure project when an input file changes:
set( PROJECT_DEPENDENCIES )
list( APPEND PROJECT_DEPENDENCIES "${SDE_ROOT_DIR}/lib/lib1/config.txt" )
list( APPEND PROJECT_DEPENDENCIES "${SDE_ROOT_DIR}/lib/lib2/config.txt" )
ADD_CUSTOM_COMMAND( OUTPUT ${SDE_ROOT_DIR}/build/myproject/CMakeLists.txt COMMAND tools/python/Python370/python.exe scripts/configure.py myproject WORKING_DIRECTORY ${SDE_ROOT_DIR} DEPENDS ${PROJECT_DEPENDENCIES} )
以下是我的工作:
- 我运行脚本(
)生成scripts/configure.py myproject
和Visual Studio解决方案CMakeLists.txt
- 然后我打开解决方案
- 第一次构建时,它报告生成CMakeLists.txt,我看到调用了我的脚本
这不是预期的强>scripts/configure.py
- 第二次构建时,什么都没有发生。这没关系
- 如果我编辑
,下次构建时,我会看到config.txt
,我会看到调用了我的脚本生成CMakeLists.txt
。那很好scripts/configure.py
CMakeLists.txt
,而且肯定比config.txt
更新,我不明白为什么它需要再次生成CMakeLists.txt
知道我做错了什么吗?是否有其他命令需要添加到CMakeLists.txt
,以使此自定义命令的输出默认为“最新”
这是一个MCVE(
config.txt
替换为prgname.txt
):
prg/main.cpp
:
#include <iostream>
int main( int argc, char* argv[] )
{
std::cout << "Hello World!" << std::endl;
return 0;
}
脚本/configure.py
:
import sys
import subprocess
import argparse
import os
from contextlib import contextmanager
@contextmanager
def pushd(newDir):
previousDir = os.getcwd()
os.chdir(newDir)
yield
os.chdir(previousDir)
def configure_project():
# check configuration args
parser = argparse.ArgumentParser(description="CMakeLists generator.")
parser.add_argument('project', metavar='project', type=str, help='project name')
args = parser.parse_args()
working_directory = os.getcwd()
project = args.project
buildfolder = os.path.normpath(os.path.join( os.path.dirname(os.path.abspath(__file__)), os.pardir, "build", project ))
if not os.path.isdir(buildfolder):
os.makedirs(buildfolder)
prgsourcefolder = os.path.normpath(os.path.join( os.path.dirname(os.path.abspath(__file__)), os.pardir, "prg" ))
prgbuildfolder = os.path.join( buildfolder, "prg" )
if not os.path.isdir(prgbuildfolder):
os.makedirs(prgbuildfolder)
prgnamepath = os.path.join( prgsourcefolder, "prgname.txt" )
with open( prgnamepath, "r" ) as prgnamefile:
prgname = prgnamefile.read()
with open( os.path.join( prgbuildfolder, "CMakeLists.txt" ), "w" ) as cmakelists:
cmakelists.write( "add_executable(" + prgname + " " + os.path.join(prgsourcefolder,"main.cpp").replace("\\","/") + ")\n" )
cmakelistspath = os.path.join( buildfolder, "CMakeLists.txt" )
with open( cmakelistspath, "w" ) as maincmakelists:
maincmakelists.write( "cmake_minimum_required(VERSION 3.11)\n" )
maincmakelists.write( "project(" + project + ")\n" )
maincmakelists.write( "add_subdirectory(prg)\n" )
maincmakelists.write( "add_custom_command( OUTPUT " + cmakelistspath.replace("\\","/") + " COMMAND python " + " ".join( [ x.replace("\\","/") for x in sys.argv] ) + " WORKING_DIRECTORY " + working_directory.replace("\\","/") + " DEPENDS " + prgnamepath.replace("\\","/") + ")\n" )
# Run CMake:
with pushd( buildfolder ):
cmd = ['cmake.exe', '-G', 'Visual Studio 14 2015 Win64', buildfolder]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
out = proc.stdout.read(1)
if proc.poll() != None:
break
sys.stdout.write(out.decode())
sys.stdout.flush()
proc.wait()
if __name__ == "__main__":
import sys
sys.exit( configure_project() )
- 将CMake和Python添加到
路径中
- 从脚本文件夹运行
python configure.py myproject
- 打开
build/myproject/myproject.sln
- 点击“全部编译”,您将在日志中看到意外消息
生成CMakeLists.txt
添加自定义命令
创建一个目标不依赖的命令。因此,它不会被执行。我不知道为什么VisualStudio第一次运行它,但在Linux上Python脚本从未运行过
为了让它正常工作,您还需要一个目标。在这种情况下,可以与ALL
选项一起使用,并将自定义命令的输出指定为其依赖项。这样,自定义目标将在需要时运行该命令
只需添加一行,如
maincmakelists.write( "add_custom_target( Configure ALL DEPENDS " + cmakelistspath.replace("\\","/") + " )\n" )
在您编写的命令之后添加_custom_命令
,它应该可以工作。对我来说是这样的
请注意,自定义目标将始终运行,但自定义命令仅在修改prgname.txt文件时运行。这里是一个“解决方案”,而不是“解决方案”
我只需添加一个状态文件,告知何时应跳过从VS调用的generate,因为我知道解决方案是最新的:
向解析器添加新参数:
parser.add_argument('--from_vs', action='store_true', help='identify that configure is ran from VS to prevent useless regeneration, don't set this manually please')
从脚本本身正确维护此文件:
vs_force_up_to_date_file = os.path.join( buildfolder, "vs_force_up_to_date" )
if args.from_vs:
is_up_to_date = False
if os.path.isfile( vs_force_up_to_date_file ):
with open( vs_force_up_to_date_file, "r" ) as file:
content = file.readlines()[0]
is_up_to_date = ( content == "True" )
if is_up_to_date:
# It's the first time VS runs this script, we know it is up-to-date, so let's not regenerate
# See https://stackoverflow.com/questions/59861101/cmake-how-can-i-make-add-custom-command-output-up-to-date-by-default
print( "First time calling generate from VS, project is likely up-to-date, let's not reconfigure!" )
# Make next generate from VS be skipped
with open( vs_force_up_to_date_file, "w" ) as file:
file.write( str(False) )
exit(0)
else:
# need to generate, let's continue!
pass
else:
# Generating from console, let's make VS believe it is up to date for first time it will generate
if os.path.isfile( vs_force_up_to_date_file ):
# Let the file as it is, "True" if VS generate never ran, else False
pass
else:
# Create the file to prevent first VS generate to rerun this script while it does not need to be ran
with open( vs_force_up_to_date_file, "w" ) as file:
file.write( str(True) )
从VS运行时设置此新参数:
run_args = " ".join( [ x.replace("\\","/") for x in sys.argv] )
if not args.from_vs:
run_args += " --from_vs"
maincmakelists.write( "add_custom_command( OUTPUT " + cmakelistspath.replace("\\","/") + " COMMAND python " + run_args + " WORKING_DIRECTORY " + working_directory.replace("\\","/") + " DEPENDS " + prgnamepath.replace("\\","/") + ")\n" )
这使得VS在第一次生成请求时配置的无用调用被跳过,并且所需的调用将按预期工作
编辑
实际上,这并不像预期的那样有效。因为VS在每次配置时都会第一次运行脚本。因此,在以发布模式构建一次后,切换到调试将再次生成CMakeLists.txt,而这不应该发生,因为第一代在vs_force_up_to_date文件中写入了False。这是一个太幼稚的解决方案
相反,我最终采用的解决方案是将所有输入文件(prgname.txt)和输出文件(CMakeLists.txt)的路径传递给脚本,并让脚本检查所有输出是否比所有输入都要新,跳过生成。然后,无论VS对脚本执行什么意外调用,它们都将由脚本本身正确处理。Hi。谢谢你看看我的问题。不幸的是,这并不能解决问题。拥有这个自定义目标只会使“配置”项目出现在VS中(然后我可以根据需要手动运行它)。但是,这并不能阻止VS在我第一次编译时运行它,这正是我想要避免的。@jpo38从Visual Studio在没有附加目标的情况下运行该命令这一事实判断,这可能表明这是不可能避免的。之后,它是否只在修改prgname.txt时重新运行Python脚本?是的,它在我第一次编译时运行,然后在我修改prgname.txt之前不会运行它。因此,它显然能够检测到何时需要运行。@jpo38这很好听。如果你真的不需要它第一次运行,你可以尝试使用另一个发电机,如忍者。似乎Visual Studio从开始就支持它。但我需要找到一种方法使它与CMake和VS2015一起工作。您没有指定要使用的CMake版本。我在你的一篇评论中看到,你希望它能在VS2015中运行,所以这些是你的代表性工作示例中缺少的一些重要细节。@JorgeBellon:没错,刚刚在我的帖子中添加了这一点。
run_args = " ".join( [ x.replace("\\","/") for x in sys.argv] )
if not args.from_vs:
run_args += " --from_vs"
maincmakelists.write( "add_custom_command( OUTPUT " + cmakelistspath.replace("\\","/") + " COMMAND python " + run_args + " WORKING_DIRECTORY " + working_directory.replace("\\","/") + " DEPENDS " + prgnamepath.replace("\\","/") + ")\n" )