过去几年大家一直都在使用 .NET Core(有这么久吗?)并且都知道“生成系统”经历了重大改变,不论是终止对 Gulp 的内置支持,还是放弃 Project.json。对于我这个专栏作家来说,这些变化一直很棘手,因为我不想我亲爱的读者花了很多时间来了解这些功能和详情,最后却只能使用短短几个月。
这就是为什么我所有的 .NET Core 相关文章都以基于 Visual Studio .NET 4.6 的 *.CSPROJ 文件为基础撰写,这些文件引用 .NET Core 中的 NuGet 包,而非实际编译的 .NET Core 项目。
本月,我很荣幸向大家宣布 .NET Core 项目的项目文件已经确定为(你会相信吗) MSBuild 文件。但这是与前几代 Visual Studio 不同的 MSBuild 文件,是经过改进和简化的 MSBuild 文件。
这是一个包括 Project.json 所有功能(而不陷入波形括号与尖括号的宗教战争),但是配备自 Visual Studio 2005 以来我们知晓(并且可能喜爱?)的传统 MSBuild 文件的工具支持的文件。
总之,这些功能包括开源、跨平台兼容性、简化的人类可编辑的格式以及完全现代的 .NET 工具支持(包括通配符文件引用)。
工具支持
要清楚,MSBuild 始终支持通配符等功能,但是现在 Visual Studio 工具也同样支持。换句话说,MSBuild 最大的亮点在于它被紧密集成为所有新 .NET 工具—DotNet.exe、Visual Studio 2017、Visual Studio Code 和 Visual Studio for Mac—的生成系统基础并且支持 .NET Core 1.0 和 .NET Core 1.1 运行时。
.NET 工具和 MSBuild 之间强大耦合的巨大优势在于你创建的任何 MSBuild 文件与所有 .NET 工具兼容,并且可从任何系统生成。
用于 MSBuild 集成的 .NET 工具通过 MSBuild API 而非命令行过程耦合。例如,执行 .NET CLI command Dotnet.exe Build 不会在内部生成 msbuild.exe 过程。但是,这不会调用过程中的 MSBuild API 来执行工作(MSBuild.dll 和 Microsoft.Build.* 程序集)。即便如此,不论工具如何,输出在各平台之间均相似,因为存在共享的日志记录框架,所有 .NET 工具均在其中注册。
*.CSPROJ/MSBuild 文件结构
正如我所提到的,文件格式本身被简化为极简。它支持通配符、项目和 NuGet 包引用以及多个框架。此外,在 Visual Studio 过去创建的项目文件中发现的项目类型 GUID 已经消失。
图 1 显示 *.CSPROJ/MSBuild 文件示例。
图 1 CSProj/MSBuild 文件基本示例
netcoreapp1.0 1.0.1 1.0.0-* All
让我们来详细地回顾下结构和功能:
简化了的标头: 首先要注意的是根元素仅仅是一个项目元素。甚至对命名空间和版本属性的需要也已经消失:
ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
(尽管它们是通过候选发布版本工具创建的。) 同样地,甚至导入常见属性的需要也是可选的:
项目引用: 从项目文件可以将条目添加到项目组元素:
-
NuGet 包:
1.1.0
-
项目引用:
-
程序集引用:
...
直接的程序集引用应该属于例外情况,因为通常优先采用 NuGet 引用。
通配符包括: 可以通过通配符将编译的代码文件和资源文件均包括在内。
但是,你仍可使用删除属性选择要忽略的特定文件。(注意对通配符的支持很多时候被称为通配。)
多重目标: 要找出你所针对的平台,可以使用属性组、TargetFramework 元素以及输出类型(可选):
netcoreapp1.0 netstandard1.3
鉴于这些条目,每个目标的输出将被生成到 bin\Debug or bin\Release 目录(取决于你指定的配置)。如果目标不止一个,则生成执行会将输出置于与目标框架名称对应的文件夹中。
无项目类型 GUID: 注意不再需要包括识别项目类型的项目类型 GUID。
Visual Studio 2017 集成
至于 Visual Studio 2017,Microsoft 继续提供用于编辑 CSPROJ/MSBuild 项目文件的富 UI。
例如,图 2 显示了使用 CSPROJ 文件列表加载的 Visual Studio 2017,它在图 1 的基础上稍做修改,包括 netcoreapp1.0 和 net45 的目标框架元素、Microsoft.Extensions.Configuration、Microsoft.NETCore.App 和 Microsoft.NET.Sdk 的包引用以及对 MSBuild 的程序集引用和对 SampleLib 的项目引用。
图 2 解决方案资源管理器是基于 CSProj 文件的富 UI
注意在解决方案资源管理器中的依赖项树中,每个依赖项类型—程序集、NuGet 包或项目引用—如何拥有相应的组节点。
此外,Visual Studio 2017 支持项目和解决方案的动态重新加载。例如,如果新文件被加载到项目目录—与其中一个通配符相匹配的目录—Visual Studio 2017 会自动检测更改并在解决方案资源管理器中显示文件。
同样地,如果你从 Visual Studio 项目排除一个文件(通过 Visual Studio 菜单选项或 Visual Studio 属性窗口),Visual Studio 将相应地自动更新项目文件。(例如,它将添加一个
此外,将自动检测对项目文件的编辑并将其重新加载到 Visual Studio 2017。事实上,解决方案资源管理器中的 Visual Studio 项目节点现在支持内置编辑
还支持在 Visual Studio 2017 中进行内置迁移,将项目转换为新的 MSBuild 格式。如果你接受其提示,你的项目将自动从 Project.json/*.XPROJ 类型升级到 MSBUILD/*.CSPROJ 类型。
注意此类升级将破坏与 Visual Studio 2015 .NET Core 的向后兼容性,所以在他人使用 Visual Studio 2015 的同时,不能让团队成员在 Visual Studio 2017 中处理同一个 .NET Core 项目。
MSBuild
有一点需要提醒你注意的是,2016 年 3 月,Microsoft 在 GitHub 上将 MSBuild 发布为开源 (github.com/Microsoft/msbuild) 并将其贡献给了 .NET Foundation (dotnetfoundation.org)。
对于到 Mac 和 Linux 的平台可移植性,将 MSBuild 设为开源有利于回到正轨,最终允许其成为所有 .NET 工具的基础生成引擎。
与之前发现的 CSPROJ\MSBuild 文件 PackageReference 元素不同,MSBuild 版本 15 除了开源和跨平台未引入太多其他功能。
实际上,通过比较命令行帮助,结果显示选项相同。对于那些尚不熟悉它的人来说,下面列出了你应熟悉的 syntax MSBuild.exe [options] [project file] 的最常用选项:
/target:
/property:
/maxcpucount[:n]: 指定要使用的 CPU 的数量。默认情况下,msbuild 在单个 CPU 上运行(单线程)。如果可以同步的话,则可以通过指定并发级别来增加数量。如果在不提供值的情况下指定 /maxcpucount 选项,msbuild 将使用计算机上的处理器数量。
/preprocess[:file]: 通过初始化所有包含的目标来生成聚合的项目文件。这样在出现问题时有利于调试。
@file: 提供一个(或多个)包含选项的响应文件。此类文件在单独的行上拥有命令行选项(注释要添加“#”前缀)。
默认情况下,MSBuild 将从生成的第一个项目或解决方案导入名为 msbuild.rsp 的文件。响应文件有助于找出不同的生成属性和目标,具体取决于生成的环境(研发、测试、生产)等。
Dotnet.exe
大约一年前引入了 .NET 的 dotnet.exe 命令行并将其作为生成、构建和运行基于 .NET Core 的项目的跨平台机制。正如我所提到的,dotnet.exe 已经得到更新,现在很大程度上依赖于MSBuild 作为内部引擎来在可行的情况下处理大量工作。
以下是对各种命令的概述:
dotnet new: 创建你的初始项目。此项目生成器开箱即用,支持 Console、Web、Lib、MSTest 和 XUnitTest 项目类型。但是将来有望提供自定义模板,从而能够生成你自己的项目类型。(这一功能可用时,新的命令将不再依赖于 MSBuild 生成项目。)
dotnet restore: 读取在项目文件中指定的项目依赖项并下载任何缺失的 NuGet 包和其中发现的工具。项目文件本身可以被指定为参数或从当前目录暗示(如果当前目录中的项目文件不止一个,则要求指定要使用哪个)。注意因为恢复利用 MSBuild 引擎处理工作,所以 dotnet 命令允许其他 MSBuild 命令行选项。
dotnet build: 在项目文件中调用 MSBuild 引擎执行生成目标(默认情况下)。如同恢复命令一样,你可以将 MSBuild 参数传递到 dotnet 生成命令。例如,命令 dotnet build /property:configuration=Release 将触发要输出的 Release 生成,而不是 Debug 生成(默认情况下)。
同样地,你可以使用 /target(或 /t)指定 MSBuild 目标。例如,dotnet build /t:compile 命令将运行编译目标。
dotnet clean: 删除所有生成输出,以便执行完全生成而非增量生成。
dotnet migrate: 将基于 Project.json/*.XPROJ 的项目升级为 *.CSPROJ/MSBuild 格式。
dotnet publish: 连同任何依赖项将所有生成输出组合到单个文件夹,从而将其暂存以部署到另一台计算机。
这对不仅包含编译输出和依赖包,而且包含 .NET Core 运行时本身的自包含部署来说尤其有用。自包含应用程序没有设定特定版本的 .NET 平台已经安装到了目标计算机上的任何前提条件。
dotnet run: 启动 .NET 运行时并托管项目和/或编译的程序集以执行你的程序。注意对于 ASP.NET 来说,编译并不如可以托管项目本身一样必要。
在执行 msbuild.exe 和 dotnet.exe 之间存在重大重叠,需要你选择运行哪个。如果你生成的是默认 msbuild 目标,则仅可从项目目录内执行命令“msbuild.exe”,它将为你编译并输出目标。等同的 dotnet.exe 命令为“otnet.exe msbuild”。
另一方面,如果你运行的是“清理”目标,则命令是“msbuild.exe /t:clean”(对于 MSBuild)和“dotnet.exe clean”(对于 dotnet)。此外,两个工具均支持扩展名。MSBuild 拥有在项目文件本身内和通过 .NET 程序集的综合性扩展性框架(请参阅bit.ly/2flUBza)。
同样地,可以扩展 dotnet,但对此的建议基本都涉及扩展 MSBuild 和一些规定的步骤。
虽然我喜欢 dotnet.exe 这个想法,但是最后它似乎未提供太多的 MSBuild 优势,MSBuild 不支持的除外(其中 dotnet new 和 dotnet run 可能是最重要的)。
最后,我认为通过 MSBuild,可以轻松地执行简单的操作,同时在需要时仍可执行复杂的操作。此外,通过提供合理的默认值,即使是 MSBuild 中复杂的操作也可以被简化。
最后,是 dotnet 还是 MSBuild 更可取由个人喜好决定,而时间将会告诉我们通常会为 CLI 前端选用哪个开发社区。
global.json:
虽然 Project.json 功能已迁移到 CSPROJ,但仍完全支持 global.json。文件允许指定项目目录和包目录并识别要使用的 SDK 版本。下面是 global.json 文件的示例:
{ "projects": [ "src", "test" ], "packages": "packages", "sdk": { "version": "1.0.0-preview3", "runtime": "clr", "architecture": "x64" } }
有三个部分与 global.json 文件的主要目的一致:
-
projects: 识别 .NET 项目所在的根目录。项目节点对于调试 .NET Core 源代码来说极为关键。在克隆源代码后,你可以将目录添加到项目节点,然后 Visual Studio 将在解决方案中自动将其作为项目上载。
-
packages: 指示 NuGet 包文件夹的位置。
-
sdk: 指定要使用的运行时的版本。
总结
在本文中,我宽泛地概述了在 .NET 工具套件中使用 MSBuild 的所有情形。结束之前,请允许我基于我使用成千上万行 MSBuild 处理项目的经验,为大家提供些许建议。别落入使用 XML MSBuild 架构等松散类型声明性语言编写脚本的圈套。这不是它的用途。
项目文件应该是相对较薄的包装器,能够识别生成目标之间的顺序和依赖项。如果你的 MSBuild 项目文件太大,那么维护起来可能会比较痛苦。请勿耽搁太久,立即将其重构为可调试和可轻松通过装置测试的 C# MSBuild 任务。