用msbuild自动创建nuget软件包

        NuGet是一个传播项目的好方法。你在一个项目上工作,你发布一个包,它就可以立即提供给数百万的开发者。创建一个包包括几个步骤,如编写一个.nuspec文件,创建一个文件夹结构,将正确的文件复制到正确的文件夹/子文件夹中,并调用nuget pack命令。虽然这些步骤并不复杂,但却很容易出错。我在发送我的一些NuGet包的第一个alpha版本时学到了这个教训。发生的情况是,我创建了一个包,然后我开始感到有些疑惑--在复制文件之前,我是否真的建立了这个项目? 我是否复制了发布版本而不是调试版本?然后人们开始使用我的软件包,并开始要求(除其他外)有一个能在其他版本的.NET框架上运行的版本。这意味着创建软件包的工作量基本上至少要增加一倍,因为我上面概述的步骤必须在每个目标平台上都要遵循。我已经决定将创建NuGet包的过程自动化,但要运送一个多平台的NuGet包是一个强迫性的功能,要真正做这个工作。在开始工作之前,我设定了几个目标。

  • 我将能够只用一个简单的命令来创建一个包
  • 我将能够创建一个多平台的包
  • 我将能够排除/包括特定平台的代码
  • 包含在软件包中的程序集将被签名
  • 我将能够从我的程序集中剥离InternalsVisibleTo
  • 我将不必在源码控制中检查任何二进制文件
  • 这些变化都不会破坏Visual Studio的体验(也就是说,我将能够以与变化前相同的方式使用Visual Studio)。

由于我在所有的项目中都使用Visual Studio,所以使用MSBuild来实现我的目标是不难的。我从启用为多个.NET框架版本构建项目开始(在我的案例中,我真的只需要能够针对.NET框架4和.NET框架4.5)。这是很简单的--如果你打开你的.csproj文件,你会很快发现以下一行。

<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>

你可能已经猜到了,这表示目标.NET框架版本。这可以被参数化,如下所示。

<TargetFrameworkVersion
Condition="'$(TargetFrameworkVersion)' != 'v4.0'">v4.5</TargetFrameworkVersion>

只要将TargetFrameworkVersion参数传递/设置为'v4.0',就能为.NET Framework 4构建项目。如果传递/设置任何其他值(或者根本没有传递/设置该值),该项目将针对.NET框架4.5构建。现在你可以通过从开发者命令提示符中构建项目来测试它。下面的命令将为.NET Framework 4构建版本。

msbuild myproj.csproj /t:Build /p:TargetFrameworkVersion=v4.0

请注意,上述命令可能会失败。其中一个原因可能是你的代码使用了只有在.NET Framework 4.5上才有的API(async可能是最好的例子,但还有很多)。因此,你可能需要一种机制来排除这段代码,或者用.NET Framework 4的对应代码来替代它。典型的做法是使用#ifdef预编译器指令。你只需要一个常量(我们称之为NET40),它将表明代码是针对.NET框架4构建的。我们可以根据已经设置的TargetFrameworkVersion属性的值,在csproj文件中定义这个常量。
要做到这一点,我们只需要在用于设置配置(即Debug/Release)特定属性的PropertyGroups下面创建一个新的PropertyGroup。下面是这个新属性组的样子。
 

<PropertyGroup>
<DefineConstants
Condition=" '$(TargetFrameworkVersion)' == 'v4.0'">$(DefineConstants);NET40</DefineConstants>
</PropertyGroup>

现在我们可以在#ifdef预编译器指令中使用NET40来有条件地编译/排除特定平台的代码。
当我们在做这件事的时候,我们可以注意从代码中删除InternalsVisibleTo属性。我们可以使用与TargetFrameworkVersion相同的技巧,如果一个属性(我们称之为InternalsVisibleToEnabled)被设置为false,我们可以定义一个常量。默认情况下,它的值将被设置为true true,但当构建一个包时(相对于构建项目本身),我们将把它设置为false。这将允许我们使用#ifdef指令中的常量来排除我们在构建包时不需要的代码。有了这个改变,上面创建的PropertyGroup将变成。

<PropertyGroup>
<DefineConstants
Condition=" '$(TargetFrameworkVersion)' == 'v4.0'">$(DefineConstants);NET40</DefineConstants>
<DefineConstants
Condition=" '$(InternalsVisibleToEnabled)'">$(DefineConstants);INTERNALSVISIBLETOENABLED</DefineConstants>
</PropertyGroup>

另一件要看的重要事情是引用。如果你引用了一个没有与.NET框架一起提供的程序集,你需要确保你引用的是这个程序集的正确版本。如果你包括其他多平台的NuGet包,这一点尤其明显--引用一个错误版本的包可能会导致奇怪的构建中断,或者,一些你期望存在的API会显得缺失。这可以通过条件引用来解决。例如,在我的一些项目中,我引用了EntityFramework NuGet包,我需要引用EntityFramework.dll的正确版本,这取决于我编译项目时的.NET框架版本。为了解决这个问题,我可以使用MSBuild的Choose结构,像这样。

