[CMake翻译]CMake、Visual Studio和命令行

3,117 阅读15分钟

原文地址:dmerej.info/blog/post/c…

原文作者: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.exelink.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

我找了一个解决方案,但得到的都是人们告诉我用手设置INCLUDELIB

好吧,我已经有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上。让我们拭目以待吧!


  1. David,如果你看到这篇文章,非常感谢你! ︎

  2. 更多信息请参见CMake文档︎

  3. 构建完成后,只是一行输出,很快就消失了,你懂吗?︎

  4. 原来我在节点上设置Visual Studio时,不知为何错过了这个页面,告诉我signtool.exe已经安装好了......︎。


谢谢你读到这里 :)

我很想听听你的意见,所以请在下面留言,或者阅读联系页面,了解更多与我联系的方式。

请注意,要想在新文章发布时收到通知,你可以

订阅RSS提要时事通讯 或者在Mastodondev.totwitter上关注我。

干杯!


通过www.DeepL.com/Translator (免费版)翻译