使用MSBuild编写构建脚本

  1. 1. 背景
  2. 2. Roslyn与MSBuild
    1. 2.1. 找到MSBuild
    2. 2.2. MSBuild的命令行选项
  3. 3. 踩坑
    1. 3.1. 部分参数无效
    2. 3.2. C++工程指定输出目录无效
    3. 3.3. C++工程DefineConstants无效
  4. 4. 对PowerShell的吐槽

背景

项目需求变更,需要从一份代码里编译出好几个不同的版本。编译和部署的复杂度都成指数增加,简单的Release构建搞不定了,写构建脚本迫在眉睫。

大致介绍下项目的组成吧。整个项目由三部分组成:

  • Installer.vcxproj
  • Main.csproj
  • Updater.csproj

Main是项目本体。Installer负责项目的安装和卸载,Updater负责项目的更新。解决方案里除Installer使为C++外,其余均为C#。

解决方案的结构如下:

1
2
3
4
MyProject.sln
- Installer.vcxproj
- Main.csproj
- Updater.csproj

现在需要从MyProject.sln条件编译出多个版本,要求

  • 对于每个版本,InstallerMain须重新编译
  • 所有版本共享同一份Updater的二进制。

Roslyn与MSBuild

构建的过程自然离不开编译。编译器虽软能将代码转化为二进制,但这个转换的单位是文件:

1
2
gcc --help
Usage: gcc [options] file...

作为.NET的编译器,Roslyn也不例外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
csc /?
.........
- INPUT FILES -
/recurse:<wildcard> Include all files in the current directory and
subdirectories according to the wildcard
specifications
/reference:<alias>=<file> Reference metadata from the specified assembly
file using the given alias (Short form: /r)
/reference:<file list> Reference metadata from the specified assembly
files (Short form: /r)
/addmodule:<file list> Link the specified modules into this assembly
/link:<file list> Embed metadata from the specified interop
assembly files (Short form: /l)
/analyzer:<file list> Run the analyzers from this assembly
(Short form: /a)
/additionalfile:<file list> Additional files that don't directly affect code
generation but may be used by analyzers for producing
errors or warnings.
/embed Embed all source files in the PDB.
/embed:<file list> Embed specific files in the PDB

我们不会把所有源文件的路径、所有引用的程序集的都告诉编译器,这样既低效又不利于维护。
最好有个能有个程序,能把所有编译的参数都记下来。这个过程最好能自动化:在文件夹里添加一个源文件、新增一个引用项,它都自动的更新编译参数。
存在这样的工具吗?当然了。在Visual Studio里添加一个文件是不需要动编译选项的。有心的人会发现,Visual Studio在.csproj记录了新增的文件。.csproj是项目文件。一个叫MSBuild的工具可以解析.csproj,并生成对应的编译选项调用编译器。这样的工具称为构建自动化工具。用微软自己的话说,是构建引擎。大型软件的开发离不开构建引擎。
与MSBuild同类的工具还有有很多常见的有make,maven等。