<Choose>
<When Condition="'$(TargetFrameworkVersion)' == 'v4.0'">
<ItemGroup>
<Reference Include="EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\EntityFramework.6.1.0\lib\net40\EntityFramework.dll</HintPath>
</Reference>
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\EntityFramework.6.1.0\lib\net40\EntityFramework.SqlServer.dll</HintPath>
</Reference>
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<Reference Include="EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.dll</HintPath>
</Reference>
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\EntityFramework.6.1.0\lib\net45\EntityFramework.SqlServer.dll</HintPath>
</Reference>
</ItemGroup>
</Otherwise>
</Choose>

就针对不同版本的.NET框架构建项目而言,这已经是非常多了。有了这个,我们可以看看NuGet方面的情况。首先要做的是在Visual Studio中打开解决方案,在项目资源管理器中右击解决方案节点,选择 "启用NuGet包恢复"。这将做几件事

  • 它将启用在构建时恢复丢失的NuGet包。这是非常有用的,有助于防止在源控制中检查二进制文件(你几乎总是希望如此)。
  • 为了实现上述目标,它将创建一个.nuget文件夹,其中包含一些文件,其中NuGet.target对我们来说是最重要的。
  • 它将修改.csproj文件以定义一些属性,但最重要的是它将包括NuGet.target文件。

注意:NuGet丢到.nuget文件夹中的一个文件是Nuget.exe。如果你使用了源码控制(如果你没有,你应该被命名为这个多年来没人碰过的老COBOL项目的新功能的主要开发者--他们也没有使用源码控制,所以你应该没问题),你要从.nuget文件夹中签入所有的文件,但是Nuget.exe。
保存你的文件,或者,更好的是,关闭Visual Studio。这很重要。如果你在VS内部和外部都修改了你的项目文件,在最好的情况下你会失去你在VS外部所做的修改(只有当你在VS检测到你在VS外部编辑了文件时选择了正确的选项)。在最坏的情况下,当.csproj文件只被工具部分修改时,你将会陷入这种.csproj的困境,你无法重新创建已经丢失的东西。工具不再工作(或者随机工作,这更糟糕),因为一些修改被丢失了,项目不能编译,最好的选择就是把所有的修改恢复到最后一次提交(如果你没有使用源代码控制,那么,你知道,COBOL项目需要一个主要的(或者,实际上,任何)开发者),然后从头开始。


现在我们准备添加逻辑来构建NuGet包。我们将创建一个名为 "CreatePackage "的新目标(我原本想叫它 "BuildPackage",但结果发现在.NuGet.target文件中已经有了这个名字的目标),这将是构建软件包的入口。目前,我的所有项目的NuGet包都很简单,只包含一个带有产品代码的汇编和一个带有测试的汇编。由于测试不是我发送的NuGet包的一部分,我可以在包含产品代码的.csproj文件中添加CreatePackage目标。如果我有更多的程序集,我将创建一个单独的文件(可能称为类似build.proj的文件,在那里我将拥有构建和打包所有程序集所需的所有目标)。在这个目标中,我将需要。

  • 为我想针对的每个平台构建我的程序集/程序集
  • 创建NuGet要求的文件夹结构
  • 将第一步中构建的工件复制到第二步的文件夹中
  • 使用NuGet.exe打包命令创建软件包。

