Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Demo/WpfDemo/WpfDemo.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project>
<Project>

<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

Expand All @@ -15,7 +15,7 @@
<PropertyGroup>
<GenerateResxSource>false</GenerateResxSource>
<GenerateNeutralResourcesLanguageAttribute>false</GenerateNeutralResourcesLanguageAttribute>
<DefaultItemExcludes>$(DefaultItemExcludes);artifacts\**</DefaultItemExcludes>
<DefaultItemExcludes>$(DefaultItemExcludes);artifacts\**</DefaultItemExcludes>
</PropertyGroup>

<ItemGroup Condition="'$(RepoWpfConsumerEnabled)' == 'true'">
Expand Down
6 changes: 3 additions & 3 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<WindowsDesktopARM64Support>true</WindowsDesktopARM64Support>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworkVersion>8.0</TargetFrameworkVersion>
<LangVersion>latest</LangVersion>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<!-- Normalize $(TestWpfArcadeSdkPath) by appending a '\' to it if one is missing -->
<PropertyGroup Condition="'$(TestWpfArcadeSdkPath)'!=''">
Expand Down Expand Up @@ -110,8 +110,8 @@
<PerlCommand Condition="'$(PerlCommand)' == ''">perl</PerlCommand>
</PropertyGroup>

<PropertyGroup>
<VCTargetsPath Condition="'$(VCTargetsPath)' == ''">$([MSBuild]::GetVsInstallRoot())\Common7\IDE\VC\VCTargets\</VCTargetsPath>
<PropertyGroup>
<VCTargetsPath Condition="'$(VCTargetsPath)' == ''">$([MSBuild]::GetVsInstallRoot())\Common7\IDE\VC\VCTargets\</VCTargetsPath>
</PropertyGroup>

<!-- warning MSB4011: 无法再次导入“C:\lindexi\Code\WpfReorganize_dotnetcampus\eng\Versions.props”。可能已在“C:\Users\lindexi\.nuget\packages\microsoft.dotnet.arcade.sdk\10.0.0-beta.25411.109\tools\DefaultVersions.props (15,3)”处导入过它。这很可能是生成创作错误。将忽略此后续导入。[C:\lindexi\Code\WpfReorganize_dotnetcampus\src\Microsoft.DotNet.Wpf\src\System.Xaml\System.Xaml.csproj] -->
Expand Down
6 changes: 5 additions & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<Project>
<Project>
<PropertyGroup Condition="'$(WpfRuntimeAssemblyVersion)' != ''">
<AssemblyVersion>$(WpfRuntimeAssemblyVersion)</AssemblyVersion>
</PropertyGroup>

<Import Project="Sdk.targets" Sdk="Microsoft.DotNet.Arcade.Sdk" />

<Import Project="$(WpfSourceDir)PresentationBuildTasks\Microsoft.WinFX.targets"
Expand Down
2 changes: 2 additions & 0 deletions Docs/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ IDE 接口对新增 `eng/Builder.Tests` 和 `eng/Builder.ProcessTestHelper` 尚

