本文整理 dotnet 打包 CBB 组件为 NuGet 包时可以使用的配置的各个属性

本文将会持续更新,可以通过搜 《dotnet 打包 NuGet 的配置属性大全整理 林德熙》 找到我主站的博客,避免各个备份地址陈旧的内容误导

本文更新于:2024.03.23

如更新时间距离当前阅读时间过远,则表示可能你阅读的是转发的或转载的文章,推荐去到我主站的博客,了解更新的知识

基础知识

在编辑 NuGet 的打包配置属性之前,我期望你了解一些基础知识。了解这部分知识减少一些奇怪的问题和奇怪的决策

基本上使用 dotnet 打包 NuGet 包时,都是通过配置 csproj 项目文件来完成实现功能。其中 csproj 文件有多个版本,当前主力推荐使用的是 SDK 风格的 csproj 格式。可参阅此博客提供的方法将旧的 csproj 格式升级到 SDK 风格的 csproj 格式

在 csproj 项目文件里面,支持编辑内容,在 PropertyGroup 标签里面添加属性值。例如加入 TargetFramework 属性之后的 csproj 的代码大概如下

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>

</Project>

更多关于 csproj 项目文件格式,请参阅 理解 C# 项目 csproj 文件格式的本质和编译流程 - walterlv

一些前置知识博客:

相关博客

以下是我记录的一些工具博客,便于查阅

CSPROJ 系属性

PackageId

包的 Id 属性,这是不区分大小写的包标识符,该标识符在 nuget.org 或包所在的私有的 NuGet 源中必须是唯一的。不写默认等同于 AssemblyName 程序集名,即 $(AssemblyName) 的值。此 ID 不能包含对于URL无效的空格或字符,且通常遵循.NET命名空间规则

  <PropertyGroup>
    <PackageId>Foo.Fx</PackageId>
  </PropertyGroup>

更多 Id 相关,请参阅 ID Prefix Reservation Microsoft Learn

Title

包的人类阅读友好标题,通常在UI显示中使用,如在 nuget.org 和 Visual Studio 中的包管理器上显示给开发者

默认不写等同于 PackageId 内容

  <PropertyGroup>
    <Title>标题内容</Title>
  </PropertyGroup>

由于存在语言文化相关问题,如果是公开发布的包且期望国际上的朋友使用,则不建议写入中文。此标题限制为 256 个字符长度

PackageVersion

包版本号,默认不写为 1.0.0 版本号。可使用语义版本号,详细请参阅 语义版本号(Semantic Versioning) - walterlv

  <PropertyGroup>
    <PackageVersion>1.0.0</PackageVersion>
  </PropertyGroup>

与此相关的还有 Version 属性,大部分情况下都采用 Version 属性。此 Version 属性将会被 PackageVersion 和程序集版本号等所使用,用途较广。除非确实不想要让包版本号确实和程序集版本号相同,否则推荐使用 Version 属性。如果没有明确设置 PackageVersion 属性,将会使用已设置的 Version 属性

  <PropertyGroup>
    <Version>1.0.0</Version>
  </PropertyGroup>

默认 dotnet 规范请参阅: NuGet 包版本引用 Microsoft Learn

如项目没有配置 AssemblyVersion 程序集版本号和 FileVersion 文件版本号,那么默认将使用此 Version 内容作为版本号

如期望自动生成版本号,请参阅 VisualStudio 2017 项目格式 自动生成版本号

Owners

此包的拥有者,可以不同于作者。大部分作用是在开源组织上,由开源组织拥有此包,然后由具体开发者作为作者。这里的拥有者是可以有多个,推荐多个之间使用分号分割。大部分情况下 Owners 拥有者将和 Company 公司相同

<Project>
  <PropertyGroup>
    <Company>dotnet-campus</Company>
    <Owners>$(Company)</Owners>
  </PropertyGroup>
</Project>

Company

公司,也可以当成是组织。一般写全商标注册的公司信息。对外可以使用 Owners 写简称

Authors

作者,表示这个包由谁制作。作者不一定拥有此包的所有权,和 Owners 不相同。例如公司雇用你打工,你帮助公司发布的包,自然此包的 所有权 就在公司上,而你自己就是此包的作者

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <PackageId>ClassLibDotNetStandard</PackageId>
    <Version>1.0.0</Version>
    <Authors>your_name</Authors>
    <Company>your_company</Company>
  </PropertyGroup>