相比于Roslyn,MSBuild的使用就简单多了,只需要指定项目文件就万事大吉了。
下面是在一个新建WPF项目下调用MSbuild的输出。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
C:\Users\Von\source\repos\WpfApp1>msbuild WpfApp1.sln
Microsoft (R) Build Engine version 15.7.180.61344 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.
Building the projects in this solution one at a time. To enable parallel build, please add the "/m" switch.
Build started 9/1/2018 2:08:01 PM.
Project "C:\Users\Von\source\repos\WpfApp1\WpfApp1.sln" on node 1 (default targets).
ValidateSolutionConfiguration:
Building solution configuration "Debug|Any CPU".
Project "C:\Users\Von\source\repos\WpfApp1\WpfApp1.sln" (1) is building "C:\Users\Von\source\repos\WpfApp1\WpfApp1\WpfA
pp1.csproj" (2) on node 1 (default targets).
GenerateBindingRedirects:
No suggested binding redirects from ResolveAssemblyReferences.
Project "C:\Users\Von\source\repos\WpfApp1\WpfApp1\WpfApp1.csproj" (2) is building "C:\Users\Von\source\repos\WpfApp1\W
pfApp1\WpfApp1_r1lsxnqk_wpftmp.csproj" (3) on node 1 (_CompileTemporaryAssembly target(s)).
CoreCompile:
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\Roslyn\csc.exe /noconfig /nowarn:1701,
1702 /nostdlib+ /platform:anycpu32bitpreferred /errorreport:prompt /warn:4 /define:DEBUG;TRACE /highentropyva+ /refer
ence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\Microsoft.CSharp.dll" /ref
erence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\mscorlib.dll" /reference
:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\PresentationCore.dll" /referen
ce:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\PresentationFramework.dll" /
reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Core.dll" /ref
erence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Data.DataSetExten
sions.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Da
ta.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.dll"
/reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Net.Http.dll"
/reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Xaml.dll" /r
eference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Xml.dll" /refer
ence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Xml.Linq.dll" /refe
rence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\WindowsBase.dll" /debug+
/debug:full /filealign:512 /optimize- /out:obj\Debug\WpfApp1.exe /ruleset:"C:\Program Files (x86)\Microsoft Visual St
udio\2017\Community\Team Tools\Static Analysis Tools\\Rule Sets\MinimumRecommendedRules.ruleset" /subsystemversion:6.
00 /target:winexe /utf8output App.xaml.cs MainWindow.xaml.cs Properties\AssemblyInfo.cs Properties\Resources.Designer
.cs Properties\Settings.Designer.cs C:\Users\Von\source\repos\WpfApp1\WpfApp1\obj\Debug\MainWindow.g.cs C:\Users\Von\
source\repos\WpfApp1\WpfApp1\obj\Debug\App.g.cs
Using shared compilation with compiler from directory: C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\
MSBuild\15.0\bin\Roslyn
Done Building Project "C:\Users\Von\source\repos\WpfApp1\WpfApp1\WpfApp1_r1lsxnqk_wpftmp.csproj" (_CompileTemporaryAsse
mbly target(s)).
MarkupCompilePass2:
MarkupCompilePass2 successfully generated BAML or source code files.
CleanupTemporaryTargetAssembly:
Deleting file "obj\Debug\WpfApp1.exe".
CoreResGen:
"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\resgen.exe" /useSourcePath /r:"C:\Program
Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\Microsoft.CSharp.dll" /r:"C:\Program Files
(x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\mscorlib.dll" /r:"C:\Program Files (x86)\Referen
ce Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\PresentationCore.dll" /r:"C:\Program Files (x86)\Reference Ass
emblies\Microsoft\Framework\.NETFramework\v4.7.1\PresentationFramework.dll" /r:"C:\Program Files (x86)\Reference Asse
mblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Core.dll" /r:"C:\Program Files (x86)\Reference Assemblies\Micr
osoft\Framework\.NETFramework\v4.7.1\System.Data.DataSetExtensions.dll" /r:"C:\Program Files (x86)\Reference Assembli
es\Microsoft\Framework\.NETFramework\v4.7.1\System.Data.dll" /r:"C:\Program Files (x86)\Reference Assemblies\Microsof
t\Framework\.NETFramework\v4.7.1\System.dll" /r:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NET
Framework\v4.7.1\System.Net.Http.dll" /r:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramewo
rk\v4.7.1\System.Xaml.dll" /r:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\S
ystem.Xml.dll" /r:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Xml.Li
nq.dll" /r:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\WindowsBase.dll" /co
mpile Properties\Resources.resx,obj\Debug\WpfApp1.Properties.Resources.resources
Processing resource file "Properties\Resources.resx" into "obj\Debug\WpfApp1.Properties.Resources.resources".
CoreCompile:
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\Roslyn\csc.exe /noconfig /nowarn:1701,
1702 /nostdlib+ /platform:anycpu32bitpreferred /errorreport:prompt /warn:4 /define:DEBUG;TRACE /highentropyva+ /refer
ence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\Microsoft.CSharp.dll" /ref
erence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\mscorlib.dll" /reference
:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\PresentationCore.dll" /referen
ce:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\PresentationFramework.dll" /
reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Core.dll" /ref
erence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Data.DataSetExten
sions.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Da
ta.dll" /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.dll"
/reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Net.Http.dll"
/reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Xaml.dll" /r
eference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Xml.dll" /refer
ence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\System.Xml.Linq.dll" /refe
rence:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.1\WindowsBase.dll" /debug+
/debug:full /filealign:512 /optimize- /out:obj\Debug\WpfApp1.exe /ruleset:"C:\Program Files (x86)\Microsoft Visual St
udio\2017\Community\Team Tools\Static Analysis Tools\\Rule Sets\MinimumRecommendedRules.ruleset" /subsystemversion:6.
00 /resource:obj\Debug\WpfApp1.g.resources /resource:obj\Debug\WpfApp1.Properties.Resources.resources /target:winexe
/utf8output App.xaml.cs MainWindow.xaml.cs Properties\AssemblyInfo.cs Properties\Resources.Designer.cs Properties\Set
tings.Designer.cs C:\Users\Von\source\repos\WpfApp1\WpfApp1\obj\Debug\MainWindow.g.cs C:\Users\Von\source\repos\WpfAp
p1\WpfApp1\obj\Debug\App.g.cs "C:\Users\Von\AppData\Local\Temp\.NETFramework,Version=v4.7.1.AssemblyAttributes.cs"
Using shared compilation with compiler from directory: C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\
MSBuild\15.0\bin\Roslyn
_CopyAppConfigFile:
Copying file from "App.config" to "bin\Debug\WpfApp1.exe.config".
CopyFilesToOutputDirectory:
Copying file from "obj\Debug\WpfApp1.exe" to "bin\Debug\WpfApp1.exe".
WpfApp1 -> C:\Users\Von\source\repos\WpfApp1\WpfApp1\bin\Debug\WpfApp1.exe
Copying file from "obj\Debug\WpfApp1.pdb" to "bin\Debug\WpfApp1.pdb".
Done Building Project "C:\Users\Von\source\repos\WpfApp1\WpfApp1\WpfApp1.csproj" (default targets).
Done Building Project "C:\Users\Von\source\repos\WpfApp1\WpfApp1.sln" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.60

看看CoreCompile里传给Roslyn的参数。自己调用Roslyn的话光给出正确的参数就让人头痛了。所以还是直接调用MSBuild好了。
单单调用一次MSBuild无法完成我们的需求,因此构建脚本是少不了的了。

找到MSBuild

首先在`Developer command prompt for VS 2017找到MSbuiid的完全路径:

1
2
3
$ where msbuild
> C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe
> C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe

这里需要的是Visual Studio目录下的那一个。

MSBuild的命令行选项

  • /t:Rebuild
    强制重新编译。

  • /p:Configuration=RELEASE
    以Release配置编译。会开启优化。

  • /p:DefineConstants="Value1,Value2,..."
    定义条件编译常量。

  • /p:Platform=
    指定目标架构。x86,x64和AnyCpu三选一。

  • /p:OutputPath
    输出目录。可接受绝对和相对路径。

各个参数中间用空格分开。

一个例子

1
msbuild WpfApp1.csproj /t:Rebuild /p:Configuration=RELEASE /p:DefineConstants="TRACE,DEMO" /p:Platform="x86" /p:OutputPath="F:\Release" /p:TargetFrameworkVersion=4.7.1 /tv:15.0

更多参数请参阅这里


能配好参数调用MSBuild是构建脚本中最重要的一步。至于需求里的其他要求没什么难度,无非用合适的控制流去调用MSBuild。这里就略过了。

踩坑

部分参数无效

如果你发现你设置的部分参数无效,那可能是被因为项目文件的参数覆盖掉了。
为了少掉坑,请将项目文件的路径放作为给MSBuild的第一个参数。

C++工程指定输出目录无效

配好/p:Platform后MSbuild会提示你用/p:OutputPath指定输出路径。
如果你用了OutputPath,那么恭喜你掉进了微软挖的暗坑:OutputPath对C++项目无效。
解决方法:请使用使用/p:OutputDir指定输出目录
原因:

To expand on what @AndyGerlicher said, we can’t do what you’re asking because we too have lost the reasoning behind this decision. The current team thinks it looks pretty broken.

From comments in the targets

OutDir and OutputPath are distinguished for legacy reasons, and OutDir should be used if at all possible.

It seems like we got stuck in the middle of a transition. However, as you’ve discovered, there’s a ton of MSBuild code out there that exploits the differences between the two variables. That keeps us from completing the transition (or for that matter backing it out), because it would cause a lot of churn in customer projects.

更多详情请参阅这里

C++工程DefineConstants无效

喜闻乐见的legacy reasons

比较简单的一个workaround

.vcxprojLabel为Global的PropertyGroup中加上
<DefineConstants></DefineConstants>

在所有的PreprocessorDefinitions内容的开头添加$(DefineConstants);

最终结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="14.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
.....
<PropertyGroup Label="Global">
<DefineConstants></DefineConstants>
</PropertyGroup>
......
<ItemDefinitionGroup>
<ClCompile>
....
<PreprocessorDefinitions>$(DefineConstants);NDEBUG;WIN32;_WINDOWS;NO_EXP10;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions);</PreprocessorDefinitions>
....
</ClCompile>
</ItemDefinitionGroup>
</Project>

更多详情请参阅这里

对PowerShell的吐槽

本以为基于.NET对象的PowerShell挺好上手的。没想到由JSON反序列化出来的对象是PSObject类型,原来类里的方法都用不了了,想把PSObject转换回去也十分麻烦。可能Shell天生跟OOP合不来吧。
最后用C#写了构建脚本。C#,脚本,是不是哪里不太对啊🤔