一个典型的.NET5.0项目文件是这样的,看着非常简洁:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
</ItemGroup>
</Project>
但是,当我们执行“生成”时,却可以看到输出了大量日志,完全不知道这些目标都是哪来的?
我们知道,生成操作实际是由MSBuild执行的。
那么,MSBuild到底干了什么?
查看日志
虽然,只要你在选项里设置日志级别为“诊断”,项目生成时会输出非常详细的日志记录:
但是,这样生成的文本日志量太大了,要找出需要的信息难如登天。
这时,我们可以使用“MSBuild结构化日志查看器”,以可视化的方式查看日志。
安装
查看器的安装依赖Chocolatey。
首先,以管理员身份打开命令提示符,运行下列命令安装Chocolatey:
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
然后,运行下列命令安装日志查看器:
choco install msbuild-structured-log-viewer
生成日志
打开MSBuild Structured Log Viewer
,选择“Open Project/Solution”,打开我们新建的Web API示例项目WebApplication1.sln
,点击“Build”按钮生成日志:
运行完成后,你应该可以看到如下内容:
点击项目名称左边的箭头展开后,可以看到MSBuild准备执行的所有目标,每个目标中包含多个任务:
灰色的表示跳过的目标,展开后可以看到跳过的原因。
下面,我们以bin\Debug\net5.0\Swashbuckle.AspNetCore.Swagger.dll
文件怎么输出的为例,演练如何分析日志。
分析日志
Copy任务
在左侧Search Log
窗口上方,输入bin\Debug\net5.0\Swashbuckle.AspNetCore.Swagger.dll
作为条件:
可以看到文件是由_CopyFilesMarkedCopyLocal
目标中的Copy
任务生成的,选中后在中间Log
窗口双击任务名,会在右侧窗口显示任务详情,原来任务来源于MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets文件。
Copy
任务作用是将源文件ReferenceCopyLocalPaths
复制到目标文件$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)
那么源文件和目标文件的值,又是从哪来的呢?
OutDir属性
我们可以轻易地查找到$(OutDir)
的值等于bin\Debug\net5.0
,却没看到bin\Debug\net5.0
这个值是由谁赋给它的:
通过左侧的Find In Files
窗口,原来它来自于MSBuild\Current\Bin\amd64\Microsoft.Common.CurrentVersion.targets
文件,从OutputPath
赋值:
metaproj文件
OutputPath
的值来源于同一个文件,等于$(BaseOutputPath)$(Configuration)\
而BaseOutputPath
也来源于这个文件。但奇怪的是,Configuration
却来源于一个叫做WebApplication1.sln.metaproj
的文件:
项目目录下并没有这个文件啊?!
随后,我们在日志中找到这样一条消息:
已生成元项目“D:\Codes\WebApplication1\WebApplication1.sln.metaproj”。
而且,在WebApplication1.sln.metaproj
中,我们还可以找到Rebuild
目标:
而Rebuild又依赖于其他目标:
你还记得生成日志时,带的/t:Rebuild
参数吗?
现在清楚了,MSBuild启动时首先生成.metaproj文件,然后根据文件中的元数据,按照依赖关系执行目标。
DestinationSubDirectory属性
但是,%(DestinationSubDirectory)
在日志里并没有找到任何赋值的位置。
试着继续探索原始文件来源,最终定位到了ResolvePackageAssets
目标下的ResolvePackageAssets
任务:
具体参数值对应任务的输出参数RuntimeAssemblies
:
<Output TaskParameter="RuntimeAssemblies" ItemName="RuntimeCopyLocalItems" />
查看dotnet/sdk/Tasks/Microsoft.NET.Build.Tasks/ResolvePackageAssets.cs
的源码,RuntimeAssemblies
的类型是ITaskItem[]
。
ITaskItem
定义如下:
public interface ITaskItem
{
string ItemSpec { get; set; }
int MetadataCount { get; }
ICollection MetadataNames { get; }
IDictionary CloneCustomMetadata();
void CopyMetadataTo(ITaskItem destinationItem);
string GetMetadata(string metadataName);
void RemoveMetadata(string metadataName);
void SetMetadata(string metadataName, string metadataValue);
}
接着,我们找到这样一段代码:
if (!string.IsNullOrEmpty(destinationSubDirectory))
{
WriteMetadata(MetadataKeys.DestinationSubDirectory, destinationSubDirectory);
}
DestinationSubDirectory原来是Metadata啊!
结论
根据上面的分析,可以梳理出bin\Debug\net5.0\Swashbuckle.AspNetCore.Swagger.dll
文件如何输出的整个流程:
MSBuild启动,根据项目文件生成
.metaproj
文件MSBuild根据
/t
参数, 从.metaproj
文件中读取目标根据目标的依赖关系,按顺序执行其他目标
其中,
ResolvePackageAssets
目标下的ResolvePackageAssets
任务获取项目所有依赖包的Metadata再由
_CopyFilesMarkedCopyLocal
目标中的Copy
任务遍历依赖包,根据Metadata复制文件到指定目录下的指定文件名
现在,你可以跟同事show一下:我知道MSBuild干了什么!
如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“,记住我!