</Project>

版权信息,官方推荐的格式是 "Copyright (c) <name/company> <year> 的格式。正经的包一般都会如此遵守。年份上是可以写范围或固定某个年

<Project>
  <PropertyGroup>
    <Copyright>Copyright (c) dotnet-campus 2020-2023</Copyright>
  </PropertyGroup>
</Project>

详细请参阅 Package authoring best practices Microsoft Learn

如果想要保持每次打包都是最新年份,不用每一年都手动更新,那可以使用 $([System.DateTime]::Now.ToString(yyyy)) 来表示当前年份,如以下代码

<Copyright>Copyright (c) dotnet-campus 2020-$([System.DateTime]::Now.ToString(`yyyy`))</Copyright>

加入以上代码之后,即可每次打包都设置版权信息为当前的年份

CopyrightSlim

只是 Copyright 的较短版本,默认不设置将采用 Copyright 的值

PackageLicenseExpression

许可证信息,可以在 Copyright 不存在时勉强当成版权信息。可以打入的是当前的包使用的是什么协议进行许可,比如当前是给一个 MIT 协议开源的仓库进行打包的,可以使用如下设置当前的 NuGet 包使用最友好的 MIT 协议

<PropertyGroup>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>

如果这个包是一个混合包,包含多个协议,比如 MIT 和 Apache-2.0 协议,可以这样写

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <PackageLicenseExpression>MIT OR Apache-2.0</PackageLicenseExpression>
  </PropertyGroup>
</Project>

PackageLicenseFile

以上的 PackageLicenseExpression 是一个简单的写法,适合用在一些明确当前使用类型的许可证。但如果自己的许可证有些特殊,比如是公司的法务写的许可证,需要特殊的许可证文件,那可以使用 PackageLicenseFile 属性。通过 PackageLicenseFile 属性设置采用打入到 NuGet 包的哪个文件作为许可证文件

<PropertyGroup>
    <PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
</PropertyGroup>

<ItemGroup>
    <None Include="..\LICENSE.txt" Pack="true" PackagePath=""/>
</ItemGroup>

这里需要明确的是 PackageLicenseExpression 和 PackageLicenseFile 以及不被推荐使用的 PackageLicenseUrl 三个只能同时存在其中一个

额外说明的是,对于许多仓库的 LICENSE 许可证文件来说,都是没有带后缀名的。由于历史原因,在 NuGet 里面对于没有后缀名的都是会被当成是文件夹。为了能够正确的进行打包,就必须带上 PackagePath 属性,如下面代码例子

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFrameworks>netstandard2.0</TargetFrameworks>
    <PackageLicenseFile>LICENSE</PackageLicenseFile>
  </PropertyGroup>

  <ItemGroup>
    <None Include="LICENSE" Pack="true" Visible="false" PackagePath=""/>
  </ItemGroup>
  
</Project>

以上的代码里面核心的逻辑在于 PackagePath="" 设置了无后缀名的 LICENSE 不是文件

上面代码是从 https://github.com/NuGet/Samples/blob/ec30a2b7c54c2d09e5a476444a2c7a8f2f289d49/PackageLicenseFileExtensionlessExample/PackageLicenseFileExtensionlessExample.csproj#L1 拷贝的

PackageReadmeFile

打包到 NuGet 包里面的自述文件,一般可以将仓库里面的 README.md 文件打进来,如以下例子

<PropertyGroup>
    ...
    <PackageReadmeFile>README.md</PackageReadmeFile>
    ...
</PropertyGroup>

<ItemGroup>
    ...
    <None Include="..\..\README.md" Link="README.md" Pack="True" PackagePath="\"/>
    ...
</ItemGroup>

这里包括两个方面的内容,第一个是在 PropertyGroup 里面使用 PackageReadmeFile 属性标明 README.md 文件,然后在 ItemGroup 里面设置打包到 NuGet 包里面的是哪个文件当成 README.md 文件