- 已实现共享 native 资产清单。
- Builder 已实现 x64 和 x86 路径。
- NuGet framework-dependent 消费问题已修复。Builder 将统一程序集版本 `42.42.42.42424` 传播到全部 x86/x64 WPF 运行时程序集,C++/CLI `DirectWriteForwarder` 显式写入相同 CLR 版本,组包前通过 PE 元数据执行版本一致性门禁。`PresentationCore/ModuleInitializer.cs` 已移除手工 app-local 加载与 `NoInlining` workaround,只保留正常初始化职责。清理后生成的 `WpfLab.WpfRuntime.1.0.0-cleanup-validation.nupkg` 已通过 .NET 8/9、win-x86/win-x64、单目标/多目标的真实 `dotnet build` + `dotnet run --no-build` 矩阵。详细证据见 [09-directwrite-forwarder-resolution.md](09-directwrite-forwarder-resolution.md)。
- Builder 已注册独立 `relay-pr` 命令,使用 Octokit 14.0.0 读取来源 PR,并在独立 clone 中执行固定 base/head SHA fetch、纯 Patch 应用、本地门禁、精确 SHA push 和目标 PR 创建/复用;命令缺少 `GITHUB_TOKEN` 时会在 clone 和远端写入前退出;`--allow-untrusted-build` 仅控制是否在 Temp workspace 执行本地构建验证,默认跳过并依赖 GitHub Actions。
- 本地门禁在隔离 HOME/NuGet/AppData/TEMP 环境中依次执行 Builder Restore/Build、x64/x86 构建打包、精确 nupkg `test-package` 和根 `Debug|x64` Rebuild,并校验构建前后 HEAD、tree、index 和 tracked working tree。
- `eng/Builder.Tests` 与 `eng/Builder.ProcessTestHelper` 已纳入根 `slnx`;单元、进程和本地 bare repository 集成测试覆盖 URL/remote 解析、敏感环境、取消/超时、PR ref fallback、纯 Patch 应用与冲突、精确 SHA push、lease 竞争、GitHub Actions 事件/身份、artifact 评论格式、workflow 安全契约和 checkout 换行保持契约。
Expand Down Expand Up @@ -126,6 +127,7 @@ IDE 接口对新增 `eng/Builder.Tests` 和 `eng/Builder.ProcessTestHelper` 尚

