名称: dotnet-nuget-包创作 描述: “创建NuGet包。SDK风格的csproj,源生成器,多目标框架,符号包,签名。” 用户可调用: false
dotnet-nuget-包创作
面向.NET库作者的NuGet包创作:SDK风格的.csproj包属性(PackageId、PackageTags、PackageReadmeFile、PackageLicenseExpression),使用analyzers/dotnet/cs/文件夹布局和buildTransitive目标的源生成器NuGet打包,多目标框架包,具有确定性构建的符号包(snupkg),包签名(使用证书的作者签名、仓库签名),包验证(EnablePackageValidation、用于API兼容性的Microsoft.DotNet.ApiCompat.Task),以及NuGet版本策略(SemVer 2.0、预发布后缀、NBGV集成)。
版本假设:.NET 8.0+ 基线。NuGet客户端随.NET 8+ SDK捆绑。Microsoft.DotNet.ApiCompat.Task 8.0+ 用于API兼容性验证。
范围
- SDK风格的csproj包属性和元数据
- 使用分析器文件夹布局的源生成器NuGet打包
- 多目标框架包和符号包(snupkg)
- 包签名(作者和仓库签名)
- 包验证(EnablePackageValidation、API兼容性)
- NuGet版本策略(SemVer 2.0、NBGV)
超出范围
- 中央包管理、SourceLink、nuget.config —— 参见[技能:dotnet-project-structure]
- CI/CD NuGet推送工作流 —— 参见[技能:dotnet-gha-publish]和[技能:dotnet-ado-publish]
- CLI工具打包和分发 —— 参见[技能:dotnet-cli-packaging]
- Roslyn分析器创作 —— 参见[技能:dotnet-roslyn-analyzers]
- 发布生命周期和NBGV设置 —— 参见[技能:dotnet-release-management]
交叉参考:[技能:dotnet-project-structure]用于CPM、SourceLink、nuget.config,[技能:dotnet-gha-publish]用于CI NuGet推送工作流,[技能:dotnet-ado-publish]用于ADO NuGet推送工作流,[技能:dotnet-cli-packaging]用于CLI工具分发格式,[技能:dotnet-csharp-source-generators]用于Roslyn源生成器创作,[技能:dotnet-release-management]用于发布生命周期和NBGV设置,[技能:dotnet-roslyn-analyzers]用于Roslyn分析器创作。
SDK风格包属性
每个NuGet包都从.csproj中的MSBuild属性开始。SDK风格的项目使用dotnet pack生成NuGet包——无需.nuspec文件。
基本包元数据
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>MyCompany.Widgets</PackageId>
<Version>1.0.0</Version>
<Authors>My Company</Authors>
<Description>用于管理小部件的库,支持流畅API。</Description>
<PackageTags>widgets;fluent;dotnet</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/mycompany/widgets</PackageProjectUrl>
<RepositoryUrl>https://github.com/mycompany/widgets</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<!-- 在nuget.org包页面上显示的README -->
<PackageReadmeFile>README.md</PackageReadmeFile>
<!-- 包图标(推荐128x128 PNG) -->
<PackageIcon>icon.png</PackageIcon>
<!-- 为IntelliSense生成XML文档 -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- 用于可重现性的确定性构建 -->
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
<!-- 在包中包含README和图标 -->
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>
属性参考
| 属性 | 用途 | 示例 |
|---|---|---|
PackageId |
nuget.org上的唯一包标识符 | MyCompany.Widgets |
Version |
SemVer 2.0版本 | 1.2.3-beta.1 |
Authors |
逗号分隔的作者名称 | Jane Doe, My Company |
Description |
nuget.org上的包描述 | 流畅小部件管理库 |
PackageTags |
分号分隔的搜索标签 | widgets;fluent;dotnet |
PackageLicenseExpression |
SPDX许可证标识符 | MIT, Apache-2.0 |
PackageLicenseFile |
许可证文件(替代表达式) | LICENSE.txt |
PackageReadmeFile |
在nuget.org上显示的Markdown自述文件 | README.md |
PackageIcon |
包图标文件名 | icon.png |
PackageProjectUrl |
项目主页URL | https://github.com/mycompany/widgets |
PackageReleaseNotes |
此版本的发布说明 | 添加了小部件缓存支持 |
Copyright |
版权声明 | Copyright 2024 My Company |
RepositoryUrl |
源代码仓库URL | https://github.com/mycompany/widgets |
RepositoryType |
仓库类型 | git |
Directory.Build.props用于共享元数据
对于多项目仓库,在Directory.Build.props中设置公共属性:
<!-- Directory.Build.props(仓库根目录) -->
<Project>
<PropertyGroup>
<Authors>My Company</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/mycompany/widgets</PackageProjectUrl>
<RepositoryUrl>https://github.com/mycompany/widgets</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Copyright>Copyright 2024 My Company</Copyright>
</PropertyGroup>
</Project>
各个.csproj文件然后只设置包特定属性(PackageId、Description、PackageTags)。
源生成器NuGet打包
源生成器和分析器需要特定的NuGet包布局。生成器DLL必须放置在analyzers/dotnet/cs/文件夹中,而不是lib/文件夹。对于Roslyn源生成器创作(IIncrementalGenerator、语法/语义分析),参见[技能:dotnet-csharp-source-generators]。本节仅涵盖生成器的NuGet打包。
源生成器包的项目设置
<!-- MyCompany.Generators.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<!-- 包元数据 -->
<PackageId>MyCompany.Generators</PackageId>
<Description>用于小部件自动注册的源生成器。</Description>
<!-- 不要将生成器DLL包含在lib/文件夹中 -->
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<!-- 生成器必须针对netstandard2.0以兼容Roslyn主机 -->
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
</ItemGroup>
<!-- 将生成器DLL放置在分析器文件夹中 -->
<ItemGroup>
<None Include="$(OutputPath)$(AssemblyName).dll"
Pack="true"
PackagePath="analyzers/dotnet/cs"
Visible="false" />
</ItemGroup>
</Project>
添加构建Props/Targets
当源生成器需要在消费项目中设置MSBuild属性时,使用buildTransitive文件夹:
<!-- build/MyCompany.Generators.props -->
<Project>
<PropertyGroup>
<MyCompanyGeneratorsEnabled>true</MyCompanyGeneratorsEnabled>
</PropertyGroup>
<ItemGroup>
<!-- 示例:为生成器消费添加额外文件 -->
<CompilerVisibleProperty Include="MyCompanyGeneratorsEnabled" />
</ItemGroup>
</Project>
在包中包含buildTransitive内容:
<!-- 在.csproj中 -->
<ItemGroup>
<!-- buildTransitive确保props/targets通过传递依赖流动 -->
<None Include="build\MyCompany.Generators.props"
Pack="true"
PackagePath="buildTransitive\MyCompany.Generators.props" />
<None Include="build\MyCompany.Generators.targets"
Pack="true"
PackagePath="buildTransitive\MyCompany.Generators.targets" />
</ItemGroup>
多目标分析器包(分析器 + 库)
当在同一包中同时发布分析器和运行时库时:
<!-- MyCompany.Widgets.csproj(发布运行时库 + 分析器) -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
<PackageId>MyCompany.Widgets</PackageId>
</PropertyGroup>
<!-- 引用生成器项目,但抑制其输出到lib/ -->
<ItemGroup>
<ProjectReference Include="..\MyCompany.Widgets.Generators\MyCompany.Widgets.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
NuGet包文件夹布局
MyCompany.Generators.1.0.0.nupkg
analyzers/
dotnet/
cs/
MyCompany.Generators.dll <-- 生成器/分析器程序集
buildTransitive/
MyCompany.Generators.props <-- 自动导入的MSBuild props
MyCompany.Generators.targets <-- 自动导入的MSBuild targets
lib/
netstandard2.0/
_._ <-- 空标记(无运行时库)
多目标框架包
多目标生成一个包含每个目标框架程序集的单一NuGet包。消费者自动获得最佳匹配的程序集。
何时使用多目标
| 场景 | 方法 |
|---|---|
| 库仅适用于net8.0 | 单一TFM:<TargetFramework>net8.0</TargetFramework> |
| 库需要netstandard2.0 + net8.0 API | 多TFM:<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks> |
库使用net9.0特定API(例如,SearchValues) |
使用polyfills或条件代码的多TFM |
| 库目标为.NET Framework消费者 | 包含net472或netstandard2.0 TFM |
多目标配置
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
</PropertyGroup>
<!-- 每个TFM的API差异 -->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.6.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
</Project>
条件编译
public static class StringExtensions
{
public static bool ContainsIgnoreCase(this string source, string value)
{
#if NET8_0_OR_GREATER
return source.Contains(value, StringComparison.OrdinalIgnoreCase);
#else
return source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
#endif
}
}
NuGet包文件夹布局(多TFM)
MyCompany.Widgets.1.0.0.nupkg
lib/
netstandard2.0/
MyCompany.Widgets.dll
net8.0/
MyCompany.Widgets.dll
net9.0/
MyCompany.Widgets.dll
符号包和确定性构建
符号包(.snupkg)通过NuGet符号服务器为包消费者启用源码级调试。
启用符号包
<PropertyGroup>
<!-- 生成.snupkg与.nupkg一起 -->
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<!-- 确定性构建(用于可重现包) -->
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
<!-- 在PDB中嵌入源码以便无需源码服务器调试 -->
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
当使用dotnet nuget push时,snupkg会自动与nupkg一起推送:
# 将.nupkg和.snupkg推送到nuget.org
dotnet nuget push "bin/Release/*.nupkg" \
--source https://api.nuget.org/v3/index.json \
--api-key "$NUGET_API_KEY"
SourceLink集成: 对于链接到实际源码仓库的源码级调试,在项目中配置SourceLink。参见[技能:dotnet-project-structure]获取SourceLink设置——不要在此重复配置。
嵌入式PDB替代方案
对于不希望使用单独符号包的包:
<PropertyGroup>
<DebugType>embedded</DebugType>
</PropertyGroup>
这将PDB直接嵌入到程序集DLL中。权衡是包大小更大但分发更简单。
包签名
NuGet支持作者签名(证明包来源)和仓库签名(证明它来自特定源)。
使用证书进行作者签名
# 使用PFX证书签名包
dotnet nuget sign "MyCompany.Widgets.1.0.0.nupkg" \
--certificate-path ./signing-cert.pfx \
--certificate-password "$CERT_PASSWORD" \
--timestamper http://timestamp.digicert.com
# 使用证书存储中的证书签名(Windows)
dotnet nuget sign "MyCompany.Widgets.1.0.0.nupkg" \
--certificate-fingerprint "ABC123..." \
--timestamper http://timestamp.digicert.com
证书要求
| 要求 | 详情 |
|---|---|
| 密钥用法 | 代码签名(1.3.6.1.5.5.7.3.3) |
| 算法 | RSA 2048位最小 |
| 时间戳 | 长期有效性所需 |
| 可信CA | DigiCert、Sectigo或其他nuget.org可信CA |
| 自签名 | 私有源接受;nuget.org拒绝 |
仓库签名
仓库签名由源操作员应用(例如,nuget.org对所有包签名)。包作者无需配置仓库签名——它由源基础设施自动应用。
验证包签名
# 验证签名包
dotnet nuget verify "MyCompany.Widgets.1.0.0.nupkg"
# 详细输出验证
dotnet nuget verify "MyCompany.Widgets.1.0.0.nupkg" --verbosity detailed
包验证
包验证在发布前捕获API中断、无效包布局和兼容性问题。
内置打包验证
<PropertyGroup>
<!-- 在dotnet pack上启用包验证 -->
<EnablePackageValidation>true</EnablePackageValidation>
</PropertyGroup>
这验证:
- 所有TFM具有兼容的API表面
- 包版本之间无意外API移除
- 包布局遵循NuGet约定
与基线版本的API兼容性
比较当前包与先前发布的基线版本以检测破坏性更改:
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<!-- 与最后发布版本比较 -->
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
</PropertyGroup>
Microsoft.DotNet.ApiCompat.Task
用于跨程序集的高级API兼容性检查:
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ApiCompat.Task" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup>
<!-- 启用API兼容性分析 -->
<ApiCompatEnableRuleAttributesMustMatch>true</ApiCompatEnableRuleAttributesMustMatch>
<ApiCompatEnableRuleCannotChangeParameterName>true</ApiCompatEnableRuleCannotChangeParameterName>
</PropertyGroup>
抑制已知中断
当进行故意的API更改时,生成并提交抑制文件:
# 为已知中断生成抑制文件
dotnet pack /p:GenerateCompatibilitySuppressionFile=true
这将创建CompatibilitySuppressions.xml:
<!-- CompatibilitySuppressions.xml(提交到源代码控制) -->
<?xml version="1.0" encoding="utf-8"?>
<Suppressions xmlns:ns="https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:MyCompany.Widgets.Widget.OldMethod</Target>
<Left>lib/net8.0/MyCompany.Widgets.dll</Left>
<Right>lib/net8.0/MyCompany.Widgets.dll</Right>
</Suppression>
</Suppressions>
引用抑制文件:
<ItemGroup>
<ApiCompatSuppressionFile Include="CompatibilitySuppressions.xml" />
</ItemGroup>
NuGet版本策略
NuGet的SemVer 2.0
NuGet遵循语义化版本2.0:
| 版本 | 含义 |
|---|---|
1.0.0 |
稳定发布 |
1.0.1 |
补丁(错误修复,无API更改) |
1.1.0 |
次要(新功能,向后兼容) |
2.0.0 |
主要(破坏性更改) |
1.0.0-alpha.1 |
预发布alpha |
1.0.0-beta.1 |
预发布beta |
1.0.0-rc.1 |
发布候选 |
预发布后缀
<!-- 稳定发布 -->
<Version>1.2.3</Version>
<!-- 带有SemVer 2.0点分隔后缀的预发布 -->
<Version>1.2.3-beta.1</Version>
<!-- 具有提交高度的CI构建(NBGV模式) -->
<!-- 产生:1.2.3-beta.42+abcdef -->
NBGV集成
Nerdbank.GitVersioning(NBGV)从git历史计算版本。对于NBGV设置和version.json配置,参见[技能:dotnet-release-management]。本技能涵盖NBGV生成的版本如何与NuGet打包交互:
<PropertyGroup>
<!-- NBGV自动设置Version、PackageVersion、AssemblyVersion -->
<!-- 使用NBGV时不要显式设置Version -->
</PropertyGroup>
NBGV生成像1.2.42-beta+abcdef这样的版本,其中:
1.2来自version.json42是git提交高度-beta是来自version.json的预发布后缀+abcdef是git提交哈希(构建元数据,NuGet解析忽略)
版本属性参考
| 属性 | 用途 | 设置者 |
|---|---|---|
Version |
完整SemVer版本(驱动PackageVersion) | 手动或NBGV |
PackageVersion |
NuGet包版本(默认为Version) | 手动或NBGV |
AssemblyVersion |
CLR程序集版本 | 手动或NBGV |
FileVersion |
Windows文件版本 | 手动或NBGV |
InformationalVersion |
带有元数据的完整版本字符串 | 手动或NBGV |
打包和本地测试
构建包
# 以Release配置打包
dotnet pack --configuration Release
# 以特定版本覆盖打包
dotnet pack --configuration Release /p:Version=1.2.3-beta.1
# 输出到特定目录
dotnet pack --configuration Release --output ./artifacts
本地源测试
发布前在本地测试包:
# 创建本地源目录
mkdir -p ~/local-nuget-feed
# 将包添加到本地源
dotnet nuget push "bin/Release/MyCompany.Widgets.1.0.0.nupkg" \
--source ~/local-nuget-feed
# 在消费项目中,添加本地源
dotnet nuget add source ~/local-nuget-feed --name LocalFeed
包内容检查
# 列出包内容(nupkg是zip文件)
unzip -l MyCompany.Widgets.1.0.0.nupkg
# 验证分析器放置
unzip -l MyCompany.Generators.1.0.0.nupkg | grep analyzers/
代理陷阱
-
不要同时设置
PackageLicenseExpression和PackageLicenseFile——它们是互斥的。对标准SPDX标识符使用PackageLicenseExpression,仅对自定义许可证使用PackageLicenseFile。 -
源生成器必须针对
netstandard2.0——Roslyn主机要求此。不要对生成器本身进行多目标;对引用生成器项目的运行时库进行多目标。 -
不要在库项目上将
IncludeBuildOutput设置为false——仅对不应贡献运行时程序集的纯分析器/生成器项目。 -
buildTransitive与build文件夹——对应该通过传递PackageReference依赖流动的props/targets使用buildTransitive。build文件夹仅影响直接消费者。 -
包验证抑制使用带有
CompatibilitySuppressions.xml的ApiCompatSuppressionFile——不是PackageValidationSuppressionMSBuild项。使用/p:GenerateCompatibilitySuppressionFile=true生成文件。 -
SDK风格项目自动包含所有
*.cs文件——添加TFM条件Compile Include而不带前导Compile Remove会导致NETSDK1022重复项。 -
永远不要在CLI示例中硬编码API密钥——始终使用环境变量占位符(
$NUGET_API_KEY)并注明CI秘密存储。 -
ContinuousIntegrationBuild必须基于CI条件——无条件设置会通过使PDB不可重现本地文件路径来破坏本地调试。