本文涉及 MSBuild XML 文件处理过程的一部分,因此需要你对其有所了解。比如:
- Property 解析是不同于 Task Action 等,前者当场计算,后者随 Target 触发。
- Property Item 解析是与位置相关的,从上到下、一条条,立即生效。
参考文档,你也可以先去看它们
- MSBuild: Property Functions
- MSBuild: Item Functions
- MSBuild: Well-known Item Metadata
- VS Blog: # MSBuild Property Functions
- VS Blog: # MSBuild Property Functions (2)
- SO: In MSBuild, can I use the String.Replace function on a MetaData item?
${VisualStudio.InstallationPath}\MSBuild里的官方用例
实例 1 获取相对路径
<!-- Directory.Build.props -->
<PropertyGroup>
<MyRootDir>$(MSBuildThisFileDirectory)</MyRootDir>
<ProjectDirRel>$(MSBuildProjectDirectory.Substring($(MyRootDir.Length)))</ProjectDirRel>
</PropertyGroup>
假如有这样的目录结构:
hello\
hello.vcxproj
Directory.Build.props
那么 $(ProjectDirRel) == 'hello'。
这个写法说明了,
- MSBuild Property 可直接作为 dotnet
System.String使用其 method、property。 - 嵌套时,每层需要使用表达式时,都要用
$(expr)包装之,不能裸写expr。 - MSBuild Property 处理是立即的,下一行就可以使用上一行的结果。
此外,还试出来一个写法,即使用 dotnet property $(s.Length) 等价于 dotnet method $(s.get_Length())。
实例 2 删除字符串的后缀
<PropertyGroup>
<ProjectNameAlt>_static</ProjectNameAlt>
<_ProjectNameNoSuffix>$(MSBuildProjectName)</_ProjectNameNoSuffix>
<_ProjectNameNoSuffix Condition="$(MSBuildProjectName.EndsWith($(ProjectNameAlt)))">$(_ProjectNameNoSuffix.Substring(0, $([MSBuild]::Subtract($(_ProjectNameNoSuffix.Length), $(ProjectNameAlt.Length)))))</_ProjectNameNoSuffix>
</PropertyGroup>
这里使用了 $(MSBuildProjectName),表示当前 proj 的文件名(无扩展名)。假如项目文件是 a_static.csproj,那么 $(_ProjectNameNoSuffix) == 'a'。
MSBuild 提供了命令行写法,以获取这个结果:
msbuild a.csproj "-getProperty:_ProjectNameNoSuffix"
如果你喜欢,还可以利用 <Import> 的报错来观察:
<!-- 紧接着写在上述 </PropertyGroup> 之后就行,执行到这必然报错,把 “路径” 打出来 -->
<Import Project="['$(_ProjectNameNoSuffix)']nonexist" />
这个写法说明了,
- MSBuild 不支持直观的数学表达式,
$(s.Length - 1)这种是不行的,得用$([MSBuild]::Subtract($(s.Length), 1)
此外,下面这种写法也行,说明只要允许,它会自动 string to int:
<PropertyGroup>
<ProjectNameAlt>_static</ProjectNameAlt>
<_ProjectNameNoSuffix>$(MSBuildProjectName)</_ProjectNameNoSuffix>
<_ProjectNameAltLength>$(ProjectNameAlt.Length)</_ProjectNameAltLength>
<_ProjectNameNoSuffixLength>$([MSBuild]::Subtract($(_ProjectNameNoSuffix.Length), $(_ProjectNameAltLength)))</_ProjectNameNoSuffixLength>
</PropertyGroup>
实例 3 内容复制,且保留相对路径
本意是想把项目目录下 deploy\ 子目录的所有内容,保留除了 deploy\ 外的目录结构,复制到输出目录。
从
src\
deploy\
config.toml
data\
a.bin
产生
out\
config.toml
data\
a.bin
最终写法,
<ItemGroup>
<!-- None Item 是本就有的类型,被 t:CopyToOutputDirectory 支持 -->
<None Update="deploy\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPathRelativeStart>$([System.String]::Copy('deploy\').Length)</TargetPathRelativeStart>
<TargetDir>$([System.String]::Copy(%(RelativeDir)).Substring(%(TargetPathRelativeStart)))</TargetDir>
<TargetPath>%(TargetDir)%(FileName)%(Extension)</TargetPath>
</None>
</ItemGroup>
这个写法中,
[System.String]::Copy(string-like)用于把可能是字符串的对象转换为 dotnet string。旧版 MSBuild 不支持把 MSBuild Property、Item Metadata 字符串、字符串字面量等直接作为System.String使用,需要构造。%()不似$(),它是访问<None>的 Item Metadata,而非上文中的<Project>的 Property。
上述没有体现出试错中的一个复杂情况,下面是一个中间结果,
<ItemGroup>
<None Update="deploy\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<RelativeDirLength>$([MSBuild]::Add($(ProjectDir.Length), $([System.String]::Copy('deploy\').Length)))</RelativeDirLength>
<TargetPath>$([System.String]::Copy(%(FullPath)).Substring(%(RelativeDirLength)))</TargetPath>
</None>
</ItemGroup>
最后总结,同样是基于 dotnet,MSBuild 中函数与 PowerShell 写法很像,用起来却不趁手,束手束脚的。