移植MSbuild到.NET core

[原文发表地址]:Porting MSBuild to .NET Core

[原文发表时间]:February 23, 2016

这篇文章是由.NET团队的软件工程师Daniel Plaisted所创作。

对.NET来说,.NET核心的到来是个激动人心的时刻,我们正朝着一个开源跨平台的世界全速前进。.NET核心为app提供了一个现代化,app本地化并且一直开源的框架。但是,相对与.NET框架,.NET核心有一些变化,这意味着如果你想要把基于现有的.NET框架代码移植到.NET核心框架,需要做一些努力。

在我们最近移植到.NET核心的文章中,我们讨论了何种类型的代码移植到.NET Core比较有意义,并且对将代码移植到.NET 核心提供了一些建议,方法和工具。在这篇博客中,我将会分享一些将MSBuild移植到.NET 核心的经验。如果你正将代码移植到.NET 核心,它会有助于提高那些已经成熟的重点的移植经验。

MSBuild是.NET和Visual Studio的构造引擎。常用于建立许多开源的.NET项目,包括corefx(.NET 核心类库),coreclr((.NET 核心的运行环境)和Roslyn(.NET 的编译平台)。作为一个开源跨平台的项目,我们希望用户可以修改,构建并为这些项目贡献,而不需要他们用窗口去做。为了支持这些,我们在去年的三月开源MSBuild,并且在九月宣布,我们正致力于在.NET核心的顶端将它跨平台移植运行。现在我们已经基本完成了将MSBuild移植到.NET核心的工作并且正在研究将它集成到开源库的基础架构。

战略部署

在2015的五月,MSBuild最初是在GitHub上被开源的。最初他被运行Mono运行环境下的一个独立的linux分支上。这个分支是我们.NET核心端口的出发点。这一计划最终将这些变化合并到主分支,并且对拥有相同的源代码的.NET核心和.NET框架的MSBuild 进行编译。

MSBuild的.NET核心版本的目标是支持在Windows,Linux,和Mac OS构建.NET核心项目,不依赖于完整的.NET框架或是Mono。兼容全部MSBuild的.NET框架版本或者构建桌面.NET框架项目都不是我们的目标,因为构建这样的一个项目要依赖于那些.NET核心上不可用的一些功能(例如,全局程序集缓存)。

因为MSBuild的.NET核心版本不能完全与.NET 框架版本相兼容,但是我们最终想要合并代码库。在.NET核心版本与桌面版本不同的地方,我们使用条件编译。我们使用细粒度特征标识,例如用FEATURE_APPDOMAIN支持AppDomain或者是FEATURE_BINARY_SERIALIZATION.

[fine-grained feature flags 细粒度特征标识]

比起我们使用如NETCORE一样的一些逐行编译的汇编语言,这有助于帮助我们更清楚地了解为什么这一部分的代码对.NET 核心是不可以的。它也意味着如果一些特性被添加到`` .NET ``核心``,``这将会很容易返回和获取相应的`` MSBuild ``的`` .NET ``核心代码。

移植跟踪进度

我们使用ApiPort工具去找到那些在.NET核心不支持的正在使用的API MSBuild,去帮助我们获得我们将要移植到.NET核心的内容的一个最初的想法,紧接着,我们使用ApiPort追踪我们的移植进度需要多少工作量。

ApiPort分析编译管理程序集。所以你需要成功地编译代码从而去分析ApiPort.。因此,从定义上来说,只有完成移植,在.NET 框架上编译并运行ApiPort的代码,我们才能够将.NET 核心编译成功。我们不想改变正常.NET框架上的构建行为。因此,为了成功编译结果去分析,我们创建了一个适用于.NET框架但是同样适用于与.NET核心中相同的一些特性配置。

然而,我们发现我们无法使用ApiPort追踪我们距离成功编译这个项目还有多近,因为一旦这个项目被成功移植到.NET 核心时,ApiPort通常仍会报告它的可移植性仍然低于100%。这个主要原因是,在一些案例中,我们会将APIs从.NET框架复制到MSBuild代码库。即使我们自己通过添加实现到我们的项目里,使他可用,但是当我们用ApiPort分析可移植性的时候,这些APIs还是被报告成不可用。另一个原因就是ApiPort经常检测到APIs对一些反射并不支持(详情见GitHub上的bug)。下表是对MSBuild项目,以最小变化支持.NET 核心(初始分数)的可移植性分数报告和该项目移植到.NET 核心成功编译之后的分数(总分)。

MSBuild ApiPort Comparison

有两个关于ApiPort的小问题:不分析调用原始APIs(通过PInvoke)并且它报道了基于不同数量上的APIs的可移植性得分,而不是基于API使用数量的实例。因此在项目中删除一个仅仅使用一次且不被支持的API将会提高可移植性的得分,同时,删除许多不被支持的API接口直到删除最后一个API也不会影响分数。

虽然在本例中的ApiPort可移植性分数报告可能会被误解,但我认为让它“更精确”并不是必要的。没有工具能够神奇的告诉你移植一些代码的时候将会有多大的工作量或者是它会花费多长时间。ApiPort像所有的度量一样,它仅仅是报告的度量,如果你理解它到底意味这什么,它将会非常有用。

构建.NET 核心

移植到.NET 核心的第一部分将建立在构建系统上使我们可以对.NET 核心进行代码编译。同时,唯一的不确定性方法是在.NET 核心上构建及运行的应用程序时DNX.DNX项目系统并不像MSbuild那样,支持大量的可扩展性及灵活性,这是为什么我们首先将MSBuild移植到的.NET 核心的原因之一。所以,DNX并不适合我们

埃里克.圣约翰对.NET核心的控制台应用程序为实例项目概念提供了证明。用这个作为指导,我们可以为MSBuild项目构建配置去对.NET核心编译。

如今为.NET核心为目标的一些工具还在使用中,但是如果你想创建一个面向.NET核心的 MSBuild项目,你可以根据这个指南中的步骤进行操作:开始编写.NET核心应用及类库

将代码移植到.NET核心

将代码移植到.NET 核心主要是指移除对.NET核心不可用的APIs用法。我主要在Visual Studio中使用错误列表寻找需要被更新的代码,来消除APIs的使用。通常都会在.NET 核心上提供备用的API。反射,文件的输入/输出,文化以及全球化API在这些分类中是最为常见的。

其它的APIs对.NET核心上的MSBuild并不适用,且其的使用很容易被删除全局程序集缓存 中的XAML集成或者解析组件就是这样的例子。在一些关于API在.NET 核心不可用的情况下,我们从.NET框架复制API的实现,将其添加到MSBuild的代码库并将它移植到MSBuild编译。

我们这样做是为了XmlTextWriter及其依赖性,Environment.GetSpecialFolder和Type.InvokeMember我们计划跟进.NET 框架小组,与其讨论这些APIs是否在其功能缺陷上具有普遍适用性,并将其增加到.NET 核心。

最后,有可能通过重新实现这个功能或者一起去移除API的使用。大规模重新实现改变的行为是有一定风险的,我最初将其重新实现为 Type.InvokeMember的功能性,因为这似乎在当时是最简单的解决方案。尽管如此,我写的代码并不符合预期的行为,所以我结束了用替换它的两倍.NET框架代码.

目前,我们删除的MSBuild用于.NET核心多进程的构建支持。当前实现的.NET框架的使用二进制序列,这是不支持.NET核心进程之间的通信。为了支持此功能在.NET中的核心进程之间的通信。为了支持此功能在.NET中的核心,我们需要用不同的序列化机制进行沟通,重新实现它。(请注意:我们不打算将二进制序列化添加到.NET 的核心,它是不是在不同的运行环境或操作系统有弹性。)

移植的核心MSBuild引擎到.NET核心可以在下拉请求中看出所做的更改:#152,#156, #158, 和 #159

想要用跟我们相同的方法将.NET框架的代码复制到.NET核心项目的开发者最终可能会因为违反许可而终止。浏览.NET框架的源代码最简单的方法是referencesource.microsoft.com 网站。但是,这个网站的代码只许可“使用参考”,这是非常有限的,不会允许代码被复制到一个项目。几乎所有的代码在https://github.com/microsoft/referencesource 上,或者corefx 或者coreclr回购的MIT许可证下是有用的。开发人员很可能先找到他们所需要的参考源网站的代码,就需要小心找到等价代码(如果可用)在MIT授权回购之一,并从那里下载遵守许可证。

帮助移植的工具

有几个工具有助于将代码移植到.NET 核心。ApiPort可以扫描.NET 程序集并且生成不支持.NET核心

且随着建议项进行更换的带有APIs列表的电子表格。在我检查移植的过程中,我发现使用API 目录非常方便,它是一个网络团队的内部工具而非参考表格去查找APIs。这是因为它展示了一个API的附属合同(契约)并且它具有良好的API搜索功能。在大多数移植完成之后,我发现有一个可以公开访问的网站dotnetstatus site,其提供了类似的功能。

当进行移植的时候,这些工具的主要功能是快速查询其是否支持一个端口,如果没有,需要建议更换项是什么。如果API支持,那么工具应该显示API所在的什么合同去帮助你可以了解你需要参考什么样的NuGet包。这些工具目前只有API 目录(不公开的)展示了API 所在的合同。有一个反转搜索 网站是用来查找API 所在NuGet包。

我会在dotnetstatus 网站 介绍一些改善措施 , 在其移植到.NET 核心时,使其成为查找APIs的主要方式。

· 搜索改进(#19

· 返回匹配的搜索结果(例如,搜索“GetCulture”应该返回包含“GetCultureInfo”方法的结果)。

· 在搜索结果中返回除成员类型之外的类型。

· 在单独的页面显示搜索结果(当前的搜索结果仅仅显示在搜索框内自动完成的项目)。

· 显示包含API的协议(#20

有一个简单的方法去寻找被推荐的替换和关于为什么API在.NET核心中不可用将会帮助减少APIs被从.NET核心中任意移除的理解,以及移植代码到.NET核心是很困难的

并不是所有的APIs都有建议替换。重要的是有一个无摩擦的过程,关于对一个API推荐替换的信息的添加,以及为什么API不被支持的信息。本着这种精神,现在推荐替代的是hosted on GitHub 并且任何人都可以发送一个更新请求去添加新的建议或者是更新已存在的。

当代码被编辑时,在Visual Studio中有一个更好的方法可以直接显示这个API的信息。可能是在错误列表中将其编译错误信息显示出来,或者作为代码中的“灯泡”的地方。它可能提供重构工具来帮助你对一些简单且常见的代码进行修改。有一件事需要知道:你需要添加一个GetTypeInfo()方法去调用很多反射API你需要用CultureInfo.CurrentCulture替换Thread.CurrentCulture``。即便如此,做上数百次这样的机械化改造之后,你开始希望有一个工具可以帮助你去做这些事情。

测试

MSBuild的测试最初是用MSTest写的。由于原来的端口为Mono和linux的一部分,他们被转换为NUnit,因为MSTest对于跨平台是不可用的。不过,在那时xUnit是唯一支持.NET核心的测试框架。

我们决定主代码库的xUnit测试先转换,然后将这些变化合并到跨平台的代码。这应该更容易保持代码库同步,不久,他们将会被合并在一起。我们使用的单位转换工具,来帮助其进行合并。它会将测试属性更改为事件属性并更新其命名空间,并将许多Assert的调用转换为单元测试的APIs.

然后,我们将这些变化合并到跨平台的代码库。由于在跨平台代码库的测试已经被转换为NUnit有许多的合并冲突。Rainer写了一个工具去自动解决这些矛盾。

一旦这个测试被转换为xUnit和.NET核心,就会有很多故障。这些大部分将会归咎于对.NET 核心而言未包括进MSBuild的测试,测试既意味着测试也是间接依赖。对.NET 核心,搞清楚一个测试为什么失败了,它是否需要被修复或者被关闭就需要大量的调查。为了快速获取一个新的状态,我们尝试去找一组相关的测试或者是由于相同的原因而失败的测试,将它们作为一个小组关闭,并且将后续的问题归档去作更彻底的调查。

NuGet

.NET 核心是一个传递一系列NuGet包的模块化的框架。迁移到一个基于方法的包对我们而言是相当具有挑战性的,因为这是大量事件的合成。在NuGet中,这个支持针对NET 核心的功能非常新颖。在Windows10 的工具中安装Visual studio并且在默认情况下仅仅对针对最新平台的UWP项目和便携式类库项目启用。所以它们对一个团队来说很新颖但是同样也很陌生,有一些是没有被记录并且在一些项目中,我们使用它们的时候,没有直接支持它们的工具。我们捕捉的几个bug(已经被修复)并且当有一个问题时,在NuGet中经常会有一些错误的信息及输出没有明确问题是什么或者为什么这个问题会发生。

在我们开始移植时,.NET 核心没有提供控制台应用程序因此并没有.NET 核心的 NuGet客户端。在Windows中,我们继续使用NuGet.exe在.NET框架上运行。在Linux系统中,我们尝试在Mono运行相同的客户端。它的运行的还可以,虽然考虑到它从未被设计在Mono下运行,然而我们遇到超时,下载包失败的问题。.NET 核心介绍了比大多项目之前所参考的NuGet包更多的NuGet包并且我们发现有必要重复运行NuGet的回复命令直到将所有的包下载到高速缓冲存储器中。当我们转去使用.NET CLI 的NuGet恢复命令,这已经被用于跨平台运行,且在.NET 核心运行,而且包括了在包中对恢复速度以及可靠性的改进措施,我们期望这些问题不久可以消失

构建.NET核心也需要一个MSBuild任务,从.NET 核心中选择有用的事物使用。这对Linux或者.NET 核心都是不可用的。瓦尔发现了在Linux和Mono中实现这个任物的两个包,但是它们与“真实”任务的性能并不匹配,而且他们每一个人用不同的方法在不同的项目中都失败了。

托管语言团队拥有我们所依赖的几个任务和目标,所以我们想让他们发布我们可以消费的NuGet包。这些项目需要部署在不被NuGet所支持的目标目录下的子文件夹。我们和NuGet团队讨论我们的案例,并且他们提出他们已经计划下个发布的一些特性的特性,正好符号我们的期望

总结

这是我们关于移植.NET 核心的经验。希望你已经发现它的有趣之处。如果你正在将代码移植到.NET核心,它将对你会很有帮助。我们对你的经历也很感兴趣。让我们了解你关注的主要点是什么,在.NET 核心中,来自网络框架中的什么样的APIS对你是最有用的。同时确保在你所考虑移植的工程上运行ApiPort.

基于你的反馈,ApiPort和其他资源提供的报告,我们都将会继续添加更多从.NET到.NET核心的APIs, 同时,我们将会不断的改善全方位的体验以及那些帮助你移植到.NET核心的工具。

移植快乐!