如何在MSBuild中避免重复?

6

我不介意在必要时偶尔重复某些内容,但在MSBuild中,我真的不知道如何永远避免重复。它没有通常意义上的“函数”;一个目标只能被调用一次,即使通过CallTarget<Import>也只能在Project级别上工作。

这里有一个我正在尝试消除重复的具体例子:

<Target Name="Tgt1">
  <PropertyGroup><Conf1>Twiddle</Conf1><Conf2>Thing</Conf2></PropertyGroup>

  <PropertyGroup><xxxxxxxxxxExePath>$(xxxxxxxBuildRoot)\$(Conf1)Console-xxxxxxxxed</xxxxxxxxorExePath></PropertyGroup>
  <MSBuild Projects="$(BuildSingleProj)" Targets="Build;Merge"
           Properties="Configuration=$(Conf1)$(Conf2);Platform=$(Platform);CompiledFileName=$(CompiledFileName);ProjectName=$(ProjectName);SolutionFile=$(SolutionFile);Root=$(Root);Caller=$(MSBuildProjectFullPath)"/>
  <MakeDir Directories="$(xxxxxxxxorExePath)" />
  <WriteLinesToFile File="$(xxxxxxxxorExePath)\xxxxxxx.IsPortable.txt" />
  <WriteLinesToFile File="$(xxxxxxxxorExePath)\xxxxxxx.Global.Settings.xml" Lines="@(xxxxxxxLicense)" Overwrite="true" />
  <Exec Command='$(xxxxxxxxorExePath)\xxxxxxx.exe -a "$(xxxxxxxBuildRoot)\$(Conf1)$(Conf2)-Merged\xxxxxxx.exe" "$(xxxxxxxBuildRoot)\$(Conf1)$(Conf2)-xxxxxxxxed\xxxxxxx.exe"'/>
</Target>

我有四个这样的目标:Tgt1Tgt2Tgt3Tgt4。这四个目标之间唯一不同的是第一行,定义了Conf1Conf2
我所知道的唯一比较可行的去重想法是将共享代码移到一个新目标中,并通过MSBuild任务调用它。不幸的是,这需要手动传递一长串属性,并且该任务使用相当多的属性(我数了11个属性和1个项目组)。
另一个要求是,我可以使用任意子集的这些目标来调用脚本,例如\t:Tgt2,Tgt3
是否有任何明智的替代方法来避免只是复制/粘贴这块代码-而不涉及大量属性列表的复制?
2个回答

8
这是使用批处理的完美场景。
您需要创建带有适当元数据的自定义,然后创建一个单一的目标来引用新项。
您可以像这样将每个项包装在自己的目标中:
<Target Name="Tgt1">
  <ItemGroup>
    <BuildConfig Include="Tgt1">
      <Conf1>Twiddle</Conf1>
      <Conf2>Thing</Conf2>
    </BuildConfig>
  </ItemGroup>
</Target>

<Target Name="Tgt2">
  <ItemGroup>
    <BuildConfig Include="Tgt2">
      <Conf1>Twaddle</Conf1>
      <Conf2>Thing 1</Conf2>
    </BuildConfig>
  </ItemGroup>
</Target>

<Target Name="Tgt3">
  <ItemGroup>
    <BuildConfig Include="Tgt3">
      <Conf1>Tulip</Conf1>
      <Conf2>Thing 2</Conf2>
    </BuildConfig>
  </ItemGroup>
</Target>

您需要一个核心目标来调用并执行所有工作,如下所示:
<Target Name="CoreBuild" Outputs="%(BuildConfig.Identity)">
  <Message Text="Name  : %(BuildConfig.Identity)" />
  <Message Text="Conf1 : %(BuildConfig.Conf1)" />
  <Message Text="Conf2 : %(BuildConfig.Conf2)" />
</Target>

Outputs="%(BuildConfig.Identity)"添加到目标中,可以确保批处理在目标级别而不是任务级别执行。
只要最后一个目标是核心目标,就可以通过传递任意组合的目标来从msbuild执行此操作。例如,执行以下命令MSBuild.exe test.msbulid /t:Tgt1,Tgt3,CoreBuild将给出以下输出:
Name  : Tgt1
Conf1 : Twiddle
Conf2 : Thing

Name  : Tgt3
Conf1 : Tulip
Conf2 : Thing 2

但这意味着我不能再只构建一个或两个了,对吗?有什么方法可以保留这种能力吗? - Roman Starkov
您可以对项目设置条件。我将在答案中更新一个示例。 - Aaron Carlson
谢谢,虽然如果我想包含任意两个构建,条件会变得非常棘手。我目前倾向于保持重复,因为我想保留指定 /t:Tgt1,Tgt2 的能力——实际脚本有几个更多的目标,我们依赖于像这样选择任意子集的能力。 - Roman Starkov
我更新了答案以更好地适应问题的修订。 - Aaron Carlson

6
DRY并不是MSBuild的原则。话虽如此,在任何情况下都不应该重复自己,只要有合理的避免方法。Aaron提供的关于批处理的答案是很好的方法之一。这是一种防止重复的手段。
我想指出的一件事是,在更高的层面上,你似乎把MSBuild看作一种过程化语言(即具有可以调用的函数等)。但实际上,MSBuild比过程化更加声明性。如果你正在创建MSBuild脚本,并且你有“创建X函数以便在Y点调用”这样的思维模式,那么你就会进入痛苦的世界。相反,你应该将MSBuild视为阶段。例如:收集文件、编译、发布等。当你以这种方式思考时,就能完全理解为什么目标在执行一次后会被跳过(你在试验中显然已经观察到了)。
此外,经过我长期与MSBuild的工作,我发现以一种通用/超级可重用的方式进行操作可能会很麻烦。虽然可以做到,但我会将这种努力保留给您确定会被多次重复使用的.targets文件。现在,我更加实用主义,介于完全黑客脚本和以前的方式之间。我有一组脚本可以重复使用,但除了这些脚本外,我尽量保持简单。其中一个重要原因是有很多人知道MSBuild的基础知识,但只有很少的人对它有非常深入的了解。创建良好的通用脚本需要对MSBuild有深入的了解,因此当您离开项目时,接替您的人将不知道您在做什么(也许对承包商来说这是好事?哈哈)。

无论如何,我在http://sedotech.com/Resources#Batching上有一堆关于批处理的资源。


谢谢您的见解,希望这能让我少费些力气。我一直试图将MSBuild视为声明性语言,但即使是声明性语言也需要一种创建抽象的机制。请原谅我提出“函数”这个名称,那显然是错误的术语。 - Roman Starkov
我曾经暗自希望有一些很酷的机制,只是我不知道而已。也许有一种方法可以减轻将相同的10多个属性传递给多个MSBuild任务的痛苦?实际上我差点就做到了,只是属性被过早地评估了,而且我找不到任何像“eval”这样的东西来评估它们,就像它们被传递给MSBuild一样... - Roman Starkov
如果你想要“eval”项目,那么你需要将ItemGroup放置在一个你将要执行的目标内。在任何目标执行之前,位于目标外部的ItemGroup元素会被评估,但是位于目标内部的ItemGroup元素会在目标执行时被评估。 - Sayed Ibrahim Hashimi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接