常见写法也写在 Directory.Build.props 里面,这样可以复用仓库的 README.md 文件,大概的代码如下

  <PropertyGroup>
    <SlnDir>$(MSBuildThisFileDirectory)</SlnDir>
  </PropertyGroup>

  <!-- 以下是打 NuGet 包相关辅助方法 -->
  <PropertyGroup>
    <PackageReadmeFile>README.md</PackageReadmeFile>
  </PropertyGroup>
  <ItemGroup>
    <!-- 嵌入 README 文件 -->
    <None Include="$(SlnDir)README.md" Pack="true" PackagePath="\" Visible="false"/>
  </ItemGroup>

以上代码添加的 Visible="false" 用于让 README.md 文件不要在项目里面显示

PackageIcon

包的图标,详细请看 NuGet 如何设置图标

现在推荐将图标作为文件放入到包里面,而不是使用外链图片下载地址,解决一些奇怪的地方无法拉到包或泄露隐私

大概的例子如下

<PropertyGroup>
    ...
    <PackageIcon>Icon.png</PackageIcon>
    ...
</PropertyGroup>

<ItemGroup>
    ...
    <None Include="..\Images\Icon.png" Pack="true" PackagePath="\"/>
    ...
</ItemGroup>

打包控制

GeneratePackageOnBuild

生成的时候,构建出 NuGet 包。没有开启此属性时,是需要有额外的打包过程,例如 dotnet pack 或者在 VisuslStudio 里右击打包。开启此属性之后,每次构建都会输出 NuGet 包。实际测试是开启此属性对生成的性能影响很小

  <PropertyGroup>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
  </PropertyGroup>

IsPackable

用于设置项目是否可以被打包,默认是 true 表示项目可以打包,如果设置为 false 禁用则不打包 NuGet 包。可以用在如单元测试等项目,设置这些项目不要输出 NuGet 包

  <PropertyGroup>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

注:对于 ASP.NET Core 应用项目,在 SDK 里面默认设置了 IsPackable 为 false 的值。也就是说在 ASP.NET Core 应用项目上默认 IsPackable 就是 false 的值

对于单元测试项目,还会额外配置 IsTestProject 属性

  <PropertyGroup>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestProject>
  </PropertyGroup>

GenerateDocumentationFile

设置是否在生成的时候,同时生成注释 XML 文件。此属性设置之后,将会自动将注释 XML 文件输出到 NuGet 里

  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

在 dotnet 里面,代码上的公开成员,如公开的方法公开的属性等,的注释是存放在一个和程序集同名后缀为 XML 的文件里面。开启 GenerateDocumentationFile 属性,即可在生成过程,生成注释 XML 文件。在拥有此 XML 文件,即可让 VisualStudio 等 IDE 可以自动提示引用库的代码注释,方便让开发者了解调用库的各个成员的含义。进行 NuGet 发布的时候,将注释的 XML 文件带到 NuGet 包里面,可以方便让引用此 NuGet 包的项目获取到库的代码注释

EmbedAllSources

将源代码嵌入到 PDB 文件里面,此时构建时生成的 PDB 文件里面将包含项目的所有生成相关的源代码。如此可以方便在发布给其他开发者使用时,其他开发者在调试时可以获取到只读的源代码,从而让其他开发者更好进行调试

  <PropertyGroup>
    <!-- 嵌入源代码到符号文件,方便调试 -->
    <EmbedAllSources>true</EmbedAllSources>
  </PropertyGroup>

默认是 false 不将源代码嵌入到符号文件。推荐在源代码无需保护的项目,如内部开源项目或外部开源项目,以及 PDB 不对外发布的项目里,设置此属性为 true 从而将源代码嵌入到 PDB 文件里面,方便调试

详细请参阅 Roslyn 通过 EmbedAllSources 将源代码嵌入到 PDB 符号文件中方便开发者调试

设置 EmbedAllSources 嵌入源代码到符号文件时,可能会遇到打本地包引用的时候,无法找到本地磁盘路径的代码文件,而是会显示进入嵌入到符号文件的代码文件,导致调试困难。可通过添加判断代码,仅在非 Debug 模式下才嵌入

    <!-- 不要在 debug 开启 EmbedAllSources 或 EmbedUntrackedSources:
         1. NuGet 包会提示包含未追踪的源,但实际列出的未追踪的源是空的(所以其实都已经追踪了?)
         2. 如果采用此属性将源嵌入,会导致 JetBrians Rider 调试时使用嵌入的源而不是仓库中的源,这会导致无法使用断点等一系列依赖于 pdb 源的功能。-->
    <EmbedAllSources Condition="'$(Configuration)' != 'Debug'">true</EmbedAllSources>

