从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)
默認(rèn)情況下,我們打包 NuGet 包時(shí),目標(biāo)項(xiàng)目安裝我們的 NuGet 包會引用我們生成的庫文件(dll)。除此之外,我們也可以專門做 NuGet 工具包,還可以做 NuGet 源代碼包。然而做源代碼包可能是其中最困難的一種了,目標(biāo)項(xiàng)目安裝完后,這些源碼將直接隨目標(biāo)項(xiàng)目一起編譯。
本文將從零開始,教你制作一個(gè)支持 .NET 各種類型項(xiàng)目的源代碼包。
在開始制作一個(gè)源代碼包之間,建議你提前了解項(xiàng)目文件的一些基本概念:
理解 C# 項(xiàng)目 csproj 文件格式的本質(zhì)和編譯流程
當(dāng)然就算不了解也沒有關(guān)系。跟著本教程你也可以制作出來一個(gè)源代碼包,只不過可能遇到了問題的時(shí)候不容易調(diào)試和解決。
接下來,我們將從零開始制作一個(gè)源代碼包。
我們接下來的將創(chuàng)建一個(gè)完整的解決方案,這個(gè)解決方案包括:
一個(gè)將打包成源代碼包的項(xiàng)目
一個(gè)調(diào)試專用的項(xiàng)目(可選)
一個(gè)測試源代碼包的項(xiàng)目(可選)
像其他 NuGet 包的引用項(xiàng)目一樣,我們需要創(chuàng)建一個(gè)空的項(xiàng)目。不過差別是我們需要創(chuàng)建的是控制臺程序。
當(dāng)創(chuàng)建好之后,Main?函數(shù)中的所有內(nèi)容都是不需要的,于是我們刪除?Main?函數(shù)中的所有內(nèi)容但保留?Main?函數(shù)。
這時(shí) Program.cs 中的內(nèi)容如下:
雙擊創(chuàng)建好的項(xiàng)目的項(xiàng)目,或者右鍵項(xiàng)目 “編輯項(xiàng)目文件”,我們可以編輯此項(xiàng)目的 csproj 文件。
在這里,我將目標(biāo)框架改成了?net48。實(shí)際上如果我們不制作動態(tài)源代碼生成,那么這里無論填寫什么目標(biāo)框架都不重要。在這篇博客中,我們主要篇幅都會是做靜態(tài)源代碼生成,所以你大可不必關(guān)心這里填什么。
提示:如果 net48 讓你無法編譯這個(gè)項(xiàng)目,說明你電腦上沒有裝 .NET Framework 4.8 框架,請改成 net473, net472, net471, net47, net462, net 461, net46, net45, netcoreapp3.0, netcoreapp2.1, netcoreapp2.0 中的任何一個(gè)可以讓你編譯通過的目標(biāo)框架即可。
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
</PropertyGroup>
</Project>
接下來,我們會讓這個(gè)項(xiàng)目像一個(gè) NuGet 包的樣子。當(dāng)然,是 NuGet 源代碼包。
請?jiān)谀愕捻?xiàng)目當(dāng)中創(chuàng)建這些文件和文件夾:
- Assets- build
+ Package.props
+ Package.targets
- buildMultiTargeting
+ Package.props
+ Package.targets
- src
+ Foo.cs
- tools
+ Program.cs
在這里,-?號表示文件夾,+?號表示文件。
Program.cs 是我們一開始就已經(jīng)有的,可以不用管。src 文件夾里的 Foo.cs 是我隨意創(chuàng)建的一個(gè)類,你就想往常創(chuàng)建正常的類文件一樣創(chuàng)建一些類就好了。
比如我的 Foo.cs 里面的內(nèi)容很簡單:
using System;namespace Walterlv.PackageDemo.SourceCode
{
internal class Foo
{
public static void Print() => Console.WriteLine("Walterlv is a 逗比.");
}
}
props 和 targets 文件你可能在 Visual Studio 的新建文件的模板中找不到這樣的模板文件。這不重要,你隨便創(chuàng)建一個(gè)文本文件,然后將名稱修改成上面列舉的那樣即可。接下來我們會依次修改這些文件中的所有內(nèi)容,所以無需擔(dān)心模板自動為我們生成了哪些內(nèi)容。
為了更直觀,我將我的解決方案截圖貼出來,里面包含所有這些文件和文件夾的解釋。
我特別說明了哪些文件和文件夾是必須存在的,哪些文件和文件夾的名稱一定必須與本文說明的一樣。如果你是以教程的方式閱讀本文,建議所有的文件和文件夾都跟我保持一樣的結(jié)構(gòu)和名稱;如果你已經(jīng)對 NuGet 包的結(jié)構(gòu)有一定了解,那么可自作主張修改一些名稱。
現(xiàn)在,我們要雙擊項(xiàng)目名稱或者右鍵“編輯項(xiàng)目文件”來編輯項(xiàng)目的 csproj 文件
我們編輯項(xiàng)目文件的目的,是讓我們前一步創(chuàng)建的項(xiàng)目文件夾結(jié)構(gòu)真正成為 NuGet 包中的文件夾結(jié)構(gòu)。
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
<!-- 要求此項(xiàng)目編譯時(shí)要生成一個(gè) NuGet 包。-->
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- 這里為了方便,我將 NuGet 包的輸出路徑設(shè)置在了解決方案根目錄的 bin 文件夾下,而不是項(xiàng)目的 bin 文件夾下。-->
<PackageOutputPath>..\bin\$(Configuration)</PackageOutputPath>
<!-- 創(chuàng)建 NuGet 包時(shí),項(xiàng)目的輸出文件對應(yīng)到 NuGet 包的 tools 文件夾,這可以避免目標(biāo)項(xiàng)目引用我們的 NuGet 包的輸出文件。
同時(shí),如果將來我們準(zhǔn)備動態(tài)生成源代碼,而不只是引入靜態(tài)源代碼,還可以有機(jī)會運(yùn)行我們 Program 中的 Main 函數(shù)。-->
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<!-- 此包將不會傳遞依賴。意味著如果目標(biāo)項(xiàng)目安裝了此 NuGet 包,那么安裝目標(biāo)項(xiàng)目包的項(xiàng)目不會間接安裝此 NuGet 包。-->
<DevelopmentDependency>true</DevelopmentDependency>
<!-- 包的版本號,我們設(shè)成了一個(gè)預(yù)覽版;當(dāng)然你也可以設(shè)置為正式版,即沒有后面的 -alpha 后綴。-->
<Version>0.1.0-alpha</Version>
<!-- 設(shè)置包的作者。在上傳到 nuget.org 之后,如果作者名與 nuget.org 上的賬號名相同,其他人瀏覽包是可以直接點(diǎn)擊鏈接看作者頁面。-->
<Authors>walterlv</Authors>
<!-- 設(shè)置包的組織名稱。我當(dāng)然寫成我所在的組織 dotnet 職業(yè)技術(shù)學(xué)院啦。-->
<Company>dotnet-campus</Company>
</PropertyGroup>
<!-- 在生成 NuGet 包之前,我們需要將我們項(xiàng)目中的文件夾結(jié)構(gòu)一一映射到 NuGet 包中。-->
<Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
<ItemGroup>
<!-- 將 Package.props / Package.targets 文件的名稱在 NuGet 包中改為需要的真正名稱。
因?yàn)?NuGet 包要自動導(dǎo)入 props 和 targets 文件,要求文件的名稱必須是 包名.props 和 包名.targets;
然而為了避免我們改包名的時(shí)候還要同步改四個(gè)文件的名稱,所以就在項(xiàng)目文件中動態(tài)生成。-->
<None Include="Assets\build\Package.props" Pack="True" PackagePath="build\$(PackageId).props" />
<None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
<None Include="Assets\buildMultiTargeting\Package.props" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).props" />
<None Include="Assets\buildMultiTargeting\Package.targets" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).targets" />
<!-- 我們將 src 目錄中的所有源代碼映射到 NuGet 包中的 src 目錄中。-->
<None Include="Assets\src\**" Pack="True" PackagePath="src" />
</ItemGroup>
</Target>
</Project>
接下來,我們將編寫編譯文件 props 和 targets。注意,我們需要寫的是四個(gè)文件的內(nèi)容,不要弄錯了。
如果我們做好的 NuGet 源碼包被其他項(xiàng)目使用,那么這四個(gè)文件中的其中一對會在目標(biāo)項(xiàng)目被自動導(dǎo)入(Import)。在你理解?理解 C# 項(xiàng)目 csproj 文件格式的本質(zhì)和編譯流程?一文內(nèi)容之前,你可能不明白“導(dǎo)入”是什么意思。但作為從零開始的入門博客,你也不需要真的理解導(dǎo)入是什么意思,只要知道這四個(gè)文件中的代碼將在目標(biāo)項(xiàng)目編譯期間運(yùn)行就好。
你只需要將下面的代碼拷貝到 buildMultiTargeting 文件夾中的 Package.props 文件即可。注意將包名換成你自己的包名,也就是項(xiàng)目名。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<!-- 為了簡單起見,如果導(dǎo)入了這個(gè)文件,那么我們將直接再導(dǎo)入 ..\build\Walterlv.PackageDemo.SourceCode.props 文件。
注意到了嗎?我們并沒有寫 Package.props,因?yàn)槲覀冊诘谌骄帉戫?xiàng)目文件時(shí)已經(jīng)將這個(gè)文件轉(zhuǎn)換為真實(shí)的包名了。-->
<Import Project="..\build\Walterlv.PackageDemo.SourceCode.props" />
</Project>
你只需要將下面的代碼拷貝到 buildMultiTargeting 文件夾中的 Package.targets 文件即可。注意將包名換成你自己的包名,也就是項(xiàng)目名。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<!-- 為了簡單起見,如果導(dǎo)入了這個(gè)文件,那么我們將直接再導(dǎo)入 ..\build\Walterlv.PackageDemo.SourceCode.targets 文件。
注意到了嗎?我們并沒有寫 Package.targets,因?yàn)槲覀冊诘谌骄帉戫?xiàng)目文件時(shí)已經(jīng)將這個(gè)文件轉(zhuǎn)換為真實(shí)的包名了。-->
<Import Project="..\build\Walterlv.PackageDemo.SourceCode.targets" />
</Project>
下面是 build 文件夾中 Package.props 文件的全部內(nèi)容。可以注意到我們幾乎沒有任何實(shí)質(zhì)性的代碼在里面。即便我們在此文件中還沒有寫任何代碼,依然需要創(chuàng)建這個(gè)文件,因?yàn)楹竺娴谖宀轿覀儗⑻砑痈鼜?fù)雜的代碼時(shí)將再次用到這個(gè)文件完成里面的內(nèi)容。
現(xiàn)在,保持你的文件中的內(nèi)容與下面一模一樣就好。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
</Project>
下面是 build 文件夾中的 Package.targets 文件的全部內(nèi)容。
我們寫了兩個(gè)編譯目標(biāo),即 Target。_WalterlvDemoEvaluateProperties?沒有指定任何執(zhí)行時(shí)機(jī),但幫我們計(jì)算了兩個(gè)屬性:
_WalterlvDemoRoot?即 NuGet 包的根目錄
_WalterlvDemoSourceFolder?即 NuGet 包中的源代碼目錄
另外,我們添加了一個(gè)?Message?任務(wù),用于在編譯期間顯示一條信息,這對于調(diào)試來說非常方便。
_WalterlvDemoIncludeSourceFiles?這個(gè)編譯目標(biāo)指定在?CoreCompile?之前執(zhí)行,并且執(zhí)行需要依賴于?_WalterlvDemoEvaluateProperties?編譯目標(biāo)。這意味著當(dāng)編譯執(zhí)行到?CoreCompile?步驟時(shí),將在它執(zhí)行之前插入?_WalterlvDemoIncludeSourceFiles?編譯目標(biāo)來執(zhí)行,而?_WalterlvDemoIncludeSourceFiles?依賴于?_WalterlvDemoEvaluateProperties,于是?_WalterlvDemoEvaluateProperties?會插入到更之前執(zhí)行。那么在微觀上來看,這三個(gè)編譯任務(wù)的執(zhí)行順序?qū)⑹?#xff1a;_WalterlvDemoEvaluateProperties?->?_WalterlvDemoIncludeSourceFiles?->?CoreCompile。
_WalterlvDemoIncludeSourceFiles?中,我們定義了一個(gè)集合?_WalterlvDemoCompile,集合中包含 NuGet 包源代碼文件夾中的所有 .cs 文件。另外,我們又定義了?Compile?集合,將?_WalterlvDemoCompile?集合中的所有內(nèi)容添加到?Compile?集合中。Compile?是 .NET 項(xiàng)目中的一個(gè)已知集合,當(dāng)?CoreCompile?執(zhí)行時(shí),所有?Compile?集合中的文件將參與編譯。注意到我沒有直接將 NuGet 包中的源代碼文件引入到?Compile?集合中,而是經(jīng)過了中轉(zhuǎn)。后面第五步中,你將體會到這樣做的作用。
我們也添加一個(gè)?Message?任務(wù),用于在編譯期間顯示信息,便于調(diào)試。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<Target Name="_WalterlvDemoEvaluateProperties">
<PropertyGroup>
<_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
<_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
</PropertyGroup>
<Message Text="1. 初始化源代碼包的編譯屬性" />
</Target>
<!-- 引入 C# 源碼。 -->
<Target Name="_WalterlvDemoIncludeSourceFiles"
BeforeTargets="CoreCompile"
DependsOnTargets="_WalterlvDemoEvaluateProperties">
<ItemGroup>
<_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
<Compile Include="@(_WalterlvDemoCompile)" />
</ItemGroup>
<Message Text="2 引入源代碼包中的所有源代碼:@(_WalterlvDemoCompile)" />
</Target>
</Project>
我們剛剛花了很大的篇幅教大家完成 props 和 targets 文件,那么這四個(gè)文件是做什么的呢?
如果安裝我們源代碼包的項(xiàng)目使用?TargetFramework?屬性寫目標(biāo)框架,那么 NuGet 會自動幫我們導(dǎo)入 build 文件夾中的兩個(gè)編譯文件。如果安裝我們源代碼包的項(xiàng)目使用?TargetFrameworks(注意復(fù)數(shù)形式)屬性寫目標(biāo)框架,那么 NuGet 會自動幫我們導(dǎo)入 buildMultiTargeting 文件夾中的兩個(gè)編譯文件。
如果你對這個(gè)屬性不熟悉,請回到第一步看我們一開始創(chuàng)建的代碼,你會看到這個(gè)屬性的設(shè)置的。如果還不清楚,請閱讀博客:
讓一個(gè) csproj 項(xiàng)目指定多個(gè)開發(fā)框架
也許你已經(jīng)從本文拷貝了很多代碼過去了,但直到目前我們還沒有看到這些代碼的任何效果,那么現(xiàn)在我們就可以來看看了。這可算是一個(gè)階段性成果呢!
先編譯生成一下我們一直在完善的項(xiàng)目,我們就可以在解決方案目錄的?bin\Debug目錄下找到一個(gè) NuGet 包。
現(xiàn)在,我們要打開這個(gè) NuGet 包看看里面的內(nèi)容。你需要先去應(yīng)用商店下載?NuGet Package Explorer,裝完之后你就可以開始直接雙擊 NuGet 包文件,也就是 nupkg 文件。現(xiàn)在我們雙擊打開看看。
我們的體驗(yàn)到此為止。如果你希望在真實(shí)的項(xiàng)目當(dāng)中測試,可以閱讀其他博客了解如何在本地測試 NuGet 包。
截至目前,我們只是在源代碼包中引入了 C# 代碼。如果我們需要加入到源代碼包中的代碼包含 WPF 的 XAML 文件,或者安裝我們源代碼包的目標(biāo)項(xiàng)目包含 WPF 的 XAML 文件,那么這個(gè) NuGet 源代碼包直接會導(dǎo)致無法編譯通過。至于原因,你需要閱讀我的另一篇博客來了解:
WPF 程序的編譯過程
即便你不懂 WPF 程序的編譯過程,你也可以繼續(xù)完成本文的所有內(nèi)容,但可能就不會明白為什么接下來我們要那樣去修改我們之前創(chuàng)建的文件。
接下來我們將修改這些文件:
build 文件夾中的 Package.props 文件
build 文件夾中的 Package.targets 文件
在這個(gè)文件中,我們將新增一個(gè)屬性?ShouldFixNuGetImportingBugForWpfProjects。這是我取的名字,意為“是否應(yīng)該修復(fù) WPF 項(xiàng)目中 NuGet 包自動導(dǎo)入的問題”。
我做一個(gè)開關(guān)的原因是懷疑我們需要針對 WPF 項(xiàng)目進(jìn)行特殊處理是 WPF 項(xiàng)目自身的 Bug,如果將來 WPF 修復(fù)了這個(gè) Bug,那么我們將可以直接通過此開關(guān)來關(guān)閉我們在這一節(jié)做的特殊處理。另外,后面我們將采用一些特別的手段來調(diào)試我們的 NuGet 源代碼包,在調(diào)試項(xiàng)目中我們也會將這個(gè)屬性設(shè)置為?False?以關(guān)閉 WPF 項(xiàng)目的特殊處理。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
++ <!-- 當(dāng)生成 WPF 臨時(shí)項(xiàng)目時(shí),不會自動 Import NuGet 中的 props 和 targets 文件,這使得在臨時(shí)項(xiàng)目中你現(xiàn)在看到的整個(gè)文件都不會參與編譯。
++ 然而,我們可以通過欺騙的方式在主項(xiàng)目中通過 _GeneratedCodeFiles 集合將需要編譯的文件傳遞到臨時(shí)項(xiàng)目中以間接參與編譯。
++ WPF 臨時(shí)項(xiàng)目不會 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
++ 所以我們通過一個(gè)屬性開關(guān) `ShouldFixNuGetImportingBugForWpfProjects` 來決定是否修復(fù)這個(gè)錯誤。-->
++ <ShouldFixNuGetImportingBugForWpfProjects Condition=" '$(ShouldFixNuGetImportingBugForWpfProjects)' == '' ">True</ShouldFixNuGetImportingBugForWpfProjects>
++ </PropertyGroup>
</Project>
請按照下面的差異說明來修改你的 Package.targets 文件。實(shí)際上我們幾乎刪除任何代碼,所以其實(shí)你可以將下面的所有內(nèi)容作為你的新的 Package.targets 中的內(nèi)容。
<Project><PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
++ <PropertyGroup>
++ <!-- 我們增加了一個(gè)屬性,用于處理 WPF 特殊項(xiàng)目的源代碼之前,確保我們已經(jīng)收集到所有需要引入的源代碼。 -->
++ <_WalterlvDemoImportInWpfTempProjectDependsOn>_WalterlvDemoIncludeSourceFiles</_WalterlvDemoImportInWpfTempProjectDependsOn>
++ </PropertyGroup>
<Target Name="_WalterlvDemoEvaluateProperties">
<PropertyGroup>
<_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
<_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
</PropertyGroup>
<Message Text="1. 初始化源代碼包的編譯屬性" />
</Target>
<!-- 引入 C# 源碼。 -->
<Target Name="_WalterlvDemoIncludeSourceFiles"
BeforeTargets="CoreCompile"
DependsOnTargets="_WalterlvDemoEvaluateProperties">
<ItemGroup>
<_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
++ <_WalterlvDemoAllCompile Include="@(_WalterlvDemoCompile)" />
<Compile Include="@(_WalterlvDemoCompile)" />
</ItemGroup>
-- <Message Text="2 引入源代碼包中的所有源代碼:@(_WalterlvDemoCompile)" />
++ <Message Text="2.1 引入源代碼包中的所有源代碼:@(_WalterlvDemoCompile)" />
</Target>
++ <!-- 引入 WPF 源碼。 -->
++ <Target Name="_WalterlvDemoIncludeWpfFiles"
++ BeforeTargets="MarkupCompilePass1"
++ DependsOnTargets="_WalterlvDemoEvaluateProperties">
++ <ItemGroup>
++ <_WalterlvDemoPage Include="$(_WalterlvDemoSourceFolder)**\*.xaml" />
++ <Page Include="@(_WalterlvDemoPage)" Link="%(_WalterlvDemoPage.FileName).xaml" />
++ </ItemGroup>
++ <Message Text="2.2 引用 WPF 相關(guān)源碼:@(_WalterlvDemoPage)" />
++ </Target>
++ <!-- 當(dāng)生成 WPF 臨時(shí)項(xiàng)目時(shí),不會自動 Import NuGet 中的 props 和 targets 文件,這使得在臨時(shí)項(xiàng)目中你現(xiàn)在看到的整個(gè)文件都不會參與編譯。
++ 然而,我們可以通過欺騙的方式在主項(xiàng)目中通過 _GeneratedCodeFiles 集合將需要編譯的文件傳遞到臨時(shí)項(xiàng)目中以間接參與編譯。
++ WPF 臨時(shí)項(xiàng)目不會 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
++ 所以我們通過一個(gè)屬性開關(guān) `ShouldFixNuGetImportingBugForWpfProjects` 來決定是否修復(fù)這個(gè)錯誤。-->
++ <Target Name="_WalterlvDemoImportInWpfTempProject"
++ AfterTargets="MarkupCompilePass1"
++ BeforeTargets="GenerateTemporaryTargetAssembly"
++ DependsOnTargets="$(_WalterlvDemoImportInWpfTempProjectDependsOn)"
++ Condition=" '$(ShouldFixNuGetImportingBugForWpfProjects)' == 'True' ">
++ <ItemGroup>
++ <_GeneratedCodeFiles Include="@(_WalterlvDemoAllCompile)" />
++ </ItemGroup>
++ <Message Text="3. 正在欺騙臨時(shí)項(xiàng)目,誤以為此 NuGet 包中的文件是 XAML 編譯后的中間代碼:@(_WalterlvDemoAllCompile)" />
++ </Target>
</Project>
我們增加了?_WalterlvDemoImportInWpfTempProjectDependsOn?屬性,這個(gè)屬性里面將填寫一個(gè)到多個(gè)編譯目標(biāo)(Target)的名稱(多個(gè)用分號分隔),用于告知?_WalterlvDemoImportInWpfTempProject?這個(gè)編譯目標(biāo)在執(zhí)行之前必須確保執(zhí)行的依賴編譯目標(biāo)。而我們目前的依賴目標(biāo)只有一個(gè),就是?_WalterlvDemoIncludeSourceFiles?這個(gè)引入 C# 源代碼的編譯目標(biāo)。如果你有其他考慮有引入更多 C# 源代碼的編譯目標(biāo),則需要把他們都加上(當(dāng)然本文是不需要的)。為此,我還新增了一個(gè)?_WalterlvDemoAllCompile?集合,如果存在多個(gè)依賴的編譯目標(biāo)會引入 C# 源代碼,則需要像?_WalterlvDemoIncludeSourceFiles一樣,將他們都加入到?Compile?的同時(shí)也加入到?_WalterlvDemoAllCompile?集合中。
為什么可能有多個(gè)引入 C# 源代碼的編譯目標(biāo)?因?yàn)楸疚奈覀冎豢紤]了引入我們提前準(zhǔn)備好的源代碼放入源代碼包中,而我們提到過可能涉及到動態(tài)生成 C# 源代碼的需求。如果你有一兩個(gè)編譯目標(biāo)會動態(tài)生成一些 C# 源代碼并將其加入到?Compile?集合中,那么請將這個(gè)編譯目標(biāo)的名稱加入到?_WalterlvDemoImportInWpfTempProjectDependsOn?屬性(注意多個(gè)用分號分隔),同時(shí)將集合也引入一份到?_WalterlvDemoAllCompile?中。
_WalterlvDemoIncludeWpfFiles?這個(gè)編譯目標(biāo)的作用是引入 WPF 的 XAML 文件,這很容易理解,畢竟我們的源代碼中包含 WPF 相關(guān)的文件。
請?zhí)貏e注意:
我們加了一個(gè)?Link?屬性,并且將其指定為?%(_WalterlvDemoPage.FileName).xaml。這意味著我們會把所有的 XAML 文件都當(dāng)作在項(xiàng)目根目錄中生成,如果你在其他的項(xiàng)目中用到了相對或絕對的 XAML 文件的路徑,這顯然會改變路徑。但是,我們沒有其他的方法來根據(jù) XAML 文件所在的目錄層級來自定指定?Link?屬性讓其在正確的層級上,所以這里才寫死在根目錄中。
如果要解決這個(gè)問題,我們就需要在生成 NuGet 包之前生成此項(xiàng)目中所有 XAML 文件的正確的?Link?屬性(例如改為?Views\%(_WalterlvDemoPage.FileName).xaml),這意味著需要在此項(xiàng)目編譯期間執(zhí)行一段代碼,把 Package.targets 文件中為所有的 XAML 文件生成正確的?Link?屬性。本文暫時(shí)不考慮這個(gè)問題,但你可以參考?dotnet-campus/SourceYard?項(xiàng)目來了解如何動態(tài)生成?Link。
我們使用了?_WalterlvDemoPage?集合中轉(zhuǎn)地存了 XAML 文件,這是必要的。因?yàn)檫@樣才能正確通過?%?符號獲取到?FileName?屬性。
而?_WalterlvDemoImportInWpfTempProject?這個(gè)編譯目標(biāo)就不那么好理解了,而這個(gè)也是完美支持 WPF 項(xiàng)目源代碼包的關(guān)鍵編譯目標(biāo)!這個(gè)編譯目標(biāo)指定在?MarkupCompilePass1?之后,GenerateTemporaryTargetAssembly?之前執(zhí)行。GenerateTemporaryTargetAssembly?編譯目標(biāo)的作用是生成一個(gè)臨時(shí)的項(xiàng)目,用于讓 WPF 的 XAML 文件能夠依賴同項(xiàng)目的 .NET 類型而編譯。然而此臨時(shí)項(xiàng)目編譯期間是不會導(dǎo)入任何 NuGet 的 props 或 targets 文件的,這意味著我們特別添加的所有 C# 源代碼在這個(gè)臨時(shí)項(xiàng)目當(dāng)中都是不存在的——如果項(xiàng)目使用到了我們源代碼包中的源代碼,那么必然因?yàn)轭愋筒淮嬖诙鵁o法編譯通過——臨時(shí)項(xiàng)目沒有編譯通過,那么整個(gè)項(xiàng)目也就無法編譯通過。但是,我們通過在?MarkupCompilePass1?和?GenerateTemporaryTargetAssembly?之間將我們源代碼包中的所有源代碼加入到?_GeneratedCodeFiles?集合中,即可將這些文件加入到臨時(shí)項(xiàng)目中一起編譯。而原本?_GeneratedCodeFiles?集合中是什么呢?就是大家熟悉的 XAML 轉(zhuǎn)換而成的?xxx.g.cs?文件。
現(xiàn)在我們再次編譯這個(gè)項(xiàng)目,你將得到一個(gè)支持 WPF 項(xiàng)目的 NuGet 源代碼包。
至此,我們已經(jīng)完成了編寫一個(gè) NuGet 源代碼包所需的全部源碼。接下來你可以在項(xiàng)目中添加更多的源代碼,這樣打出來的源代碼包也將包含更多源代碼。由于我們將將 XAML 文件都通過?Link?屬性指定到根目錄了,所以如果你需要添加 XAML 文件,你將只能添加到我們項(xiàng)目中的?Assets\src?目錄下,除非做?dotnet-campus/SourceYard?中類似的動態(tài)?Link?生成的處理,或者在 Package.targets 文件中手工為每一個(gè) XAML 編寫一個(gè)特別的?Link?屬性。
另外,在不改變我們整體項(xiàng)目結(jié)構(gòu)的情況下,你也可以任意添加 WPF 所需的圖片資源等。但也需要在 Package.targets 中添加額外的?Resource?引用。如果沒有?dotnet-campus/SourceYard?的自動生成代碼,你可能也需要手工編寫?Resource。
接下來我會貼出更復(fù)雜的代碼,用于處理更復(fù)雜的源代碼包的場景。
更復(fù)雜源代碼包的項(xiàng)目組織形式會是下面這樣圖這樣:
我們在 Assets 文件夾中新增了一個(gè) assets 文件夾。由于資源在此項(xiàng)目中的路徑必須和安裝后的目標(biāo)項(xiàng)目中一樣才可以正確用 Uri 的方式使用資源,所以我們在項(xiàng)目文件 csproj 和編譯文件 Package.targets 中都對這兩個(gè)文件設(shè)置了?Link?到同一個(gè)文件夾中,這樣才可以確保兩邊都能正常使用。
我們在 src 文件夾的不同子文件夾中創(chuàng)建了 XAML 文件。按照我們前面的說法,我們也需要像資源文件一樣正確在 Package.targets 中設(shè)置 Link 才可以確保 Uri 是一致的。注意,我們接下來的源代碼中沒有在項(xiàng)目文件中設(shè)置 Link,原則上也是需要設(shè)置的,就像資源一樣,這樣才可以確保此項(xiàng)目和安裝此 NuGet 包中的目標(biāo)項(xiàng)目具有相同的 XAML Uri。此例子只是因?yàn)闆]有代碼使用到了 XAML 文件的路徑,所以才能得以幸免。
我們還利用了 tools 文件夾。我們在項(xiàng)目文件的末尾將輸出文件拷貝到了 tools 目錄下,這樣,我們項(xiàng)目的 Assets 文件夾幾乎與最終的 NuGet 包的文件夾結(jié)構(gòu)一模一樣,非常利于調(diào)試。但為了防止將生成的文件上傳到版本管理,我在 tools 中添加了 .gitignore 文件:
-- <Project Sdk="Microsoft.NET.Sdk">++ <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
++ <UseWpf>True</UseWpf>
<!-- 要求此項(xiàng)目編譯時(shí)要生成一個(gè) NuGet 包。-->
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- 這里為了方便,我將 NuGet 包的輸出路徑設(shè)置在了解決方案根目錄的 bin 文件夾下,而不是項(xiàng)目的 bin 文件夾下。-->
<PackageOutputPath>..\bin\$(Configuration)</PackageOutputPath>
<!-- 創(chuàng)建 NuGet 包時(shí),項(xiàng)目的輸出文件對應(yīng)到 NuGet 包的 tools 文件夾,這可以避免目標(biāo)項(xiàng)目引用我們的 NuGet 包的輸出文件。
同時(shí),如果將來我們準(zhǔn)備動態(tài)生成源代碼,而不只是引入靜態(tài)源代碼,還可以有機(jī)會運(yùn)行我們 Program 中的 Main 函數(shù)。-->
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<!-- 此包將不會傳遞依賴。意味著如果目標(biāo)項(xiàng)目安裝了此 NuGet 包,那么安裝目標(biāo)項(xiàng)目包的項(xiàng)目不會間接安裝此 NuGet 包。-->
<DevelopmentDependency>true</DevelopmentDependency>
<!-- 包的版本號,我們設(shè)成了一個(gè)預(yù)覽版;當(dāng)然你也可以設(shè)置為正式版,即沒有后面的 -alpha 后綴。-->
<Version>0.1.0-alpha</Version>
<!-- 設(shè)置包的作者。在上傳到 nuget.org 之后,如果作者名與 nuget.org 上的賬號名相同,其他人瀏覽包是可以直接點(diǎn)擊鏈接看作者頁面。-->
<Authors>walterlv</Authors>
<!-- 設(shè)置包的組織名稱。我當(dāng)然寫成我所在的組織 dotnet 職業(yè)技術(shù)學(xué)院啦。-->
<Company>dotnet-campus</Company>
</PropertyGroup>
++ <!-- 我們添加的其他資源需要在這里 Link 到一個(gè)統(tǒng)一的目錄下,以便在此項(xiàng)目和安裝 NuGet 包的目標(biāo)項(xiàng)目中可以用同樣的 Uri 使用。 -->
++ <ItemGroup>
++ <Resource Include="Assets\assets\Icon.ico" Link="Assets\Icon.ico" Visible="False" />
++ <Resource Include="Assets\assets\Background.png" Link="Assets\Background.png" Visible="False" />
++ </ItemGroup>
<!-- 在生成 NuGet 包之前,我們需要將我們項(xiàng)目中的文件夾結(jié)構(gòu)一一映射到 NuGet 包中。-->
<Target Name="IncludeAllDependencies" BeforeTargets="_GetPackageFiles">
<ItemGroup>
<!-- 將 Package.props / Package.targets 文件的名稱在 NuGet 包中改為需要的真正名稱。
因?yàn)?NuGet 包要自動導(dǎo)入 props 和 targets 文件,要求文件的名稱必須是 包名.props 和 包名.targets;
然而為了避免我們改包名的時(shí)候還要同步改四個(gè)文件的名稱,所以就在項(xiàng)目文件中動態(tài)生成。-->
<None Include="Assets\build\Package.props" Pack="True" PackagePath="build\$(PackageId).props" />
<None Include="Assets\build\Package.targets" Pack="True" PackagePath="build\$(PackageId).targets" />
<None Include="Assets\buildMultiTargeting\Package.props" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).props" />
<None Include="Assets\buildMultiTargeting\Package.targets" Pack="True" PackagePath="buildMultiTargeting\$(PackageId).targets" />
<!-- 我們將 src 目錄中的所有源代碼映射到 NuGet 包中的 src 目錄中。-->
<None Include="Assets\src\**" Pack="True" PackagePath="src" />
++ <!-- 我們將 assets 目錄中的所有源代碼映射到 NuGet 包中的 assets 目錄中。-->
++ <None Include="Assets\assets\**" Pack="True" PackagePath="assets" />
</ItemGroup>
</Target>
++ <!-- 在編譯結(jié)束后將生成的可執(zhí)行程序放到 Tools 文件夾中,使得 Assets 文件夾的目錄結(jié)構(gòu)與 NuGet 包非常相似,便于 Sample 項(xiàng)目進(jìn)行及時(shí)的 NuGet 包調(diào)試。 -->
++ <Target Name="_WalterlvDemoCopyOutputToDebuggableFolder" AfterTargets="AfterBuild">
++ <ItemGroup>
++ <_WalterlvDemoToCopiedFiles Include="$(OutputPath)**" />
++ </ItemGroup>
++ <Copy SourceFiles="@(_WalterlvDemoToCopiedFiles)" DestinationFolder="Assets\tools\$(TargetFramework)" />
++ </Target>
</Project>
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup>
<!-- 我們增加了一個(gè)屬性,用于處理 WPF 特殊項(xiàng)目的源代碼之前,確保我們已經(jīng)收集到所有需要引入的源代碼。 -->
<_WalterlvDemoImportInWpfTempProjectDependsOn>_WalterlvDemoIncludeSourceFiles</_WalterlvDemoImportInWpfTempProjectDependsOn>
</PropertyGroup>
<Target Name="_WalterlvDemoEvaluateProperties">
<PropertyGroup>
<_WalterlvDemoRoot>$(MSBuildThisFileDirectory)..\</_WalterlvDemoRoot>
<_WalterlvDemoSourceFolder>$(MSBuildThisFileDirectory)..\src\</_WalterlvDemoSourceFolder>
</PropertyGroup>
<Message Text="1. 初始化源代碼包的編譯屬性" />
</Target>
<!-- 引入主要的 C# 源碼。 -->
<Target Name="_WalterlvDemoIncludeSourceFiles"
BeforeTargets="CoreCompile"
DependsOnTargets="_WalterlvDemoEvaluateProperties">
<ItemGroup>
<_WalterlvDemoCompile Include="$(_WalterlvDemoSourceFolder)**\*.cs" />
<_WalterlvDemoAllCompile Include="@(_WalterlvDemoCompile)" />
<Compile Include="@(_WalterlvDemoCompile)" />
</ItemGroup>
<Message Text="2.1 引入源代碼包中的所有源代碼:@(_WalterlvDemoCompile)" />
</Target>
<!-- 引入 WPF 源碼。 -->
<Target Name="_WalterlvDemoIncludeWpfFiles"
BeforeTargets="MarkupCompilePass1"
DependsOnTargets="_WalterlvDemoEvaluateProperties">
<ItemGroup>
-- <_WalterlvDemoPage Include="$(_WalterlvDemoSourceFolder)**\*.xaml" />
-- <Page Include="@(_WalterlvDemoPage)" Link="Views\%(_WalterlvDemoPage.FileName).xaml" />
++ <_WalterlvDemoRootPage Include="$(_WalterlvDemoSourceFolder)FooView.xaml" />
++ <Page Include="@(_WalterlvDemoRootPage)" Link="Views\%(_WalterlvDemoRootPage.FileName).xaml" />
++ <_WalterlvDemoThemesPage Include="$(_WalterlvDemoSourceFolder)Themes\Walterlv.Windows.xaml" />
++ <Page Include="@(_WalterlvDemoThemesPage)" Link="Views\%(_WalterlvDemoThemesPage.FileName).xaml" />
++ <_WalterlvDemoIcoResource Include="$(_WalterlvDemoRoot)assets\*.ico" />
++ <_WalterlvDemoPngResource Include="$(_WalterlvDemoRoot)assets\*.png" />
++ <Resource Include="@(_WalterlvDemoIcoResource)" Link="assets\%(_WalterlvDemoIcoResource.FileName).ico" />
++ <Resource Include="@(_WalterlvDemoPngResource)" Link="assets\%(_WalterlvDemoPngResource.FileName).png" />
</ItemGroup>
-- <Message Text="2.2 引用 WPF 相關(guān)源碼:@(_WalterlvDemoPage);@(_WalterlvDemoIcoResource);@(_WalterlvDemoPngResource)" />
++ <Message Text="2.2 引用 WPF 相關(guān)源碼:@(_WalterlvDemoRootPage);@(_WalterlvDemoThemesPage);@(_WalterlvDemoIcoResource);@(_WalterlvDemoPngResource)" />
</Target>
<!-- 當(dāng)生成 WPF 臨時(shí)項(xiàng)目時(shí),不會自動 Import NuGet 中的 props 和 targets 文件,這使得在臨時(shí)項(xiàng)目中你現(xiàn)在看到的整個(gè)文件都不會參與編譯。
然而,我們可以通過欺騙的方式在主項(xiàng)目中通過 _GeneratedCodeFiles 集合將需要編譯的文件傳遞到臨時(shí)項(xiàng)目中以間接參與編譯。
WPF 臨時(shí)項(xiàng)目不會 Import NuGet 中的 props 和 targets 可能是 WPF 的 Bug,也可能是刻意如此。
所以我們通過一個(gè)屬性開關(guān) `ShouldFixNuGetImportingBugForWpfProjects` 來決定是否修復(fù)這個(gè)錯誤。-->
<Target Name="_WalterlvDemoImportInWpfTempProject"
AfterTargets="MarkupCompilePass1"
BeforeTargets="GenerateTemporaryTargetAssembly"
DependsOnTargets="$(_WalterlvDemoImportInWpfTempProjectDependsOn)"
Condition=" '$(ShouldFixNuGetImportingBugForWpfProjects)' == 'True' ">
<ItemGroup>
<_GeneratedCodeFiles Include="@(_WalterlvDemoAllCompile)" />
</ItemGroup>
<Message Text="3. 正在欺騙臨時(shí)項(xiàng)目,誤以為此 NuGet 包中的文件是 XAML 編譯后的中間代碼:@(_WalterlvDemoAllCompile)" />
</Target>
</Project>
本文涉及到的所有代碼均已開源到:
walterlv.demo/Walterlv.PackageDemo at master · walterlv/walterlv.demo
本文服務(wù)于開源項(xiàng)目 SourceYard,為其提供支持 WPF 項(xiàng)目的解決方案。dotnet-campus/SourceYard: Add a NuGet package only for dll reference? By using dotnetCampus.SourceYard, you can pack a NuGet package with source code. By installing the new source code package, all source codes behaviors just like it is in your project.
更多制作源代碼包的博客可以閱讀。從簡單到復(fù)雜的順序:
將 .NET Core 項(xiàng)目打一個(gè)最簡單的 NuGet 源碼包,安裝此包就像直接把源碼放進(jìn)項(xiàng)目一樣 - 呂毅
Roslyn 如何基于 Microsoft.NET.Sdk 制作源代碼包 - 林德熙
制作通過 NuGet 分發(fā)的源代碼包時(shí),如果目標(biāo)項(xiàng)目是 WPF 則會出現(xiàn)一些問題(探索篇,含解決方案) - 呂毅
SourceYard 制作源代碼包 - 林德熙
原文地址:https://walterlv.com/post/build-source-code-package-for-wpf-projects.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的从零开始制作 NuGet 源代码包(全面支持 .NET Core / .NET Framework / WPF 项目)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 带你了解C#每个版本新特性
- 下一篇: .NET 使用 ILRepack 合并多