第一件事是进一步修改.csproj文件以解决几个问题。我们需要解决的第一个问题是,每次我们构建项目时都会覆盖现有的文件。通常情况下(例如从Visual Studio工作时),这不是一个问题,但由于我们要多次调用构建(我们需要为多个平台构建),后续的构建会覆盖以前构建的文件。我们可以通过在构建之间复制文件来解决这个问题,但我更喜欢的解决方案是能够告诉构建在哪里放置构建工件。这可以通过取消在配置特定的PropertyGroups中设置OutputPath属性(例如设置为'bin\Release\'),并在Configration之后的第一个PropertyGroup中添加以下有条件的OutputPath属性定义来实现。新的OutputPath定义会像这样。

<OutputPath Condition="'$(OutputPath)' == ''">bin\$(Configuration)\</OutputPath>

这允许将OutputPath值作为一个参数传递,如果它不是空的,它将被用于整个构建过程。否则,我们将设置一个默认值,它实际上与最初设置的值相同。
第二件要注意的事是.nuget文件夹中没有NuGet.exe文件的情况(我建议不检查这个文件,所以新克隆的仓库或用 git clean -xdf 命令清理仓库时,它就会丢失)。这可以通过在第一个PropertyGroup中添加以下属性定义来轻松解决。

<DownloadNuGetExe>true</DownloadNuGetExe>

现在,每当你调用一个依赖于NuGet的CheckPrerequisites目标时,NuGet将检查NuGet.exe文件是否存在,如果不存在,它将下载它。
有了上面的改动,我们就可以准备好CreatePackage目标了。下面是它的样子(我从 Interactive Pre-Generated Views 项目中复制了它)。

<Target Name="CreatePackage" DependsOnTargets="CheckPrerequisites">
<Error Text="KeyFile parameter not spercified (/p:KeyFile=MyKey.snk)" Condition=" '$(KeyFile)' == ''" />
<PropertyGroup>
<Configuration>Release</Configuration>
<PackageSource>bin\$(Configuration)\Package\</PackageSource>
<NuSpecPath>..\tools\EFInteractiveViews.nuspec</NuSpecPath>
</PropertyGroup>
<RemoveDir Directories="$(PackageSource)" />
<MSBuild Projects="$(MSBuildThisFile)" Targets="Rebuild" Properties="InternalsVisibleToEnabled=false;SignAssembly=true;AssemblyOriginatorKeyFile=$(KeyFile);TargetFrameworkVersion=v4.5;OutputPath=bin\$(Configuration)\net45;Configuration=$(Configuration)" BuildInParallel="$(BuildInParallel)" />
<MSBuild Projects="$(MSBuildThisFile)" Targets="Rebuild" Properties="InternalsVisibleToEnabled=false;SignAssembly=true;AssemblyOriginatorKeyFile=$(KeyFile);TargetFrameworkVersion=v4.0;OutputPath=bin\$(Configuration)\net40;Configuration=$(Configuration)" BuildInParallel="$(BuildInParallel)" />
<Copy SourceFiles="bin\$(Configuration)\net45\$(AssemblyName).dll" DestinationFolder="$(PackageSource)\lib\net45" />
<Copy SourceFiles="bin\$(Configuration)\net40\$(AssemblyName).dll" DestinationFolder="$(PackageSource)\lib\net40" />
<Copy SourceFiles="$(NuSpecPath)" DestinationFolder="$(PackageSource)" />
<Exec Command='$(NuGetCommand) pack $(NuSpecPath) -BasePath $(PackageSource) -OutputDirectory bin\$(Configuration)' LogStandardErrorAsError="true" />
</Target>

让我们逐行看一下这个目标,因为有一些小的补充我还没有提到。我的目标之一是能够对汇编进行签名。我通过向我的.snk文件传递一个路径来做到这一点。为了确保我不会用没有签名的程序集来构建包,如果没有提供文件的路径,我会在第2行出错(这个错误也会告诉我参数名称,这样我就不必在每次需要构建包时打开.csproj文件来记住用于传递.snk文件路径的参数名称)。然后我定义一些我以后使用的属性。

  • Configuration--我总是想在NuGet包中发送发布版本,所以它被硬编码为 "发布"。
  • PackageSource - 目标将创建文件夹的路径,这些文件夹随后将被NuGet使用。平台特定的程序集将被放置在子文件夹中。
  • NuSpecPath - .nuspec文件所在的路径。

之后,我构建项目。我做了两次 - 每个目标平台一次。你看一下传递给Build目标的参数就知道了--特别是TragetFrameworkVersion('v4.5' vs. 'v4.0')和OutputPath('net45' vs. 'net40')。

SignAssembly和AssemblyOriginatorKeyFile确保汇编将被签名。InternalsVisibleToEnabled被设置为false以排除InternalsVisbileTo属性。一旦程序集被构建,它们将被复制到NuGet包将被构建的文件夹中。注意,如果目标文件夹不存在,复制MSBuild任务将创建它。剩下的事情就是创建NuGet包了(终于可以了!)。我执行NuGet.exe(伪装成$(NuGetCommand) - 一个在NuGet.target文件中定义的变量),并提供.nuspec文件的路径,建立软件包的文件夹结构(BasePath参数)和保存NuGet软件包的路径。LogStandardErrorAsError属性告诉MSBuild,如果执行的命令返回一个非零的退出代码,则构建失败。


现在我可以用下面的命令来构建我的NuGet包(从开发人员的命令提示符)。

msbuild myproject.csproj /t:CreatePackage /p:KeyFile=MyKey.snk

尽管对.csproj文件进行了所有的手工编辑,Visual Studio继续工作(至少在以前的水平上),这是我最后的目标。


这就是了! 有了这个指南,你应该能够自动构建你的NuGet包。为了以防万一,这里有一些我介绍这个方法的章节链接。

一旦你明白了这是怎么回事,并且有了我上面提供的模板,只需要不到10分钟的时间就可以把它改编成你的项目(有趣的是,即使想出第一个版本也比在这篇博文中描述它花费的时间(和酒)少得多)。


好好享受吧(并向我展示你的包装)!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值