笔记:MSBuild proj 文件的 Property 中使用函数表达式

212 阅读1分钟

本文涉及 MSBuild XML 文件处理过程的一部分,因此需要你对其有所了解。比如:

  1. Property 解析是不同于 Task Action 等,前者当场计算,后者随 Target 触发。
  2. Property Item 解析是与位置相关的,从上到下、一条条,立即生效。

参考文档,你也可以先去看它们

实例 1 获取相对路径

<!-- Directory.Build.props -->
<PropertyGroup>
  <MyRootDir>$(MSBuildThisFileDirectory)</MyRootDir>
  <ProjectDirRel>$(MSBuildProjectDirectory.Substring($(MyRootDir.Length)))</ProjectDirRel>
</PropertyGroup>

假如有这样的目录结构:

hello\
    hello.vcxproj
Directory.Build.props

那么 $(ProjectDirRel) == 'hello'

这个写法说明了,

  1. MSBuild Property 可直接作为 dotnet System.String 使用其 method、property。
  2. 嵌套时,每层需要使用表达式时,都要用 $(expr) 包装之,不能裸写 expr
  3. 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" />

这个写法说明了,

  1. 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 写法很像,用起来却不趁手,束手束脚的。