Repository Wiki
BeyondDimension/SteamTools

构建配置与依赖管理

本页解析 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 重复维护同样的设置:

  1. Directory.Build.props(公共构建属性):MSBuild 在编译任何项目时,会从项目文件所在目录向上逐级查找最近的一个 Directory.Build.props 并自动导入。因此 src/Directory.Build.props 对 src 下全部项目生效,ref/Directory.Build.props 对 ref 下全部项目生效,二者互不干扰(默认情况下 MSBuild 命中最近一个后停止向上查找)。
  2. 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

下图展示本仓库构建配置的导入链路与职责分层(节点均对应仓库中真实存在的文件/机制):

Loading diagram...

要点解读:

  • 两个独立的 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:全仓公共属性的单一事实来源

这是整个构建配置体系的核心文件,完整属性组如下:

xml
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&amp;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_Version11.0目标框架基线变量。项目文件用 net$(DotNet_Version) 之类方式消费,升级 .NET 只改这一处
NoWarn追加 NU1507;1591;SA1612;IL2026;IL2091抑制已知且接受噪声:NU1507(多源解析警告)、1591(缺 XML 注释)、SA1612(文档参数顺序)、IL2026/IL2091(裁剪不可达注解),为 AOT/裁剪清障
LangVersionlatest不随 TFM 自动放宽语言版本,总是使用最新编译器特性
Nullableenable全仓启用可空引用类型检查
GenerateAssemblyInfo / GenerateDocumentationFiletrue / false保留 AssemblyInfo 生成框架但关闭所有自动特性(见下一组),同时不生成 XML 文档(与 1591 抑制配套,减少发布体积)
ManagePackageVersionsCentrallytrue启用 CPM:项目内 PackageReference 不写版本,版本表见 Directory.Packages.props
ImplicitUsingsenable配合下方注入的 ImplicitUsings.BCL.cs / ImplicitUsings.Common.cs 扩展隐式 using 集合
IsTrimmabletrue程序集声明可裁剪,配合发布时的 trimming 缩小体积
PackageIconUrl / RepositoryTypeGitHub 头像 URL / git打 NuGet 包时所需的图标与仓库类型元数据
GenerateAssembly*Attribute(8 个)全部 false关闭 MSBuild 自动生成的 Title/Company/Copyright/Description/FileVersion/InformationalVersion/Product/Version/NeutralResources 特性——版本与版权等由共享的 AssemblyInfo.cs 统一提供,避免双重来源冲突
ValidateExecutableReferencesMatchSelfContainedfalse允许混用自包含/框架依赖引用而不报错,适配多形态发布
Deterministictrue确定性构建:同输入同输出字节
CheckEolWorkloadsfalse跳过 workload EOL 检查(ref 侧同样关闭)
CETCompatfalse关闭 CET(控制流强制技术)兼容,减少对特定 CPU 特性的要求
DefineConstants(注释掉)REMOVE_DNS_INTERCEPT预留的编译开关:取消注释即可全局移除 DNS 拦截相关代码路径

条件性 .NET 11+ 特性开关

xml
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 条件而不是无差别打开,是因为仓库中仍存在多目标/旧目标项目——特性开关只应作用于真正支持它的运行时目标。

共享源码注入:一次编写,所有程序集一致

xml
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:可调试、可溯源的发布件

xml
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:引用子树的最小覆盖

xml
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 的薄委托层

xml
<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 中,构建配置与依赖解析的实际顺序如下:

Loading diagram...

关键顺序说明:

  1. Directory.Build.props 在求值早期被导入,因此其中的属性能影响项目文件自身的条件求值(如基于 $(DotNet_Version) 的 TFM);
  2. Directory.Packages.props 在还原求值阶段被消费——项目内无版本的 PackageReference 必须能在 PackageVersion 表中找到同 Include 的条目,否则构建失败;
  3. Compile Include 注入发生在公共项阶段,因此每个 src 程序集都携带相同的共享元数据与全局 using。

Configuration Options

以下为 src/Directory.Build.props 中全部可配置项的完整清单(含 ref 侧覆盖项):

OptionTypeDefault(本仓取值)Description
DotNet_Versionstring"11.0"目标框架基线版本变量,供项目文件构造 TFM
NoWarnstring (追加)NU1507;1591;SA1612;IL2026;IL2091追加抑制的警告码列表
LangVersionstringlatestC# 语言版本
Nullableenumenable可空引用类型检查
GenerateAssemblyInfobooltrue是否启用 AssemblyInfo 生成框架(具体特性逐项关闭)
GenerateDocumentationFileboolfalse是否生成 XML 文档
ManagePackageVersionsCentrallybooltrue启用集中包管理(CPM)
ImplicitUsingsenumenable启用隐式 using
IsTrimmablebooltrue程序集可裁剪标记
PackageIconUrlstringGitHub 头像 URLNuGet 包图标地址
RepositoryTypestringgit仓库类型元数据
GenerateAssemblyTitleAttribute … GenerateAssemblyNeutralResourcesLanguageAttributebool全部 false8 个自动程序集特性逐项关闭,版本/版权由共享 AssemblyInfo.cs 提供
ValidateExecutableReferencesMatchSelfContainedboolfalse自包含/框架依赖混合引用校验
Deterministicbooltrue确定性构建
CheckEolWorkloadsboolfalseworkload EOL 检查(src 与 ref 均关闭)
CETCompatboolfalseCET 兼容性
Features(条件属性)string (追加)runtime-async=on(仅 net11.0+ 目标)Roslyn 实验特性开关
DefineConstantsstring(已注释: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 的失败。