- 根 `slnx` 的 `Debug|x64` 和 `Debug|Any CPU` Restore + Rebuild 已在当前工作区成功;该结论不外推到其他配置、平台或 Visual Studio 设计时/F5 行为。
- Builder PR relay 的本地自动化验证不包含真实 GitHub push、PR 创建或不可信外部 PR 构建;Actions 权限、fork 与评论行为仍需真实 PR 验收。
- `DirectWriteForwarder` 冲突已由真实 framework-dependent NuGet 消费测试复现,不能通过移除 `Microsoft.WindowsDesktop.App` 或改用 self-contained publish 回避。完成标准是:所有自产 WPF 运行时程序集和 C++/CLI forwarder 使用统一程序集版本 `42.42.42.42424`;消费项目保留共享框架依赖;执行 `dotnet build` 后由 `dotnet run --no-build` 实际加载输出目录中的 forwarder,并通过精确 ABI、文本 shaping、MVID/SHA-256 与加载路径验证。
- 主题 ref 与运行时主题的成功依赖当前显式完整 `PresentationFramework` 引用边界;在同名打印 cycle-breaker 收敛前,不应移除该隔离,也不应使用会在干净项目求值时消失的条件式输出引用。
- 独立项目成功不能替代根 `slnx` 成功;增量成功不能替代强制重建成功。
- IDE 枚举项目路径不能证明项目已加载;必须由 Visual Studio 的加载状态和实际构建验证。
Expand Down
16 changes: 11 additions & 5 deletions Docs/05-builder-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ dotnet build eng/Builder/Builder.csproj --no-restore
| `WpfRuntimeDefinition` | 读取 `eng/WpfRuntimeDependencies.props` 与 `eng/Versions.props` 中的托管程序集和运行时 NuGet 依赖定义 |
| `NuGetPackageService` | 解析还原包路径、收集 native 资产、生成 `buildTransitive` targets 与 nuspec、校验包资产并执行打包 |
| `CompareService` | 与官方参考程序集做清单和尺寸级报告比较;不进行 API 二进制兼容性证明 |
| `PackageTestService` | 动态创建隔离消费项目,发布、校验包资产哈希并运行 WPF 探针 |
| `PackageTestService` | 动态创建隔离消费项目,发布、校验包资产哈希、`.deps.json` runtime 登记和 `runtimeconfig.json` 框架依赖;framework-dependent WPF 应用必须保留 `Microsoft.NETCore.App` 与 `Microsoft.WindowsDesktop.App`,随后运行文本 shaping 与程序集来源 WPF 探针 |
| `ProcessRunner` | 运行外部进程、合并标准输出和错误输出,并对探针执行超时终止 |
| `GitHubActionsBuildService` | 由受信任 Builder 校验 tested checkout 的凭据、事件 SHA/merge 双亲与 Git 状态,并在脱敏隔离环境中执行 solution 或 package 门禁 |
| `GitHubArtifactCommentService` | 通过 Octokit 分页读取 workflow run、PR、artifact 与评论元数据,执行最新运行判定、artifact 身份筛选和 bot 评论幂等回写 |
Expand Down Expand Up @@ -127,27 +127,33 @@ Builder 构建时使用 `$(NuGetPackageRoot)` 和共享版本写出 `PackagePath

## NuGet 包结构与消费逻辑

包 ID 为 `DotNetCampus.WpfLib`。当前包布局为:
包 ID 为 `WpfLab.WpfRuntime`。当前包布局为:

```text
DotNetCampus.WpfLib.<version>.nupkg
WpfLab.WpfRuntime.<version>.nupkg
├─ ref/net8.0/*.dll
├─ runtimes/win-x64/lib/net8.0/*.dll(包含 ijwhost.dll)
├─ runtimes/win-x64/native/*.dll(包含 ijwhost.dll)
├─ runtimes/win-x86/lib/net8.0/*.dll(包含 ijwhost.dll)
├─ runtimes/win-x86/native/*.dll(包含 ijwhost.dll)
└─ buildTransitive/DotNetCampus.WpfLib.targets
└─ buildTransitive/WpfLab.WpfRuntime.targets
```

nuspec 为 `net8.0` 和 `net9.0` 写入运行时包依赖组,依赖版本来自 `eng/WpfRuntimeDependencies.props` 和 `eng/Versions.props`。实现程序集仍是 `net8.0` 资产并写入 RID 目录;公共 `lib/net8.0` 不承载这些实现。通用输出回退可能让同一托管 DLL 同时进入两个 RID,不能仅凭目录布局断言二进制架构不同。

`buildTransitive/DotNetCampus.WpfLib.targets` 承担以下消费行为:
`buildTransitive/WpfLab.WpfRuntime.targets` 承担以下消费行为:

- 移除 `Microsoft.WindowsDesktop.App.WPF` FrameworkReference。
- 在解析引用后按文件名移除选定的 WPF 同名引用,并注入包内 `ref/net8.0`;当前实现不区分这些引用来自 inbox、显式引用还是其他包。
- 当 `RuntimeIdentifier` 为 `win-x64` 或 `win-x86` 时,选择对应的托管实现和 native DLL。
- 在普通 Build 与 Publish 后把 RID 资产复制到应用输出目录。

### Builder 与生成 targets 的命名约定

对外发布的包、文件和诊断来源统一使用正式名称 `WpfLab.WpfRuntime`。生成 targets 中以下划线开头的私有 MSBuild 属性、Item 和 Target 不机械拼接组织名与产品名,统一使用简洁的 `WpfRuntime` 前缀,例如 `_WpfRuntimeIdentifier`、`_WpfRuntimeReferenceDll`、`RemoveInboxWpfReferencesForWpfRuntime`。现有 `_DotNetCampus...` 和 `...ForDotNetCampusWpfLib` 属于旧命名,后续修改生成逻辑时应按该约定迁移;这些内部名称不是兼容性契约。

`DotNetCampus.Cli` 命名空间和 `DotNetCampus.CommandLine` 包名是当前第三方命令行依赖的正式名称,不属于仓库或 NuGet 包改名范围,应继续保留。

打包前会校验两个 RID 的核心 ref、实现、native 和 `buildTransitive` 文件。实际 `dotnet pack` 使用系统临时目录中的最小 SDK 项目,避免临时 pack 项目继承仓库根构建导入;生成的包写入 `eng/Builder/bin/nupkg/`。

构建末尾还会以报告模式比较官方 `Microsoft.WindowsDesktop.App.Ref`。该比较只检查清单缺失与显著尺寸差异,且报告模式不会让完整构建命令失败,不能替代 API、加载或运行验证。独立运行 `compare` 时应先确保 `staging/ref/net8.0` 已由完整 Builder 构建生成;当前无 staging 的回退只选择收集结果中一个目录,可能产生不完整报告。
Expand Down
131 changes: 131 additions & 0 deletions Docs/09-directwrite-forwarder-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# DirectWriteForwarder framework-dependent 加载问题

## 问题范围

本专题记录 `WpfLab.WpfRuntime` NuGet 包被普通 WPF 开发项目消费时,`DirectWriteForwarder.dll` 未从应用输出目录加载的问题。

目标消费场景是:

- 项目目标框架为 `net8.0-windows`。
- 使用 `dotnet build` 生成 framework-dependent 输出。
- 使用 `dotnet run --no-build` 启动。
- 项目引用本仓库构建出的 `WpfLab.WpfRuntime` NuGet 包。
- 运行时继续依赖 `Microsoft.WindowsDesktop.App`,不能用 self-contained 发布改变问题模型。

## 原测试缺陷

此前 `PackageTestService` 使用 `dotnet publish --self-contained true`,然后直接启动发布目录中的 EXE。该测试会将运行时闭包完整复制到发布目录,不能覆盖普通开发者使用 `dotnet build` 和共享框架运行的程序集解析行为。

该测试即使通过,也不能证明 framework-dependent 应用会加载包内 `DirectWriteForwarder.dll`。以此得出 `ModuleInitializer` 加载顺序有效的结论是错误的。

当前包测试已改为:

1. 在隔离 NuGet 源和隔离包缓存中创建消费项目。
2. 执行 `dotnet build`,并显式保持 `SelfContained=false`。
3. 使用 SDK 默认的 `bin/Release/<TFM>/<RID>/` 输出目录。
4. 执行 `dotnet run --no-build --no-restore`。
5. 验证托管程序集实际加载路径、MVID 和 SHA-256。
6. 验证 `MS.Internal.Text.TextInterface.TextAnalyzer.Itemize` 的精确 ABI。
7. 实际执行 `FormattedText` 文本 shaping 和 XAML 控件创建。

## 已复现行为

在 `net8.0-windows/win-x86` 的真实 build/run 场景中:

- 应用输出目录存在 NuGet 包提供的 `DirectWriteForwarder.dll`。
- 应用 `.deps.json` 包含 `DirectWriteForwarder.dll` runtime 资产登记。
- `WindowsBase.dll`、`PresentationCore.dll` 和 `PresentationFramework.dll` 从应用输出目录加载。
- `DirectWriteForwarder.dll` 实际从已安装的 `Microsoft.WindowsDesktop.App/8.0.x` 共享框架目录加载。
- 随后 `PresentationCore` 调用包内新 ABI 时可能出现 `TextAnalyzer.Itemize` 的 `MissingMethodException`。

因此问题不是旧文件残留、NuGet 缓存混用或输出目录缺少文件,而是 framework-dependent 默认加载上下文中的程序集身份与统一行为。

## ModuleInitializer 的职责与能力边界

`PresentationCore/ModuleInitializer.cs` 本身仍有正常的 WPF 初始化职责,包括:

- 尽早设置进程 DPI awareness。
- 调用 `DWriteLoader.LoadDWrite()` 初始化 DirectWrite。
- 调用 `MS.Internal.NativeWPFDLLLoader.LoadDwrite()` 触发 WPF native/C++/CLI 组件初始化。

这些初始化职责与本次程序集身份冲突不同,应继续保留。

当前文件中后来加入的 app-local 程序集加载逻辑属于独立 workaround:

- `LoadAppLocalDirectWriteForwarder()`。
- `AssemblyLoadContext.Default.LoadFromAssemblyPath(...)`。
- 为确保该调用先执行而增加的 `NoInlining` 辅助方法和加载顺序调整。

该 workaround 不能可靠覆盖已经由共享框架满足的同身份程序集引用。`NoInlining` 可以避免 JIT 在方法入口过早解析静态依赖,但不能解决以下情况:

- 包内和共享框架中的程序集简单名称、版本、区域性和公钥标记构成兼容身份。
- 默认加载上下文已经选择共享框架程序集来满足引用。

统一程序集版本修复后,`PresentationCore` 引用 `DirectWriteForwarder, Version=42.42.42.42424`,共享框架中的 `8.0.0.0` 不能满足该引用。此时正常的 `.deps.json` 和默认加载上下文应直接选择应用输出目录中的包内 forwarder,不再需要手工按路径抢先加载。

因此,最终收敛目标是:保留正常 DPI、DirectWrite 和 native 初始化职责;删除仅用于 app-local 程序集抢先加载的 workaround。删除后必须重新执行真实 framework-dependent NuGet 消费测试,只有加载路径、ABI 和文本 shaping 继续通过,才能确认该 workaround 可以安全移除。

## DirectWriteForwarder 版本缺陷

已确认 `DirectWriteForwarder.vcxproj` 构建求值期间存在 `$(AssemblyVersion)`,但 C++/CLI 项目不会像 SDK 风格 C# 项目一样自动生成托管 `AssemblyVersionAttribute`。

当前未显式生成该特性时,产出的 `DirectWriteForwarder.dll` 程序集版本为 `0.0.0.0`。这是构建链缺陷,不是期望设计。

已执行过两项诊断实验:

- 硬编码 `AssemblyVersion("8.0.0.1")`:net8 x86/x64 build/run 可以加载 app-local forwarder 并通过 shaping,但该版本没有接入仓库统一版本体系,只能证明程序集身份是根因,不能作为最终实现。
- 使用普通 `$(AssemblyVersion)`,即 `8.0.0.0`:真实 build/run 仍加载共享框架 forwarder,因为共享框架版本也是 `8.0.0.0`,身份冲突未消除。

上述实验均已撤回。

## 统一隔离版本要求

本仓库自产 WPF 运行时程序集应使用统一隔离程序集版本:

`42.42.42.42424`

当前实现采用以下统一版本链:

1. Builder 以独立的 `WpfRuntimeAssemblyVersion` 属性将 `42.42.42.42424` 传入所有运行时项目构建,不与 NuGet 包版本混用。
2. 根 `Directory.Build.targets` 在 Arcade props 求值完成后、程序集属性生成前,将 `WpfRuntimeAssemblyVersion` 映射为 `AssemblyVersion`。
3. SDK 风格托管项目由正常程序集属性生成流程写入 `AssemblyVersionAttribute`。
4. C++/CLI `DirectWriteForwarder` 通过预处理宏接收同一个 `WpfRuntimeAssemblyVersion`,并在 `OtherAssemblyAttrs.cpp` 显式生成托管 `AssemblyVersionAttribute`,避免退化为 `0.0.0.0`。
5. Builder 在组包前使用 PE 元数据读取所有 x86/x64 运行时程序集的实际 CLR 版本;任一程序集不是 `42.42.42.42424` 时立即停止组包。
6. 消费探针同时检查实际加载路径、程序集版本、MVID、SHA-256、`TextAnalyzer.Itemize` ABI、文本 shaping 和 XAML 控件创建。

NuGet 包语义版本和 CLR 程序集版本是不同概念。Builder 的 `--version` 参数继续控制 NuGet 包版本;`42.42.42.42424` 控制本仓库运行时程序集身份隔离,不应从任意 NuGet 预发布版本字符串直接推导。

## 不采用的修复

以下方案不能作为该问题的最终修复:

- 修改或降级 `global.json` 中的 Arcade SDK。
- 为 `Demo/WpfDemo` 添加仅对仓库 Demo 生效的特殊程序集解析逻辑。
- 添加 `SkipDirectWriteForwarderProjectReference` 来绕开 NuGet 消费问题。
- 在 `OtherAssemblyAttrs.cpp` 中硬编码临时版本号。
- 只复制 app-local DLL,而不验证实际加载位置。
- 只检查 `.deps.json` 中存在 runtime 资产。
- 使用 self-contained publish 结果代替 framework-dependent build/run 验证。

## MSBuild 与 dotnet build 边界

仓库本身包含 C++/CLI 项目,完整产品构建应继续使用 Builder 找到的 Visual Studio `MSBuild.exe`。`dotnet build` 使用 Core MSBuild,不能可靠承载 Visual C++ targets;手工设置 `VCTargetsPath` 会在 Visual C++ 任务加载阶段产生 MSBuild API 不兼容,不是正确解决方式。

这不影响 NuGet 消费验证:开发者消费已经构建好的 NuGet 包时不应构建仓库内的 vcxproj,消费项目必须能够直接使用普通 `dotnet build` 和 `dotnet run`。

## 当前状态与下一步

当前已完成:

- 真实 framework-dependent build/run 测试能够稳定复现原问题。
- 已确认 app-local 文件存在且 `.deps.json` 已登记时,同身份 forwarder 仍可能由共享框架满足。
- 已确认 `0.0.0.0` 和 `8.0.0.0` 均不能作为本仓库包的隔离程序集身份。
- Builder 已向全部 x86/x64 WPF 运行时项目传播统一程序集版本 `42.42.42.42424`。
- `DirectWriteForwarder` 已显式写入相同的 C++/CLI 托管程序集版本。
- 组包前版本门禁已确认所有收集到的 x86/x64 运行时程序集均为 `42.42.42.42424`。
- `PresentationCore/ModuleInitializer.cs` 已恢复为正常初始化逻辑,只保留 DPI awareness、`DWriteLoader.LoadDWrite()` 和 `NativeWPFDLLLoader.LoadDwrite()`;手工 app-local 加载与 `NoInlining` workaround 已移除。
- 清理后重新生成的 `复包 `WpfLab.WpfRuntime.1.0.0-cleanup-validation.nupkg` 已通过 framework-dependent 消费矩阵。
- 消费矩阵覆盖 .NET 8、.NET 9、win-x86、win-x64、单目标和多目标项目,并通过 app-local 加载、精确 ABI、文本 shaping 与 XAML 控件验证。
- Builder 完整单元测试共 140 项通过。

当前结论:统一程序集身份修复是根本修复,`ModuleInitializer` 不再承担程序集解析 workaround。后续变更不得重新引入 self-contained-only 验证或手工抢先加载来替代 framework-dependent build/run 门禁。
1 change: 1 addition & 0 deletions Docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [05-builder-plan.md](05-builder-plan.md):记录 Builder 的构建、资产收集和打包设计及专题实施细节。
- [07-wpfdemo-implementation.md](07-wpfdemo-implementation.md):记录 WpfDemo 消费仓库 WPF 的实现结构、MSBuild 数据流和扩展约束。
- [08-builder-pr-relay-design.md](08-builder-pr-relay-design.md):设计 Builder 从 GitHub PR 链接搬运提交、本地验证后创建目标 PR,以及 Actions 构建产物回写机制。
- [09-directwrite-forwarder-resolution.md](09-directwrite-forwarder-resolution.md):记录 framework-dependent NuGet 消费时 DirectWriteForwarder 的程序集统一问题、错误测试模型和修复约束。
- [PresentationBuildTasks-bootstrap.md](PresentationBuildTasks-bootstrap.md):说明 `PresentationBuildTasks` 的任务程序集选择、按需构建和锁定输出处理机制。
- [strong-name-signing.md](strong-name-signing.md):说明 WPF 强名称密钥来源、与原始仓库一致的身份映射及修改约束。
- [cycle-breaker.md](cycle-breaker.md):记录循环依赖证据、cycle-breaker 的职责、保留条件和退出条件。
Expand Down
Loading
Loading