需求
在项目开发时,特别是Qt项目开发中,采用Shadow文件夹构建,即构建的目录与源码目录分离。我们经常碰到一个需求,即需要将源码中部分文件夹拷贝到生成目录(特别是exe所在目录)。或者是将运行时需要的动态库拷贝过来。
为方便项目构建,我们希望不需要每个成员编译时进行手动拷贝,而是编译中自动完成。在VS或Qt的IDE中,可以添加构建前/后的步骤,添加拷贝命令。
或者是告诉编译器由编译器完成。这里针对QMake项目,提出QMake实现文件拷贝的方法。
下面描述整个探索过程,省流找方案可跳转至总结的最后。
QMake拷贝文件
单纯从拷贝文件/目录的需求来说,应该是个很简单的需求(事实上也是),无非是一个copy命令,附带特定的参数。
事情复杂在于:
- 不同平台的拷贝命令有一些差别,包括指定的参数。以Qt的跨平台特性来说,写qmake也应该实现能跨平台。
- 特指Windows平台,会有一些坑。首先是拷贝命令不一定好用,主流分为用copy和xcopy,处理能否自动新建目录的情况。其次,Windows平台目录路径要用反斜杠\,而Qt中一般路径是用正斜杠/,比如一些预定义宏PWD等。要自己处理路径问题。
- 还有使用不同的方法命令,复制的时机可能不一样。
所以综合几种情况,会导致很多简单处理的示例无法生效,让人迷惑。
首先给出一个完整的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
debug: compiled = debug
else: compiled = release
EXE_DIR = $$OUT_PWD/$$compiled
SrcIncludeDIR = $$PWD/test/*.txt
DLL_DIR = $$PWD/TEST_1/*.dll
win32{
SrcIncludeFile = $$replace(SrcIncludeDIR, /, \\)
EXE_DIR = $$replace(EXE_DIR, /, \\)
DLL_DIR = $$replace(DLL_DIR, /, \\)
}
QMAKE_PRE_LINK += $$QMAKE_COPY_DIR $$SrcIncludeFile $$EXE_DIR\\test
QMAKE_PRE_LINK += && $$QMAKE_COPY_DIR $$DLL_DIR $$EXE_DIR\\TEST_1
针对上述几种问题处理逐一解决:
针对不同平台的拷贝命令,这里不使用具体的命令和参数,而是使用
QMAKE_COPY_DIR
。如最后注释所看见的,该变量会展开为xcopy /s /q /y /i
。具体来说,在"D:\Qt\5.12.1\msvc2017_64\mkspecs\features\spec_post.prf"
这个路径下定义了相关命名:QMAKE_COPY = copy /y QMAKE_COPY_FILE = $$QMAKE_COPY QMAKE_COPY_DIR = xcopy /s /q /y /i
然后针对于Windows平台,要对文件路径进行处理,处理时使用
replace
函数,替换正斜杠为反斜杠。最后,这里拷贝是添加到
QMAKE_PRE_LINK
中,该变量在编译器build链接前生效,执行命令。要注意的是,需要首次构建或者重新构建才能生效。Qt描述这个命令:1 2
QMAKE_PRE_LINK Specifies the command to execute before linking the TARGET together. This variable is normally empty and therefore nothing is executed.
总结和测试一下各种的命令,其生效方式和使用方法。
QMAKE_PRE_LINK/QMAKE_POST_LINK
QMAKE_PRE_LINK
即上面第一个示例的用法。QMAKE_POST_LINK
:一样的效果,只是复制发生在link之后。
写法:如前面第一个示例。
命令来源:QMake指令,能添加使用各种命令,这个使用的命令是
QMAKE_COPY_DIR
。命令解释参考上面Qt的帮助文档描述。命令展开:打开Makefile,可以看到命令展开如下。即在Link命令前/后,直接展开添加的命令。
1 2 3 4 5 6 7 8 9 10
first: all all: Makefile.Debug debug\QtTest.exe debug\QtTest.exe: ui_mainwindow.h $(OBJECTS) xcopy /s /q /y /i F:\Program\qt\QtTest\test\*.txt F:\Program\qt\build-QtTest-Desktop_Qt_5_12_1_MSVC2017_64bit-Debug\debug\test $(LINKER) $(LFLAGS) /MANIFEST:embed /OUT:$(DESTDIR_TARGET) @<< debug\main.obj debug\mainwindow.obj debug\test1.obj debug\moc_mainwindow.obj $(LIBS) << xcopy /s /q /y /i F:\Program\qt\QtTest\TEST_1\*.dll F:\Program\qt\build-QtTest-Desktop_Qt_5_12_1_MSVC2017_64bit-Debug\debug\TEST_1
生效方式:首次构建或重新构建。如前面Makefile看到的,是生成exe对象(即发生链接过程)时执行命令,意味着如果原本已经有exe对象,不需要重新链接的时候,不会执行命令。 编译输出:
1 2 3 4 5
xcopy /s /q /y /i F:\Program\qt\QtTest\test\*.txt F:\Program\qt\build-QtTest-Desktop_Qt_5_12_1_MSVC2017_64bit-Debug\debug\test 复制了 1 个文件 link /NOLOGO /DYNAMICBASE /NXCOMPAT /DEBUG /SUBSYSTEM:WINDOWS "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /MANIFEST:embed /OUT:debug\QtTest.exe @F:\Data\Temp\QtTest.exe.10860.1688.jom xcopy /s /q /y /i F:\Program\qt\QtTest\TEST_1\*.dll F:\Program\qt\build-QtTest-Desktop_Qt_5_12_1_MSVC2017_64bit-Debug\debug\TEST_1 复制了 1 个文件
COPIES
命令来源:
应该属于普通Makefile的指令,不属于预定义指令,可能也是QMake提供的。其解释如下:写法:这里写法比较方便的是,可以直接使用正斜杠,而不需要自己进行处理。(为什么不需要:是因为这个命令最后展开成
$(QINSTALL)
,而不是具体的xcopy
这种依赖于windows平台的命令,qt自己的命令肯定经过了跨平台处理)1 2 3 4 5 6
# 用两个拷贝示例 cp_kk.files += $$PWD/test/*.txt cp_kk.path += $$OUT_PWD/$$compiled/copy cp_dll.files += $$DLL_DIR cp_dll.path += $$EXE_DIR COPIES += cp_kk cp_dll
命令展开:打开Makefile查看如下。即将命令展开成:
$(QINSTALL)
.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
first: all all: Makefile.Debug debug\QtTest.exe debug\QtTest.exe: ui_mainwindow.h debug\copy\1.txt debug\1.DLL $(OBJECTS) $(LINKER) $(LFLAGS) /MANIFEST:embed /OUT:$(DESTDIR_TARGET) @<< debug\main.obj debug\mainwindow.obj debug\test1.obj debug\moc_mainwindow.obj $(LIBS) << ... compiler_copy_cp_kk_make_all: debug\copy\1.txt debug\copy\1.txt: ..\QtTest\test\1.txt $(QINSTALL) ..\QtTest\test\1.txt debug\copy\1.txt compiler_copy_cp_dll_make_all: debug\1.DLL debug\1.DLL: ..\QtTest\TEST_1\1.DLL $(QINSTALL) ..\QtTest\TEST_1\1.DLL debug\1.DLL
第一部分exe生成那里,依赖于拷贝的文件,这个展开是类似于上面那个的。不过这里没有展开拷贝命令。而是在最后构建的目标:
compiler_copy_cp_dll_make_all
. 在这里展开为QINSTALL
(所以说COPIES
大概也是QMake提供的)。生效方式:每次构建时即生效,因为命令不是依赖于构建exe,而是单独构建一个
compiler_copy_cp_dll_make_all
的目标。编译输出如下:1 2 3 4
D:\Qt\Tools\QtCreator\bin\jom.exe -f Makefile.Debug D:\Qt\5.12.1\msvc2017_64\bin\qmake.exe -install qinstall ..\QtTest\test\1.txt debug\copy\1.txt D:\Qt\5.12.1\msvc2017_64\bin\qmake.exe -install qinstall ..\QtTest\TEST_1\1.DLL debug\1.DLL link /NOLOGO /DYNAMICBASE /NXCOMPAT /DEBUG /SUBSYSTEM:WINDOWS "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /MANIFEST:embed /OUT:debug\QtTest.exe @F:\Data\Temp\QtTest.exe.280.46.jom
QMAKE_EXTRA_TARGETS
命令来源:类似于
QMAKE_PRE_LINK
等,由QMake预定义的变量。Qt帮助文档的解释:QMAKE_EXTRA_TARGETS
Specifies a list of additional qmake targets.
写法:这里除了
QMAKE_EXTRA_TARGETS
添加,还需要PRE_TARGETDEPS
添加,使得构建exe时,依赖于自己额外添加的对象,构成构建关系链。另外,这里具体命令还是使用QMAKE_COPY_DIR
,会展开成xcopy
,因此需要处理路径问题。1 2 3
cp_ext.commands += $$QMAKE_COPY_DIR $$SrcIncludeFile $$EXE_DIR\\test QMAKE_EXTRA_TARGETS += cp_ext PRE_TARGETDEPS += cp_ext
命令展开:
1 2 3 4 5 6 7 8 9 10 11 12
first: all all: Makefile.Debug debug\QtTest.exe debug\QtTest.exe: cp_ext ui_mainwindow.h $(OBJECTS) $(LINKER) $(LFLAGS) /MANIFEST:embed /OUT:$(DESTDIR_TARGET) @<< debug\main.obj debug\mainwindow.obj debug\test1.obj debug\moc_mainwindow.obj $(LIBS) << ... cp_ext: xcopy /s /q /y /i F:/Program/qt/QtTest/test/*.txt F:/Program/qt/build-QtTest-Desktop_Qt_5_12_1_MSVC2017_64bit-Debug/debug/copy
其实跟
COPIES
非常类似,这是COPIES
是预定义为拷贝的一种特殊情况,无需自行指定命令。生效方式:Make构建时生效。编译输出:
1 2 3 4
D:\Qt\Tools\QtCreator\bin\jom.exe -f Makefile.Debug xcopy /s /q /y /i F:\Program\qt\QtTest\test\*.txt F:\Program\qt\build-QtTest-Desktop_Qt_5_12_1_MSVC2017_64bit-Debug\debug\test 复制了 1 个文件 link /NOLOGO /DYNAMICBASE /NXCOMPAT /DEBUG /SUBSYSTEM:WINDOWS "/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /MANIFEST:embed /OUT:debug\QtTest.exe @F:\Data\Temp\QtTest.exe.11948.47.jom
QMAKE_SUBSTITUTES
写法:
1
2
3
cp_et.input = $$PWD/test/1.txt
cp_et.output = $$OUT_PWD/$$compiled/copy
QMAKE_SUBSTITUTES += cp_et
实现一个文件拷贝到另一个文件里面。具体展开为:
1
D:\Qt\5.12.1\msvc2017_64\mkspecs\features\lex.prf ..\QtTest\QtTest.pro ..\QtTest\test\1.txt D:\Qt\5.12.1\msvc2017_64\lib\Qt5Widgetsd.prl
关于这里使用到lex.prf:
# Lex extra-compiler for handling files specified in the LEXSOURCES variable
PRF作为QMake中高级用法的扩展:
总结
完整的示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
debug: compiled = debug
else: compiled = release
EXE_DIR = $$OUT_PWD/$$compiled
SrcIncludeDIR = $$PWD/test/*.txt
DLL_DIR = $$PWD/TEST_1/*.dll
win32{
SrcIncludeFile = $$replace(SrcIncludeDIR, /, \\)
EXE_DIR = $$replace(EXE_DIR, /, \\)
DLL_DIR = $$replace(DLL_DIR, /, \\)
}
# 拷贝方法1:QMAKE_PRE_LINK
QMAKE_PRE_LINK += $$QMAKE_COPY_DIR $$SrcIncludeFile $$EXE_DIR\\test # 注意路径中用\\
#QMAKE_PRE_LINK += && $$QMAKE_COPY_DIR $$DLL_DIR $$EXE_DIR\\TEST_1
QMAKE_POST_LINK += $$QMAKE_COPY_DIR $$DLL_DIR $$EXE_DIR\\TEST_1 # 测试POST
# 拷贝方法2:COPIES
cp_kk.files += $$PWD/test/*.txt
cp_kk.path += $$OUT_PWD/$$compiled/copy
cp_dll.files += $$DLL_DIR
cp_dll.path += $$EXE_DIR
COPIES += cp_kk cp_dll
COPIES += $$PWD/test/1.txt $$OUT_PWD/$$compiled
# 拷贝方法3:QMAKE_EXTRA_TARGETS
cp_ext.commands += $$QMAKE_COPY_DIR $$SrcIncludeFile $$EXE_DIR\\test
QMAKE_EXTRA_TARGETS += cp_ext
PRE_TARGETDEPS += cp_ext
# 拷贝方法4:
cp_et.input = $$PWD/test/1.txt
cp_et.output = $$OUT_PWD/$$compiled/copy
QMAKE_SUBSTITUTES += cp_et
主要的抽象包括:
- 将拷贝的行为作为构建链中的什么步骤、什么环节:一般是link前后的一个命令、或者作为一个构建目标等。为使构建每次都能生效(因为拷贝文件可能会发生变更),较好的抽象是将拷贝动作作为一个单独的构建目标。
- 拷贝动作具体使用什么命令:最坏的做法是考虑具体平台的命令
xcopy
、copy
,比较好是使用QMake变量,如QMAKE_COPY_DIR
,让其自行展开为特定平台的命令。比较特别的是COPIES
,展开为$(QINSTALL)
. - 考虑最后展开成的命令其接收的参数:如果最后展开为特定平台的命令,那么其接收参数就要考虑特定平台的约束。其中最主要是传递的文件路径参数,windows平台需要使用反斜杠。可以使用
replace
进行替换。最方便的是$(QINSTALL)
,无需自行处理参数。
综上所述(省流):拷贝文件,使用COPIES
。贴上QMake代码:
1
2
3
cp_kk.files += $$PWD/test/*.txt
cp_kk.path += $$OUT_PWD/$$compiled/copy
COPIES += cp_kk
Reference
- (31条消息) QT .pro 拷贝文件—windows版本_qt pro 拷贝文件_明月清风-精进不止的博客-CSDN博客:成功的完整示例。
- QMAKE_COPY 不同平台对应的命令 - lsgxeva - 博客园 (cnblogs.com): QMAKE_COPY命令。
- 使用qmake编译时拷贝文件 - soso101 - 博客园 (cnblogs.com):拷贝的各种方式。
后言
写这篇博客的过程还是有不少感悟的,有一些一直的想法,比如:
- 搞这个需求也不是一次两次了,但每次再碰到的时候,总觉得不太满意。明明看起来应该有更好的方案,但一直用不了,用的都是比较ugly的方法。所以一个问题重复出现以后,还是要刨根究底的搞清楚,形成自己的东西。
- 当然了,形成自己的东西。按照我比较笨的理解能力,基本上等同于像写书一样一步一步记录的完整过程。特别是形成可归档化的博客,耗时确实是挺长的。
当然原有的想法一直如此,那学习过程也是这样一直下去。也有新的一些念头和总结:
- 积累消化的过程,如果包括累次研究这个需求的时间,其实耗时是特别长的。相比于这个任务而言。本质上讲这篇博客基本是沿着Reference 3这篇博客的思路,包括有哪些方案,以及从Makefile角度深入了解的方向,写得已经很好了。从这个意义上面讲,这篇博客本质上没有什么创新和值得记录的。如果花费两分钟搜索找到前面这篇博客,然后就能回忆、找到这个解决方案的话。
- 那还是耗时这么久去做这件事是为什么呢,是因为上面这篇博客可能是基于Unix平台写的,没有考虑Windows平台上路径斜杠的问题,导致部分代码在Windows平台是无法直接运行的。之前可能拷贝代码尝试了一下发现不生效就跳过了这篇博客。
- 其实如果花费一点时间研究为什么不成功,再稍微多走一步,就已经可以快速解决这个问题。有很多通俗的道理描述这个问题,比如“行百里者半九十”。再比如程序员的深度优先和广度优先遍历。我太喜欢先把所有方案先列一遍,为了找一个最优解。但沿着一个方向搞透,太重要了。告诉自己:坚持、再坚持一下。
- 另外是信源的可信度问题。在研究这个问题上,研究很多次了,多次去搜索、找解决方案。看了很多。以及最近AI出了,这个过程也直接问AI。作为解决问题、寻找解决方案的核心能力,快速的一套搜索、检验、测试、归纳的能力很重要。速度和质量总是要精巧的进行平衡。像这里的对比,上面那篇博客质量就很好,但是我追求速度,一旦检验到一点问题就快速抛弃了,其实另外作为cnblog上面的博客,作为信源应该重视一点。相反,在询问AI过程,得到答案的速度很快,但经常胡说八道(特别是在这个问题上),那检验的时间上限就要降低。
- AI时代,成为工具的主人。追求一个目标,精准而优雅地解决,是能力的关键。
- 吐槽:为什么用AI呢?因为Qt关于QMake的文档真的太少了…坑。