AllowedOutputExtensionsInPackageBuildOutputFolder

允许哪些扩展名的输出文件带入到 NuGet 包里面

比如说最常用的是将 PDB 文件放入到 NuGet 里面,即可通过此属性设置输出文件里面的 pdb 文件需要被添加到包里面,如以下代码

  <PropertyGroup>
    <!-- 输出 pdb 文件 NuGet 包 -->
    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
  </PropertyGroup>

此属性只能决定哪些后缀名的文件会打包到 NuGet 包里面,不合适用来决定某些文件需要打包。如果需要特殊指定某些文件,请参阅 Roslyn 打包自定义的文件到 NuGet 包

虽然将 PDB 打包到 NuGet 包里面,有些版本的 VisualStudio 不会自动拷贝 PDB 文件,解决方法请看 修复 VisualStudio 构建时没有将 NuGet 的 PDB 符号文件拷贝到输出文件夹

IncludeSymbols

设置是否输出符号文件,用于制作符号包,通常和 SymbolPackageFormat 配合使用

  <PropertyGroup>
    <!-- 输出符号文件 -->
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>

SymbolPackageFormat

输出的符号文件的格式,符号文件有两个输出格式,文件名规范不相同

  • .symbols.nupkg : 默认的文件后缀。兼容性好,但是存在冲突。比如真有一个叫 Xx.Symbols 项目就凉凉。此格式已被淘汰
  • .snupkg : 专门定义的符号包格式,可以只包含符号 PDB 文件
  <PropertyGroup>
    <!-- 输出符号文件 -->
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>

官方文档: How to publish NuGet symbol packages using the new symbol package format ‘.snupkg’ Microsoft Learn

使用 .snupkg 格式对应在 .nuspec 的配置是

<packageTypes>
   <packageType name="SymbolsPackage"/>
</packageTypes>

ContinuousIntegrationBuild

这个属性是比较复杂的,用于 CI 的确定性构建,默认不开。和 Roslyn 的确定性构建 使用的 Deterministic 属性是不相同的两个概念。此 ContinuousIntegrationBuild 是为了 SourceLink 的功能而引入的。此 SourceLink 功能是在 PDB 符号文件里面,嵌入源代码的下载地址,方便调试的时候获取到源代码,详细请看 dotnet 使用 SourceLink 将 NuGet 链接源代码到 GitHub 等仓库

大家都知道,在 PDB 符号文件里面包含的是源代码的绝对路径,在 CI CD 打包服务器上的绝对路径是大部分开发者所不期望的,于是才有了 ContinuousIntegrationBuild 确定性构建的存在。用来实现无论在哪台打包服务器上以及在任何时候打包都会输出相同

这个 ContinuousIntegrationBuild 属性在本机构建调试时,都不应该设置为 true 的值。否则将会丢失本地构建的绝对路径,从而难以自动跳转源代码。只有在 CI 服务器上构建才需要设置

大部分时候设置时,都需要配合设置 SourceRoot 属性

  <ItemGroup>
    <SourceRoot Include="$(MSBuildThisFileDirectory)"/>
  </ItemGroup>

以上代码是推荐放在 Directory.Build.props 文件里面,详细关于 Directory.Build.props 请参阅 Roslyn 使用 Directory.Build.props 文件定义编译Roslyn 使用 Directory.Build.props 管理多个项目配置 博客

例如在 GitHub 的 CI 构建时,自动设置此属性

  <PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
  </PropertyGroup>
  
  <ItemGroup>
    <SourceRoot Include="$(MSBuildThisFileDirectory)"/>
  </ItemGroup>

详细请参阅

Producing Packages with Source Link - .NET Blog

.NET 5 Deterministic Builds & Source Linking Mitchel Sellers

Deterministic Builds in C#

