原文作者:dmerej.info/
发布时间:2017年4月08日-15分钟阅读
在相当长的一段时间里,我一直在使用Visual Studio来构建复杂的C++项目的团队里面工作。
因为我经常是 "构建农场的家伙",而且我不太喜欢GUI,所以我必须找到从命令行构建Visual Studio项目的方法。
这就是我尝试过的所有方法的故事。
在我们开始之前的快速说明:在本文中,我将在Windows 10上使用Visual Studio 2015来构建CMake本身的源代码。If是一个很好的案例研究项目,因为它既不会太大,也不会太小,而且没有任何依赖关系需要担心(当然,它使用CMake来构建自己:)。
使用CMake来生成Visual Studio项目
CMake通过解析CMakeLists.txt文件中的代码来工作,然后生成代码,这些代码将被其他程序用来执行构建本身。
当你使用CMake时,你必须指定一个生成器。在Windows上,默认的生成器将是Visual Studio找到的最新的生成器,运行CMake后,你会得到一个.sln文件,你可以在Visual Studio中打开它来编辑、构建和调试项目。
所以我的任务是找到一种从命令行构建这些.sln文件的方法。
使用devenv
我发现最明显的方法是使用一个叫devenv的工具。事实上,如果你在互联网搜索引擎上查找 "从命令行构建Visual Studio项目",你可能会找到这样的答案。你还会发现有些地方他们建议你使用MSBuild.exe。
但是,运气不好,如果你试图直接从cmd.exe运行devenv,你会得到著名的错误信息。
'devenv' is not recognized as an internal or external command, operable program or batch file.
诀窍是使用你在开始菜单中找到的一个 "命令提示符"。
我从 "开发者命令提示符 "开始。
cd c:\User\dmerej\src\cmake\build-vs
devenv CMake.sln
Visual Studio打开了。哼,这不是我想要的。原来,如果你在命令行提示符中出错,Visual Studio就会打开。
正确的方法是添加/build开关。
devenv /build Debug CMake. sln
输出相当不错。
3> cmbzip2.vcxproj -> C:\...\src\cmake-3.7.2\build-vs\Utilities\cmbzip2\Debug\cmbzip2.lib
7>------ Build started: Project: cmjsoncpp, Configuration: Debug Win32 ------
7> Building Custom Rule C:/Users/dmerej/src/cmake-3.7.2/Utilities/cmjsoncpp/CMakeLists.txt
7> CMake does not need to re-run because
C:\...\src\cmake-3.7.2\build-vs\Utilities\cmjsoncpp\CMakeFiles\generate.stamp
is up-to-date.
2> Generating Code...
6> fs-poll.c
2> Compiling...
使用MSBuild
你也可以尝试使用MSBuild.exe,但输出的结果比较丑陋。但你可以得到更多的信息,比如编译一个项目所花的时间,使用的完整命令行,以及警告/错误的数量)。
Project
"c:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities\cmcompress\cmcompress.vcxproj.metaproj" (7)
is building
"c:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities\cmcompress\cmcompress.vcxproj" (8)
on node 1 (default targets).
InitializeBuildStatus:
Creating
"cmcompress.dir\Debug\cmcompress.tlog\unsuccessfulbuild"
because "AlwaysCreate" was specified.
CustomBuild:
Building Custom Rule C:/Users/dmerej/src/cmake-3.7.2/Utilities/cmcompress/CMakeLists.txt
CMake does not need to re-run because
C:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities\cmcompress\CMakeFiles\generate.stamp
is up-to-date.
ClCompile:
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\CL.exe /c
/I"C:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities"
/I"C:\Users\dmerej\src\cmake-3.7.2\Utilities" /Zi /nologo /W3 /WX- /Od /Ob0
/Oy- /D WIN32 /D _WINDOWS /D _DEBUG /D _CRT_SECURE_NO_DEPRECATE /D
_CRT_NONSTDC_NO_DEPRECATE /D CURL_STATICLIB /D "CMAKE_INTDIR=\"Debug\"" /D
_MBCS /Gm- /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline
/Fo"cmcompress.dir\Debug\\" /Fd"cmcompress.dir\Debug \cmcompress.pdb" /Gd /TC
/analyze- /errorReport:queue
"C:\Users\dmerej\src\cmake-3.7.2\Utilities\cmcompress\cmcompress.c"
cmcompress.c
...
cmArchiveWrite.cxx
cmBase32.cxx
cmBootstrapCommands1.cxx
...
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:06.93
使用CMake构建
好了,现在我知道如何从命令行构建Visual Studio项目了。
我们有一个相当大的C++代码库,我们想在Linux、macOS和Windows上构建。
我们使用Jenkins来做持续集成,所以我们必须编写构建脚本,一旦有开发人员提出合并请求,这些脚本就会在节点上运行,以确保所提出的更改能在所有平台上构建。
在Linux和macOS上,默认的生成器是 "Unix Makefiles",所以代码很直接。
所以代码很简单:
#/bin/bash -e
git pull
mkdir -p build
(
cd build
cmake ..
make
)
在Windows上,我们使用了Batch文件。
git pull
@call "%VS140COMNTOOLS%VsDevCmd.bat"
mkdir build
cd build
cmake ..
devenv foo.sln /build
你可能会想知道奇怪的@call"%VS140COMNTOOLS%VsDevCmd.bat"一行从何而来。
首先,如果你进入C:\ProgramData/Microsoft\Windows/Start Menu/Programs/Visual Studio 2015/Visual Studio Tools,你可以右键点击 "开发者命令提示符 "快捷方式,打开 "属性 "窗口。在那里你会发现,目标是:cmd.exe /k "C:\\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\VsDevCmd.bat"
其次,如果你幸运的话,有人1会告诉你,在任何一个版本的Visual Studio中,都会设置一个名为VS<version>COMNTOOLS的环境变量,其中<version>是你的Visual Studio安装的2或3位版本号。
如果你不明白我的意思,这里有一个表格。
"Marketing" version "Internal" version
Visual Studio 8 2005 80
Visual Studio 9 2008 90
Visual Studio 10 2010 100
Visual Studio 11 2012 120
Visual Studio 12 2013 130
Visual Studio 14 2015 140
因此,你可以避免硬编码Visual Studio安装路径,而使用VS140COMNTOOLS变量。(虽然你仍然需要对Visual Studio版本进行硬编码)。
因此,这个命令的作用是将VS140COMNTOOLS环境变量的值与提示文件的基名(VsDevCmd.bat)连接起来,然后在上面运行@call。
使用Python
但是,随着时间的推移,我们想用Python重写所有的构建脚本,这样我们就可以将一些代码因子化。(例如,在构建前运行git pull更新源码)
在Linux和macOS上,这很容易。
subprocess.check_call(["cmake", ".."], cwd=build_dir)
subprocess.check_call(["make"], cwd=build_dir)
但在Windows上,事情就有点棘手了。我们如何在Python中实现@call \path\to\bat_file呢?
setuptools 拯救了我们!
我发现setuptools--Python用来运行setup.py文件的模块--能够与Visual Studio一起构建东西,而不需要使用Visual Studio的命令提示。
所以我看了一下实现,发现了一个解决方案。
def source_bat(bat_file):
interesting = {"INCLUDE", "LIB", "LIBPATH", "PATH"}
result = {}
process = subprocess.Popen('"%s"& set' % (bat_file),
stdout=subprocess.PIPE,
shell=True)
(out, err) = process.communicate()
for line in out.split("\n"):
if '=' not in line:
continue
line = line.strip()
key, value = line.split('=', 1)
result[key] = value
return result
我们的想法是运行一个批处理脚本(这就是为什么我们使用shell=True),它将。
- 调用我们需要的
.bat文件 - 运行内置的
set命令并解析其输出结果 - 用Python dict返回整个环境。
事实上,在 cmd.exe 上有几种使用 set 的方法。
- 要设置一个环境变量:
set FOO=BAR - 要取消设置一个环境变量:
set FOO=。 - 要查看所有名称以前缀开头的环境变量:
set <prefix>。 - 要转储所有环境变量:
set
我们对set的输出进行解析,找到我们需要的三个变量(它们都是由分号分隔的路径组成的列表)
PATH:找到所需的可执行文件(devenv命令)。LIB:编译器寻找.lib文件的地方。INCLUDE:编译器会在这里寻找头文件。
顺便说一下,如果你想知道为什么这个函数叫source_bat,那是因为在Unix上,要执行一个bash脚本并更新你的环境,你需要使用内置的source,或者,在其他一些shell上,使用.命令,(但我离题了)。
构建.sln文件
不过还有一个问题。在 Linux 和 macOS 上,构建的命令总是 make。
但在Windows上,我不得不小心翼翼地制作devenv命令,这意味着要指定.sln文件的路径。
一开始,我只有几个不好的解决办法。
- 把
.sln文件的名字硬编码出来 - 解析顶部的
CMakeLists,找到project()call2。 - 列出构建目录的内容,并希望它们只有一个扩展名为
.sln的文件。
幸运的是,通过运行cmake --help,我发现有--build开关,我可以用它来抽象出要运行的项目的命令行。
所以代码看起来是这样的
def configure_and_build():
if os.name == "nt":
generator = "Visual Studio 14 2015"
else:
generator = "Unix Makefiles"
subprocess.check_call(["cmake", "-G", generator, ".."], cwd=build_dir)
subprocess.check_call(["cmake", "--build", ".", cwd=build_dir)
这意味着我可以在任何地方运行 cmake --build,而不必处理那些讨厌的 .bat 文件。
同时使用多个CPU
默认情况下,Visual Studio项目会使用所有的CPU资源进行构建,但make命令并非如此。
所以不得不再次对代码进行修补,让make使用所有可用的CPUS。
import multiprocessing
def build():
cpu_count = multiprocessing.cpu_count()
cmd = ["cmake", "--build", "."]
if os.name != "nt":
cmd.extend(["--", "-j", str(cpu_count)])
subprocess.check_call(cmd, cwd=build_dir, env=build_env)
(这里的--参数是为了将cmake二进制文件解析的参数和发送到底层编译命令的参数分开。这是命令行工具的常见做法)
性能问题
于是我们让Jenkins节点运行Python脚本,在Linux、macOS和Windows上构建相同的源代码,一切都很正常,只是在Windows上构建的时间会更长。
起初我想,"众所周知,在Windows上运行可执行文件和访问文件系统的速度总是比较慢,我们对此无能为力"。
但是,我的团队成员一直在抱怨构建时间太长,我心里很不是滋味:就像有人曾经说过的,"在做持续集成的时候,计算机应该是在等待人类,而不是相反"。
于是,我寻找提高性能的解决方案。
使用NMake
如果你看看使用Visual Studio时CMake生成的文件大小,你就会意识到要想有好的性能并不容易。
例如,要构建CMake,你有一个842行的.sln文件,它引用了115个.vcxproj文件。
从这些文件的内容来看,难怪解析这些文件要花不少时间。
// In CMake.sln
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CMakePredefinedTargets", "CMakePredefinedTargets", "{0FD1CAE9-153C-32D5-915F-6AB243496DE3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CTestDashboardTargets", "CTestDashboardTargets", "{991076F0-B37F-32E8-88B9-1156BDA0D346}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{81200686-E54D-3D48-BDD9-782FCB64B8A8}"
EndProject
...
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ALL_BUILD", "ALL_BUILD.vcxproj", "{BE9010A6-FD75-30CB-B4F7-EC8DD41D6F48}"
ProjectSection(ProjectDependencies) = postProject
{FF7E34C4-7170-3648-BBB6-B82173FFD31E} = {FF7E34C4-7170-3648-BBB6-B82173FFD31E}
{765BC85F-D03F-35FC-ADE0-26ED16D75F4D} = {765BC85F-D03F-35FC-ADE0-26ED16D75F4D}
...
<!-- in Source\cmake.vcxproj -->
<?xml version="1.0" encoding="UTF-8"?>
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
...
<ClCompile>
<AdditionalIncludeDirectories>C:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities;C:\Users\dmerej\src\cmake-3.7.2\Utilities;C:\Users\dmerej\src\cmake-3.7.2\build-vs\Source;C:\Users\dmerej\src\cmake-3.7.2\Source;C:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities\cmcompress;C:\Users\dmerej\src\cmake-3.7.2\Source\CTest;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AssemblerListingLocation>Debug/</AssemblerListingLocation>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<CompileAs>CompileAsCpp</CompileAs>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<ExceptionHandling>Sync</ExceptionHandling>
...
<PreprocessorDefinitions>WIN32;_WINDOWS;_DEBUG;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;CURL_STATICLIB;LIBARCHIVE_STATIC;UNICODE;_UNICODE;WIN32_LEAN_AND_MEAN;CMAKE_BUILD_WITH_CMAKE;CMAKE_INTDIR="Debug";%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ObjectFileName>$(IntDir)</ObjectFileName>
</ClCompile>
...
另外,如果你看一下构建过程中的CPU使用情况,你可以看到你远远没有使用所有的CPU功率。
所以我试图找到一个能生成更简单代码的CMake生成器。
在研究过程中,我发现微软有自己的Make程序实现,叫做NMake,所以我决定使用NMake Makefile生成器。
这次我将直接使用cl.exe、link.exe和它们的朋友。
我试着用MSBuild命令提示符,但得到的是。
cmake -G"NMake Makefiles" ..
-- The C compiler identification is unknown
-- The CXX compiler identification is unknown
CMake Error at CMakeLists.txt:11 (project):
No CMAKE_C_COMPILER could be found.
Tell CMake where to find the compiler by setting either the environment
variable "CC" or the CMake cache entry CMAKE_C_COMPILER to the full path to
the compiler, or to the compiler name if it is in the PATH.
CMake Error at CMakeLists.txt:11 (project):
No CMAKE_CXX_COMPILER could be found.
Tell CMake where to find the compiler by setting either the environment
variable "CXX" or the CMake cache entry CMAKE_CXX_COMPILER to the full path
to the compiler, or to the compiler name if it is in the PATH.
-- Configuring incomplete, errors occurred!
See also "C:/Users/dmerej/src/cmake-3.7.2/build-msbuild-nmake/CMakeFiles/CMakeOutput.log".
See also "C:/Users/dmerej/src/cmake-3.7.2/build-msbuild-nmake/CMakeFiles/CMakeError.log".
这里,CMake找不到cl.exe,因为它不在%PATH%中。事实上,如果你试图从MSBuild命令提示符中运行cl.exe,你也会得到同样的 "cl.exe is not recognized ... "错误。
所以我试着用我以前用过的 "开发者命令提示符 "来手动运行devenv。
mkdir build-nmake
cd build-nmake
cmake -G"NMake Makefiles" ..
-- The C compiler identification is MSVC 19.0.24210.0
-- The CXX compiler identification is MSVC 19.0.24210.0
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe -- works
Huzzah,编译器找到了!
你可能会注意到cl.exe的路径只是VCbin/cl.exe(VCbin中还有其他文件夹,但这里我们使用的是默认的32位版本)。
另外,我还高兴地发现,只生成了30多个Makefiles文件。
所以下一步是
cd build-nmake
nmake
[ 27%] Built target cmsysTestsCxx
[ 27%] Built target cmsysTestDynload
[ 44%] Built target cmlibarchive
[ 61%] Built target cmcurl
[ 62%] Built target LIBCURL
Scanning dependencies of target CMakeLib
[ 62%] Building CXX object Source/CMakeFiles/CMakeLib.dir/cmCommands.cxx.obj
cmCommands.cxx
[ 62%] Linking CXX static library CMakeLib.lib
我很高兴地看到,我得到了和Linux和macOS上一样漂亮的输出(有进度百分比)。
但后来我发现,在编译过程中只使用了一个CPU,而且运行nmake /?
而且运行nmake /? 并行运行多个作业也没有任何效果。
Frack!
使用JOM
再看cmake --help的输出,我发现还有一个叫 "NMake Makefiles JOM "的生成器。
Jom是由Qt人员制作的一个工具,它是对nmake命令的重新实现,但支持多个作业。它是nmake命令的重新实现,但支持多任务。用多CPU构建的命令行开关也叫-j,这很好,因为这意味着构建脚本代码会变得更简单。
这带来了相当不错的效果,但构建速度还是比Linux和macOS上慢。
为了调查,我决定在使用JOM进行构建时保持Windows资源监控器打开。
你可以看到在构建过程中,CPU的使用率有所下降。据我了解,这发生在链接过程中。
使用Ninja
终于,在2010年左右,Ninja出来了。
当我读到这个项目的描述时。"一个专注于速度的小型构建系统",以及在CMake中对它的实验性支持,我就迫不及待地想尝试一下。
而事实上,它给我带来了很好的结果。多年来第一次,我终于在Windows上获得了与Linux相同的构建时间,而且所有核心的CPU使用率都稳定在100%左右。
我还得到了给Ninja命名的梗概输出。3
cmake -GNinja ..
cmake --build .
[11/11] Linking CXX executable Tests\CMakeLib\CMakeLibTests.exe
一个关于交叉编译的故事
在使用CMake+Ninja组合数年之后,我在一次CI构建过程中得到了一个错误信息。
Linker fatal error: LNK1102: out of memory
谷歌错误导致:support.microsoft.com/en-us/help/…
于是我试着按照 "解决 "部分的建议,仔细查看了开始菜单中的命令行提示列表。
在上面的两个提示下面我已经试过了,有几个条目,都是同一个.bat文件的快捷方式,但是参数不同。
VS2015 x64 ARM "C:\...\Visual Studio 14.0\VC\vcvarsall.bat" amd64_arm
VS2015 x64 Native: "C:\...\Visual Studio 14.0\VC\vcvarsall.bat" amd64
VS2015 x66 x86 Cross "C:\...\Visual Studio 14.0\VC\vcvarsall.bat" amd64_x86
啊哈!所以,我所要做的就是用正确的参数来调用vcvarsall.bat文件,当我看了一下.bat文件的内容后,就确认了这一点。
...
:check_platform
if /i %1 == x86 goto x86
if /i %1 == amd64 goto amd64
...
goto usage
:x86
...
call "%~dp0bin\vcvars32.bat" %2 %3
goto :SetVisualStudioVersion
:amd64
...
call "%~dp0bin\amd64\vcvars64.bat" %2 %3
goto :SetVisualStudioVersion
于是又对Python代码进行了修补。
def source_bat(bat_file, arch):
interesting = {"INCLUDE", "LIB", "LIBPATH", "PATH"}
result = {}
process = subprocess.Popen('"%s %s"& set' % (bat_file, arch),
stdout=subprocess.PIPE,
shell=True)
(out, err) = process.communicate()
而CMake的输出是。
c:\Users\dmerej\src\cmake-3.7.2\build-cross-64-32>cmake -GNinja ..
-- The C compiler identification is MSVC 19.0.24210.0
-- The CXX compiler identification is MSVC 19.0.24210.0
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64_x86/cl.exe
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64_x86/cl.exe -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64_x86/cl.exe
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64_x86/cl.exe -- works
-- Detecting CXX compiler ABI info
注意amd64_x86子文件夹。
代码签名中断
有一段时间,一切都很正常,直到我们决定要在发货前对可执行文件进行签名。
似乎最明显的方法就是使用signtool.exe,所以我按照Windows Dev Center 4上的指示,继续安装Windows驱动程序套件。
但后来我们所有的构建都开始失败了。
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\crtdefs.h(10):
fatal error C1083:
Cannot open include file: 'corecrt.h': No such file or directory
我找了一个解决方案,但得到的都是人们告诉我用手设置INCLUDE和LIB。
好吧,我已经有Python代码来计算环境变量了,所以解决方法就是。
def get_build_env(arch):
vs_comntool_path = os.environ["VS140COMNTOOLS"]
bat_file = find_bat_file(vs_comntool_path)
env = source_bat(bat_file, env)
inc_path = r"C:\Program Files (x86)\Windows Kits\10\Include\10.0.10069.0\ucrt\include"
if arch == "amd64_x86":
subfolder = "x86"
else:
subfolder = "x64"
lib_path = r"C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10069.0\ucrt\%s" % subfolder
env["INCLUDE"] += inc_path + ";"
env["LIB"] += lib_path + ";"
请注意这里用x64代替了amd64:)
结束语
好了,这就是我今天要讲的全部内容。
如果你有Python中的构建脚本,用CMake和Ninja构建Visual Studio项目效果相当好,前提是你愿意运行.bat脚本,并仔细应用环境变量的变化。
很快我就会尝试看看Visual Studio 2017的情况如何,也许事情会变得更容易,谁知道呢?
在那之前,愿Build与你同在!
更新:我已经决定将这篇文章交叉发布在dev.to上。让我们拭目以待吧!
-
David,如果你看到这篇文章,非常感谢你! ︎
-
更多信息请参见CMake文档︎
-
构建完成后,只是一行输出,很快就消失了,你懂吗?︎
-
原来我在节点上设置Visual Studio时,不知为何错过了这个页面,告诉我signtool.exe已经安装好了......︎。
谢谢你读到这里 :)
我很想听听你的意见,所以请在下面留言,或者阅读联系页面,了解更多与我联系的方式。
请注意,要想在新文章发布时收到通知,你可以
订阅RSS提要或时事通讯 或者在Mastodon、dev.to或twitter上关注我。
干杯!
通过www.DeepL.com/Translator (免费版)翻译