构建配置与依赖管理
本页解析 SteamTools(Watt Toolkit)仓库中由 MSBuild 公共属性文件(Directory.Build.props)与集中包管理文件(Directory.Packages.props)构成的构建配置与依赖管理体系:所有 src/ 下的项目共享同一套编译器/链接器/程序集元数据设置,而 NuGet 包版本统一由 ref 子树中的集中版本表(CPM, Central Package Management)托管。
Purpose and Scope
本页覆盖:
src/Directory.Build.props——src子树下所有项目的公共 MSBuild 属性(目标框架基线、语言版本、可空性、AOT/裁剪、程序集特性生成开关等);ref/Directory.Build.props——ref子树(被引用的外部源码子树)自己的最小化覆盖;src/Directory.Packages.props—— 集中包管理(CPM)入口,以及它如何把版本表委托给ref\DirectoryPackages\Directory.Packages.props;- MSBuild 的
Directory.Build.props/Directory.Packages.props目录向上查找与导入机制在本仓库中的实际生效路径; - 公共编译注入项(
AssemblyInfo.cs、ImplicitUsings.BCL.cs、ImplicitUsings.Common.cs)与Microsoft.SourceLink.GitHub的接线方式。
刻意留给兄弟页面的内容:
- CI/CD 流水线(GitHub Actions 的构建、签名、打包任务)不属于本页范围;
- 各具体应用项目(Avalonia 客户端、服务进程等)的
.csproj单项目配置、发布渠道与安装包格式,见对应的构建/发布类页面; ref\DirectoryPackages\Directory.Packages.props中逐个包的具体版本号清单:该文件位于ref引用子树内,其行级版本表不在本页取证范围内(本页只说明它的接入机制与职责边界)。
Overview
本仓库采用 .NET 生态中两种"目录级共享配置"机制来避免每个 .csproj 重复维护同样的设置:
Directory.Build.props(公共构建属性):MSBuild 在编译任何项目时,会从项目文件所在目录向上逐级查找最近的一个Directory.Build.props并自动导入。因此src/Directory.Build.props对src下全部项目生效,ref/Directory.Build.props对ref下全部项目生效,二者互不干扰(默认情况下 MSBuild 命中最近一个后停止向上查找)。Directory.Packages.props(集中包管理,CPM):当ManagePackageVersionsCentrally=true时,各项目里的<PackageReference>可以(且必须)不写Version,版本号统一来自最近一级Directory.Packages.props中的<PackageVersion>表。本仓库在src/Directory.Packages.props中不直接列版本,而是Import了ref子树中的版本表,使"依赖版本治理"与"业务源码"解耦。
这种设计的意图非常明确:
- 一处修改,全仓生效:升级
LangVersion、调整NoWarn、切换目标 .NET 基线(DotNet_Version=11.0)都只改一个文件; - 可复现构建:
Deterministic=true+ SourceLink + 集中版本表共同保证同一 commit 产出可追溯、位稳定的二进制; - 裁剪/AOT 友好:
IsTrimmable=true、关闭大量自动生成的程序集特性(GenerateAssembly*Attribute=false),为后续发布瘦身与自包含裁剪铺路; - 新语言特性前置:对
net11.0目标条件性地开启runtime-async=on实验特性。
Architecture
下图展示本仓库构建配置的导入链路与职责分层(节点均对应仓库中真实存在的文件/机制):
要点解读:
- 两个独立的
Directory.Build.props:src与ref是兄弟目录,各自的服务半径不重叠。src侧是重配置(28+ 个属性),ref侧只做一件事——关闭CheckEolWorkloads,避免引用子树里旧 workload 触发 EOL 警告升级为错误。 - CPM 的"间接委托":
src/Directory.Packages.props全文只有一行Import,把版本表物理上放到ref/DirectoryPackages/。这让外部维护的版本清单可以被独立演进、并在多个仓库间复用。 - 共享源码而非共享 NuGet 包:
AssemblyInfo.cs与两个ImplicitUsings*.cs通过<Compile Include>以LinkBase=Properties的方式链接进每个src项目,比发布一个内部 NuGet 包更轻量,且保证所有程序集的元数据/全局 using 完全一致。
Main Content
src/Directory.Build.props:全仓公共属性的单一事实来源
这是整个构建配置体系的核心文件,完整属性组如下:
1<Project>
2 <PropertyGroup>
3 <DotNet_Version>11.0</DotNet_Version>
4 <NoWarn>$(NoWarn);NU1507;1591;SA1612;IL2026;IL2091</NoWarn>
5 <LangVersion>latest</LangVersion>
6 <Nullable>enable</Nullable>
7 <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
8 <GenerateDocumentationFile>false</GenerateDocumentationFile>
9 <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
10 <ImplicitUsings>enable</ImplicitUsings>
11 <IsTrimmable>true</IsTrimmable>
12 <PackageIconUrl>https://avatars.githubusercontent.com/u/79355691?s=200&v=4</PackageIconUrl>
13 <RepositoryType>git</RepositoryType>
14 <GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
15 <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
16 <GenerateAssemblyCopyrightAttribute>false</GenerateAssemblyCopyrightAttribute>
17 <GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
18 <GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
19 <GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
20 <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
21 <GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
22 <GenerateNeutralResourcesLanguageAttribute>false</GenerateNeutralResourcesLanguageAttribute>
23 <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
24 <Deterministic>true</Deterministic>
25 <CheckEolWorkloads>false</CheckEolWorkloads>
26 <!--<DefineConstants>REMOVE_DNS_INTERCEPT;$(DefineConstants)</DefineConstants>-->
27 <CETCompat>false</CETCompat>
28 </PropertyGroup>
29</Project>Source: src/Directory.Build.props
逐项设计意图(按行分组解释):
| 属性 | 取值 | 设计意图(WHY) |
|---|---|---|
DotNet_Version | 11.0 | 目标框架基线变量。项目文件用 net$(DotNet_Version) 之类方式消费,升级 .NET 只改这一处 |
NoWarn | 追加 NU1507;1591;SA1612;IL2026;IL2091 | 抑制已知且接受噪声:NU1507(多源解析警告)、1591(缺 XML 注释)、SA1612(文档参数顺序)、IL2026/IL2091(裁剪不可达注解),为 AOT/裁剪清障 |
LangVersion | latest | 不随 TFM 自动放宽语言版本,总是使用最新编译器特性 |
Nullable | enable | 全仓启用可空引用类型检查 |
GenerateAssemblyInfo / GenerateDocumentationFile | true / false | 保留 AssemblyInfo 生成框架但关闭所有自动特性(见下一组),同时不生成 XML 文档(与 1591 抑制配套,减少发布体积) |
ManagePackageVersionsCentrally | true | 启用 CPM:项目内 PackageReference 不写版本,版本表见 Directory.Packages.props |
ImplicitUsings | enable | 配合下方注入的 ImplicitUsings.BCL.cs / ImplicitUsings.Common.cs 扩展隐式 using 集合 |
IsTrimmable | true | 程序集声明可裁剪,配合发布时的 trimming 缩小体积 |
PackageIconUrl / RepositoryType | GitHub 头像 URL / git | 打 NuGet 包时所需的图标与仓库类型元数据 |
GenerateAssembly*Attribute(8 个) | 全部 false | 关闭 MSBuild 自动生成的 Title/Company/Copyright/Description/FileVersion/InformationalVersion/Product/Version/NeutralResources 特性——版本与版权等由共享的 AssemblyInfo.cs 统一提供,避免双重来源冲突 |
ValidateExecutableReferencesMatchSelfContained | false | 允许混用自包含/框架依赖引用而不报错,适配多形态发布 |
Deterministic | true | 确定性构建:同输入同输出字节 |
CheckEolWorkloads | false | 跳过 workload EOL 检查(ref 侧同样关闭) |
CETCompat | false | 关闭 CET(控制流强制技术)兼容,减少对特定 CPU 特性的要求 |
DefineConstants(注释掉) | REMOVE_DNS_INTERCEPT | 预留的编译开关:取消注释即可全局移除 DNS 拦截相关代码路径 |
条件性 .NET 11+ 特性开关
1<!-- 👇 .NET 11+ 配置 -->
2<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net11.0'))">
3 <Features>$(Features);runtime-async=on</Features>
4</PropertyGroup>Source: src/Directory.Build.props
这一段只在目标框架与 net11.0 兼容时向 Roslyn 追加 runtime-async=on 语言特性。之所以用 IsTargetFrameworkCompatible 条件而不是无差别打开,是因为仓库中仍存在多目标/旧目标项目——特性开关只应作用于真正支持它的运行时目标。
共享源码注入:一次编写,所有程序集一致
1<ItemGroup>
2 <Compile Include="..\AssemblyInfo.cs">
3 <LinkBase>Properties</LinkBase>
4 </Compile>
5 <Compile Include="..\ImplicitUsings.BCL.cs">
6 <LinkBase>Properties</LinkBase>
7 </Compile>
8 <Compile Include="..\ImplicitUsings.Common.cs">
9 <LinkBase>Properties</LinkBase>
10 </Compile>
11</ItemGroup>Source: src/Directory.Build.props
三个位于 src 根目录的源文件被以 LinkBase=Properties 的方式链接编译进每个 src 项目:
AssemblyInfo.cs:集中维护的AssemblyVersion、InternalsVisibleTo等程序集级特性(这正是上文把 8 个GenerateAssembly*Attribute全关掉的原因——版本特性不能有两个来源);ImplicitUsings.BCL.cs/ImplicitUsings.Common.cs:在 SDK 内置隐式 using 之外,补充 BCL 与项目公共命名空间的全局 using,使业务代码免写重复using。
(这三个文件的内容属于各自主题,本页不展开。)
SourceLink:可调试、可溯源的发布件
1<ItemGroup>
2 <PackageReference Include="Microsoft.SourceLink.GitHub">
3 <PrivateAssets>all</PrivateAssets>
4 <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
5 </PackageReference>
6</ItemGroup>Source: src/Directory.Build.props
注意这里没有 Version 属性——这正是 CPM 生效的直接证据,版本由 Directory.Packages.props 版本表提供。PrivateAssets=all 确保 SourceLink 只参与本项目的构建(生成 PDB 中的源码 URL 映射),不会向下游消费者传递任何资产。
ref/Directory.Build.props:引用子树的最小覆盖
1<Project>
2 <PropertyGroup>
3 <CheckEolWorkloads>false</CheckEolWorkloads>
4 </PropertyGroup>
5</Project>Source: ref/Directory.Build.props
ref 子树(外部引入的参考源码)只覆盖一个属性:关闭 workload EOL 检查。它不继承 src/Directory.Build.props 的配置——因为 MSBuild 的向上查找在命中本目录的 Directory.Build.props 后即停止,两套配置天然隔离,避免公共策略(如 Nullable/LangVersion)对外部代码造成意外行为变更。
src/Directory.Packages.props:CPM 的薄委托层
<Project>
<Import Project="..\ref\DirectoryPackages\Directory.Packages.props" />
</Project>Source: src/Directory.Packages.props
这是全文件内容。src 侧不维护任何 <PackageVersion>,而是把整张版本表 Import 自 ref\DirectoryPackages\Directory.Packages.props。收益:
- 版本治理独立于业务源码演进,可随
ref子树同步更新; src中任何项目的PackageReference若引用了版本表外的包,会在还原阶段直接报 NU1008/找不到版本类错误,从机制上杜绝"随手写死版本"。
(该版本表内的具体包与版本号不在本页取证范围内,见 Purpose and Scope 的边界说明。)
Core Flow
一次 dotnet build 中,构建配置与依赖解析的实际顺序如下:
关键顺序说明:
Directory.Build.props在求值早期被导入,因此其中的属性能影响项目文件自身的条件求值(如基于$(DotNet_Version)的 TFM);Directory.Packages.props在还原求值阶段被消费——项目内无版本的PackageReference必须能在PackageVersion表中找到同Include的条目,否则构建失败;Compile Include注入发生在公共项阶段,因此每个src程序集都携带相同的共享元数据与全局 using。
Configuration Options
以下为 src/Directory.Build.props 中全部可配置项的完整清单(含 ref 侧覆盖项):
| Option | Type | Default(本仓取值) | Description |
|---|---|---|---|
DotNet_Version | string | "11.0" | 目标框架基线版本变量,供项目文件构造 TFM |
NoWarn | string (追加) | NU1507;1591;SA1612;IL2026;IL2091 | 追加抑制的警告码列表 |
LangVersion | string | latest | C# 语言版本 |
Nullable | enum | enable | 可空引用类型检查 |
GenerateAssemblyInfo | bool | true | 是否启用 AssemblyInfo 生成框架(具体特性逐项关闭) |
GenerateDocumentationFile | bool | false | 是否生成 XML 文档 |
ManagePackageVersionsCentrally | bool | true | 启用集中包管理(CPM) |
ImplicitUsings | enum | enable | 启用隐式 using |
IsTrimmable | bool | true | 程序集可裁剪标记 |
PackageIconUrl | string | GitHub 头像 URL | NuGet 包图标地址 |
RepositoryType | string | git | 仓库类型元数据 |
GenerateAssemblyTitleAttribute … GenerateAssemblyNeutralResourcesLanguageAttribute | bool | 全部 false | 8 个自动程序集特性逐项关闭,版本/版权由共享 AssemblyInfo.cs 提供 |
ValidateExecutableReferencesMatchSelfContained | bool | false | 自包含/框架依赖混合引用校验 |
Deterministic | bool | true | 确定性构建 |
CheckEolWorkloads | bool | false | workload EOL 检查(src 与 ref 均关闭) |
CETCompat | bool | false | CET 兼容性 |
Features(条件属性) | string (追加) | runtime-async=on(仅 net11.0+ 目标) | Roslyn 实验特性开关 |
DefineConstants | string | (已注释:REMOVE_DNS_INTERCEPT) | 预留的编译期开关 |
API Reference
本页主题是 MSBuild 属性文件,不包含传统意义上的代码 API。对外可"调用"的面只有两类:
Directory.Build.props 自动导入
参数: 无(由 MSBuild 引擎按目录向上查找自动触发)
返回: 生效的属性/项集合(PropertyGroup / ItemGroup)
行为约束: MSBuild 自项目文件所在目录向上查找最近一个 Directory.Build.props 后停止;如需合并多级,须显式使用 DirectoryBuildPropsPath / <Import> 链(本仓库未使用该机制,src 与 ref 两侧完全隔离)。
Directory.Packages.props 版本表(经 Import Project)
参数: Project = "..\ref\DirectoryPackages\Directory.Packages.props"(相对 src/Directory.Packages.props)
返回: <PackageVersion Include="..." Version="..." /> 条目集合
失败行为: 项目 PackageReference 的 Include 未命中任何 PackageVersion 条目时,NuGet 还原阶段报错(找不到包版本),构建中止。
Failure Modes, Edge Cases & Concurrency
- 双源版本冲突:由于 8 个
GenerateAssembly*Attribute已关闭、版本特性集中在src/AssemblyInfo.cs,若未来有人在某个.csproj里重新打开其中任一开关,会造成"同一程序集出现重复特性"的编译错误或静默双写。修改公共元数据时应始终只改共享AssemblyInfo.cs。 - CPM 命中失败:在项目里新增
PackageReference却忘了在ref\DirectoryPackages\Directory.Packages.props中登记PackageVersion,还原会直接失败——这是刻意的治理闸门,而不是可绕过的警告。 NoWarn全局抑制的传染性:NU1507(多源解析)、IL2026/IL2091(裁剪警告)被全局压制,意味着某些真实的依赖/裁剪风险不再报错;需要排查裁剪问题时应在单项目级别临时恢复。src与ref配置漂移:两侧Directory.Build.props独立生效是特性也是风险——例如Nullable只在src侧enable,ref侧代码不受可空检查约束,跨子树阅读代码时不能假设同等严格度。- 注释掉的
REMOVE_DNS_INTERCEPT:这是一个"存在但默认关闭"的编译开关,属于潜在的定时炸弹式配置——开启它会把 DNS 拦截相关代码从所有src项目中条件编译掉,务必确认对应功能确实可整体移除。 - 构建并发:属性文件是全局共享的求值输入;在并行构建(
-m)下多个项目同时读取同一Directory.Build.props不存在竞态(MSBuild 每个项目独立求值),但任何人在构建进行中修改这些文件会导致同一批次内不同项目看到不同配置,应避免。
Performance & Operational Notes
- 关闭 XML 文档生成(
GenerateDocumentationFile=false)显著缩短编译时间并减少输出体积;文档警告1591同时被抑制,二者是配套决策。 Deterministic=true+ SourceLink 是发布运维的基础:任何发布二进制都可以通过 PDB 中的 URL 映射回 GitHub 源码行,便于线上符号化与回溯。IsTrimmable=true+NoWarn中的裁剪码:为发布时的 PublishTrimmed/AOT 流水线预铺路——库层面声明可裁剪,下游裁剪时不会因为本程序集未声明而被保守保留。- 条件性
Features:只在net11.0+目标追加runtime-async=on,避免对旧目标注入不支持的编译器特性导致编译失败。 - 版本表集中化:升级一个依赖(如 Avalonia)只改
ref\DirectoryPackages\Directory.Packages.props一处,随后全仓还原即可验证兼容性,运维上把"依赖升级"收敛为单一原子变更。
Extension Points
- 新增公共属性:直接在
src/Directory.Build.props的PropertyGroup中追加;如仅需.NET 11+目标生效,仿照IsTargetFrameworkCompatible条件块新增PropertyGroup。 - 新增共享源码:在
src根放置文件,并在ItemGroup中按<Compile Include="..\文件名.cs"><LinkBase>Properties</LinkBase></Compile>模式追加。 - 调整依赖版本:编辑
ref\DirectoryPackages\Directory.Packages.props的PackageVersion条目;不要在项目内写死Version(CPM 下会被视为错误实践甚至直接失败)。 - 启用 DNS 拦截移除:取消
<!--<DefineConstants>REMOVE_DNS_INTERCEPT;$(DefineConstants)</DefineConstants>-->的注释(影响全部src项目,需谨慎评估)。 - 引入
ref侧新策略:在ref/Directory.Build.props中追加;注意它不会继承src侧任何配置。
Tests
本主题为构建配置文件,仓库中未发现针对 Directory.Build.props / Directory.Packages.props 的独立单元测试(构建配置的正确性由 CI 上的还原/编译/发布流水线间接验证)。若配置损坏,最早暴露点是 dotnet restore 与 dotnet build 的失败。
Related Links
- src/Directory.Build.props ——
src子树公共构建属性(本页核心证据) - ref/Directory.Build.props ——
ref子树最小覆盖 - src/Directory.Packages.props —— CPM 薄委托层
- ref/DirectoryPackages/Directory.Packages.props —— 集中包版本表(
Import目标)