dotnet/reproducible-builds: Contains the DotNet.ReproducibleBuilds package

IncludeBuildOutput

默认是 true 的值,如果指定为 false 那么项目编译输出的 dll 文件将不会被打包到 NuGet 包中。可以用来配置将项目构建输出的 DLL 不要自动打入到 nupkg 的 lib 文件夹下

这个属性一般会用在分析器项目或者是工具 NuGet 包里

一般和 <IsTool>true</IsTool> 进行二选一使用。使用 <IncludeBuildOutput>false</IncludeBuildOutput> 能够实现更高的定制化,与 IsTool 不同之处在于 IsTool 属性设置为 true 的值将会让输出 NuGet 包的 tools 文件夹里面

DevelopmentDependency

这是一个仅开发阶段使用的 NuGet 包,默认是 false 的值。如果设置为 true 即可在安装此 NuGet 包后自动配置为不传递依赖。可用在工具类型的 NuGet 包上,让工具包只对当前安装的项目生效,不会传递给所引用的项目

详细请参阅 帮助官方 NuGet 解掉 Bug,制作绝对不会传递依赖的 NuGet 包 - walterlv

已知属性

以下内容是对 项目文件中的已知属性(知道了这些,就不会随便在 csproj 中写死常量啦) - walterlv 的更多补充

NuGetPackageRoot

表示的是当前的 NuGet 的 Package 文件夹路径,可以用来拼接获取到对应的 NuGet 包文件路径。一般路径是 C:\Users\【用户名】\.nuget\packages 文件夹

拼接 NuGet 包路径的例子如下

    <None Include="$(NuGetPackageRoot)\sharpziplib\1.2.0\lib\netstandard2.0\ICSharpCode.SharpZipLib.dll" Link="ICSharpCode.SharpZipLib.dll">
      <Pack>true</Pack>
      <PackagePath>tools\netstandard2.0\</PackagePath>
    </None>

常写的代码片

输出和包含 targets 文件

<None Include="Build\package.targets" Pack="True" PackagePath="\build\$(PackageId).targets" />
<None Include="Build\package.props" Pack="True" PackagePath="\build\$(PackageId).props" />

详细请参阅 Roslyn 打包自定义的文件到 NuGet 包

判断框架和平台

  <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard1.5|AnyCPU'">
    <PlatformTarget>AnyCPU</PlatformTarget>
  </PropertyGroup>

更多判断逻辑请参阅 msbuild 项目文件常用判断条件

Target 时机

动态加入打包到 NuGet 包的文件时机

可在 _GetPackageFiles 这个 Target 前执行,在此执行加入 Nuget 打包文件才是有效,在这个时机之后将会无效,如以下代码

  <ItemGroup>
    <None Include="build\package.targets" Pack="True" PackagePath="\build\$(PackageId).targets" />
  </ItemGroup>

  <Target Name="FooIncludeAllDependencies" BeforeTargets="_GetPackageFiles">
    <ItemGroup>
      <None Include="..\Foo\Foo.dll" Pack="True" PackagePath="analyzers\dotnet\cs" />
    </ItemGroup>
  </Target>

以上代码的两个加入打包的文件都会成功都被加入打包。更多请参阅 Roslyn 打包自定义的文件到 NuGet 包

更多请看 msbuild Roslyn 行为详解

进行 Publish 发布之后的时机

<Project>
  <Target Name="Fxxxxx" AfterTargets="Publish">
    <Warning Text="PublishFolder=$([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(PublishDir)))"/>
  </Target>
</Project>

以上的 [MSBuild]::NormalizePath 作用和 Path.Combine 或 Path.Join 相同

实测需要使用 AfterTargets="Publish" 而不能使用 DependsOnTargets 方式

相关文档

msbuild Roslyn 行为详解

Roslyn 的确定性构建 - walterlv

如何创建一个基于命令行工具的跨平台的 NuGet 工具包 - walterlv

MSBuild properties for Microsoft.NET.Sdk - .NET Microsoft Learn

更多构建打包相关请看手把手教你写 Roslyn 修改编译


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/dotnet-%E6%89%93%E5%8C%85-NuGet-%E7%9A%84%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%A4%A7%E5%85%A8%E6%95%B4%E7%90%86.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利