C++ CMake:在默认情况下,如何使add_custom_命令输出保持最新?

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中添加一个自定义命令

我有一个基于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
中添加一个自定义命令语句。下面是一个名为“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
    )生成
    CMakeLists.txt
    和Visual Studio解决方案
  • 然后我打开解决方案
  • 第一次构建时,它报告生成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" )