diff --git a/Demo/WpfDemo/WpfDemo.csproj b/Demo/WpfDemo/WpfDemo.csproj index 409b858b9..e374bd7bd 100644 --- a/Demo/WpfDemo/WpfDemo.csproj +++ b/Demo/WpfDemo/WpfDemo.csproj @@ -1,4 +1,4 @@ - + @@ -15,7 +15,7 @@ false false - $(DefaultItemExcludes);artifacts\** + $(DefaultItemExcludes);artifacts\** diff --git a/Directory.Build.props b/Directory.Build.props index aef0e7ff3..a725a060f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ true net8.0 8.0 - latest + latest @@ -110,8 +110,8 @@ perl - - $([MSBuild]::GetVsInstallRoot())\Common7\IDE\VC\VCTargets\ + + $([MSBuild]::GetVsInstallRoot())\Common7\IDE\VC\VCTargets\ diff --git a/Directory.Build.targets b/Directory.Build.targets index bc23ecc76..db5687e7a 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,4 +1,8 @@ - + + + $(WpfRuntimeAssemblyVersion) + + .nupkg +WpfLab.WpfRuntime..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 的回退只选择收集结果中一个目录,可能产生不完整报告。 diff --git a/Docs/09-directwrite-forwarder-resolution.md b/Docs/09-directwrite-forwarder-resolution.md new file mode 100644 index 000000000..edb04d075 --- /dev/null +++ b/Docs/09-directwrite-forwarder-resolution.md @@ -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///` 输出目录。 +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 门禁。 diff --git a/Docs/README.md b/Docs/README.md index 26cd3c423..6d656159a 100644 --- a/Docs/README.md +++ b/Docs/README.md @@ -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 的职责、保留条件和退出条件。 diff --git a/eng/Builder.Tests/BuildServiceTests.cs b/eng/Builder.Tests/BuildServiceTests.cs index fb9a286ac..900363afe 100644 --- a/eng/Builder.Tests/BuildServiceTests.cs +++ b/eng/Builder.Tests/BuildServiceTests.cs @@ -81,20 +81,213 @@ public void SystemWindowsPresentationStrongNameIdentityMatchesFriendAssemblyCont } [Fact] - public void PackagePublishEnablesWpfReferenceDiagnostics() + public void PackageBuildEnablesWpfReferenceDiagnostics() { - var arguments = PackageTestService.GetPublishArguments( + var arguments = PackageTestService.GetBuildArguments( "PackageTestApp.csproj", "net9.0-windows", "win-x86", "NuGet.Config", - "packages", - "publish"); + "packages"); Assert.Contains("--property:WpfRuntimeReferenceDiagnostics=true", arguments, StringComparison.Ordinal); Assert.Contains("--property:GenerateTemporaryTargetAssemblyDebuggingInformation=true", arguments, StringComparison.Ordinal); } + [Fact] + public void PackageBuildIsFrameworkDependent() + { + var arguments = PackageTestService.GetBuildArguments( + "PackageTestApp.csproj", + "net8.0-windows", + "win-x86", + "NuGet.Config", + "packages"); + + Assert.Contains("--property:SelfContained=false", arguments, StringComparison.Ordinal); + } + + [Fact] + public void PackagePublishAcceptsWindowsDesktopSharedFrameworkDependency() + { + var publishDirectory = Path.Join(Path.GetTempPath(), $"builder-runtimeconfig-{Guid.NewGuid():N}"); + Directory.CreateDirectory(publishDirectory); + File.WriteAllText( + Path.Join(publishDirectory, "PackageProbe.runtimeconfig.json"), + """ + { + "runtimeOptions": { + "frameworks": [ + { "name": "Microsoft.NETCore.App", "version": "8.0.0" }, + { "name": "Microsoft.WindowsDesktop.App", "version": "8.0.0" } + ] + } + } + """); + + PackageTestService.ValidatePublishedFrameworkDependencies( + publishDirectory, + "PackageProbe", + "net8.0-windows", + "win-x64"); + } + + [Fact] + public void PackagePublishAcceptsSelfContainedFrameworkDependencies() + { + var publishDirectory = Path.Join(Path.GetTempPath(), $"builder-runtimeconfig-{Guid.NewGuid():N}"); + Directory.CreateDirectory(publishDirectory); + File.WriteAllText( + Path.Join(publishDirectory, "PackageProbe.runtimeconfig.json"), + """ + { + "runtimeOptions": { + "includedFrameworks": [ + { "name": "Microsoft.NETCore.App", "version": "8.0.0" }, + { "name": "Microsoft.WindowsDesktop.App", "version": "8.0.0" } + ] + } + } + """); + + PackageTestService.ValidatePublishedFrameworkDependencies( + publishDirectory, + "PackageProbe", + "net8.0-windows", + "win-x86"); + } + + [Fact] + public void PackagePublishRejectsMissingWindowsDesktopSharedFrameworkDependency() + { + var publishDirectory = Path.Join(Path.GetTempPath(), $"builder-runtimeconfig-{Guid.NewGuid():N}"); + Directory.CreateDirectory(publishDirectory); + File.WriteAllText( + Path.Join(publishDirectory, "PackageProbe.runtimeconfig.json"), + """ + { + "runtimeOptions": { + "framework": { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + } + } + } + """); + + var exception = Assert.Throws(() => + PackageTestService.ValidatePublishedFrameworkDependencies( + publishDirectory, + "PackageProbe", + "net8.0-windows", + "win-x64")); + + Assert.Contains("must retain Microsoft.WindowsDesktop.App", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void PackagePublishRejectsMissingDirectWriteForwarderRuntimeDependency() + { + var publishDirectory = Path.Join(Path.GetTempPath(), $"builder-deps-{Guid.NewGuid():N}"); + Directory.CreateDirectory(publishDirectory); + File.WriteAllText( + Path.Join(publishDirectory, "PackageProbe.deps.json"), + """ + { + "targets": { + ".NETCoreApp,Version=v8.0/win-x64": { + "PackageProbe/1.0.0": { + "runtime": { + "PackageProbe.dll": {} + } + } + } + } + } + """); + + var exception = Assert.Throws(() => + PackageTestService.ValidatePublishedRuntimeDependencies( + publishDirectory, + "PackageProbe", + "net8.0-windows", + "win-x64")); + + Assert.Contains("must contain DirectWriteForwarder.dll", exception.Message, StringComparison.Ordinal); + } + + [Fact] + public void PackagePublishAcceptsDirectWriteForwarderRuntimeDependency() + { + var publishDirectory = Path.Join(Path.GetTempPath(), $"builder-deps-{Guid.NewGuid():N}"); + Directory.CreateDirectory(publishDirectory); + File.WriteAllText( + Path.Join(publishDirectory, "PackageProbe.deps.json"), + """ + { + "targets": { + ".NETCoreApp,Version=v8.0/win-x64": { + "DirectWriteForwarder/0.0.0.0": { + "runtime": { + "DirectWriteForwarder.dll": { + "assemblyVersion": "0.0.0.0", + "fileVersion": "0.0.0.0" + } + } + } + } + } + } + """); + + PackageTestService.ValidatePublishedRuntimeDependencies( + publishDirectory, + "PackageProbe", + "net8.0-windows", + "win-x64"); + } + + [Fact] + public void PackagePublishAcceptsRestoredDependencyProvidedBySharedFramework() + { + var assetsPath = Path.Join(Path.GetTempPath(), $"builder-assets-{Guid.NewGuid():N}.json"); + File.WriteAllText( + assetsPath, + """ + { + "libraries": { + "System.Configuration.ConfigurationManager/8.0.0": { + "type": "package" + } + } + } + """); + + PackageTestService.ValidateRestoredPackageDependencies( + assetsPath, + [new PackageDependency("System.Configuration.ConfigurationManager", "8.0.0")], + "PackageProbe", + "net8.0-windows", + "win-x86"); + } + + [Fact] + public void PackagePublishRejectsDependencyMissingFromRestoreAssets() + { + var assetsPath = Path.Join(Path.GetTempPath(), $"builder-assets-{Guid.NewGuid():N}.json"); + File.WriteAllText(assetsPath, """{ "libraries": {} }"""); + + var exception = Assert.Throws(() => + PackageTestService.ValidateRestoredPackageDependencies( + assetsPath, + [new PackageDependency("System.Configuration.ConfigurationManager", "8.0.0")], + "PackageProbe", + "net8.0-windows", + "win-x86")); + + Assert.Contains("was not restored", exception.Message, StringComparison.Ordinal); + } + [Fact] public void RuntimeAssembliesAreBuiltInReleaseWithPortableSymbols() { @@ -104,11 +297,34 @@ public void RuntimeAssembliesAreBuiltInReleaseWithPortableSymbols() "build.log"); Assert.Contains( - "/p:Configuration=Release /p:Platform=x86 /p:DebugSymbols=true /p:DebugType=portable", + $"/p:Configuration=Release /p:Platform=x86 /p:WpfRuntimeAssemblyVersion={PackageMetadata.RuntimeAssemblyVersion} /p:DebugSymbols=true /p:DebugType=portable", arguments, StringComparison.Ordinal); } + [Fact] + public void RuntimeAssemblyVersionUsesAppLocalIsolationIdentity() + { + Assert.Equal("42.42.42.42424", PackageMetadata.RuntimeAssemblyVersion); + } + + [Fact] + public void DirectWriteForwarderAssemblyVersionFallsBackForNonPackageBuilds() + { + string project = File.ReadAllText(Path.Join( + FindRepositoryRoot(), + "src", + "Microsoft.DotNet.Wpf", + "src", + "DirectWriteForwarder", + "DirectWriteForwarder.vcxproj")); + + Assert.Contains( + "$(AssemblyVersion)", + project, + StringComparison.Ordinal); + } + [Theory] [InlineData("x64", "net472")] [InlineData("x64", "net8.0")] diff --git a/eng/Builder.Tests/Builder.Tests.csproj b/eng/Builder.Tests/Builder.Tests.csproj index c1a9e8288..3ee71bfc1 100644 --- a/eng/Builder.Tests/Builder.Tests.csproj +++ b/eng/Builder.Tests/Builder.Tests.csproj @@ -22,7 +22,8 @@ - + win-x64", + targetsContent, + StringComparison.Ordinal); + } + + [Fact] + public void GenerateBuildTransitiveTargetsUsesX86RuntimeWhenPrefer32BitIsEnabled() + { + var stagingDirectory = CreateStagingDirectory(); + + NuGetPackageService.GenerateBuildTransitiveTargets(stagingDirectory); + var targetsContent = File.ReadAllText( + Path.Join(stagingDirectory, "buildTransitive", $"{PackageMetadata.Id}.targets")); + + Assert.Contains( + "Or '$(Prefer32Bit)' == 'true')\">win-x86", + targetsContent, + StringComparison.Ordinal); + } + [Fact] public void GenerateBuildTransitiveTargetsRunsBeforeWpfCompilationStages() { @@ -254,6 +297,55 @@ public void GenerateBuildTransitiveTargetsCopiesNativeAssetsForInferredRuntimeId StringComparison.Ordinal); } + [Fact] + public void GenerateBuildTransitiveTargetsRegistersDirectWriteForwarderAsPrivateReference() + { + var stagingDirectory = CreateStagingDirectory(); + + NuGetPackageService.GenerateBuildTransitiveTargets(stagingDirectory); + var targetsContent = File.ReadAllText( + Path.Join(stagingDirectory, "buildTransitive", $"{PackageMetadata.Id}.targets")); + + Assert.Contains( + @"", + targetsContent, + StringComparison.Ordinal); + Assert.Contains( + @"$(MSBuildThisFileDirectory)..\runtimes\$(_DotNetCampusWpfRuntimeIdentifier)\lib\net8.0\DirectWriteForwarder.dll", + targetsContent, + StringComparison.Ordinal); + Assert.Contains( + "true", + targetsContent, + StringComparison.Ordinal); + } + + [Fact] + public void GenerateBuildTransitiveTargetsPreservesWindowsDesktopFrameworkReferences() + { + var stagingDirectory = CreateStagingDirectory(); + + NuGetPackageService.GenerateBuildTransitiveTargets(stagingDirectory); + var targetsContent = File.ReadAllText( + Path.Join(stagingDirectory, "buildTransitive", $"{PackageMetadata.Id}.targets")); + + Assert.DoesNotContain(" CollectRuntimeDlls(string repoRoot, str } } - return result; - } - + return result; + } + + public static void ValidateRuntimeAssemblyVersions( + IReadOnlyDictionary runtimeDlls, + string expectedVersion, + string rid) + { + ArgumentNullException.ThrowIfNull(runtimeDlls); + ArgumentException.ThrowIfNullOrWhiteSpace(expectedVersion); + ArgumentException.ThrowIfNullOrWhiteSpace(rid); + + var expected = Version.Parse(expectedVersion); + foreach (var (name, path) in runtimeDlls) + { + Version actual = ReadAssemblyVersion(path); + Log.Info($" Validated runtime assembly version for {rid}: {name} {actual}"); + if (!actual.Equals(expected)) + { + throw new InvalidOperationException( + $"Runtime assembly '{name}' for {rid} must have assembly version {expected}; actual version is {actual}: {path}"); + } + } + } + + private static Version ReadAssemblyVersion(string path) + { + using var stream = File.OpenRead(path); + using var peReader = new PEReader(stream); + if (!peReader.HasMetadata) + throw new InvalidOperationException($"Runtime file is not a managed assembly: {path}"); + + MetadataReader reader = peReader.GetMetadataReader(); + return reader.GetAssemblyDefinition().Version; + } + public static string? GetPdbPath(string assemblyPath) { ArgumentException.ThrowIfNullOrWhiteSpace(assemblyPath); diff --git a/eng/Builder/BuildService.cs b/eng/Builder/BuildService.cs index 3784bc34b..a98e3338a 100644 --- a/eng/Builder/BuildService.cs +++ b/eng/Builder/BuildService.cs @@ -156,12 +156,22 @@ public static int Run(BuilderContext context, string version) foreach (var (rid, platform) in new[] { ("win-x64", "x64"), ("win-x86", "x86") }) { var runtimeDlls = AssemblyCollector.CollectRuntimeDlls(context.RepoRoot, context.ArtifactsDir, platform); - if (runtimeDlls.Count == 0) - { - Log.Error($"No runtime assemblies found for {rid}; please check build artifacts"); - return 1; - } - + if (runtimeDlls.Count == 0) + { + Log.Error($"No runtime assemblies found for {rid}; please check build artifacts"); + return 1; + } + + try + { + AssemblyCollector.ValidateRuntimeAssemblyVersions(runtimeDlls, PackageMetadata.RuntimeAssemblyVersion, rid); + } + catch (InvalidOperationException exception) + { + Log.Error(exception.Message); + return 1; + } + var runtimeLibDir = Path.Join(context.StagingDir, "runtimes", rid, "lib", "net8.0"); Directory.CreateDirectory(runtimeLibDir); foreach (var (name, sourcePath) in runtimeDlls) @@ -235,7 +245,7 @@ internal static string GetRuntimeBuildArguments( string projectPath, string platform, string logPath) => - $"\"{projectPath}\" -restore /p:Configuration=Release /p:Platform={platform} /p:DebugSymbols=true /p:DebugType=portable /p:UsePrebuiltPresentationBuildTasks=true /p:BuildPresentationBuildTasksOnDemand=false /m:1 /nr:false /v:minimal /clp:ErrorsOnly{MsBuildService.GetFileLoggerArguments(logPath)}"; + $"\"{projectPath}\" -restore /p:Configuration=Release /p:Platform={platform} /p:WpfRuntimeAssemblyVersion={PackageMetadata.RuntimeAssemblyVersion} /p:DebugSymbols=true /p:DebugType=portable /p:UsePrebuiltPresentationBuildTasks=true /p:BuildPresentationBuildTasksOnDemand=false /m:1 /nr:false /v:minimal /clp:ErrorsOnly{MsBuildService.GetFileLoggerArguments(logPath)}"; internal static string GetPresentationBuildTasksBuildArguments( string projectPath, diff --git a/eng/Builder/NuGetPackageService.cs b/eng/Builder/NuGetPackageService.cs index 0a7cde27f..3790454c9 100644 --- a/eng/Builder/NuGetPackageService.cs +++ b/eng/Builder/NuGetPackageService.cs @@ -235,10 +235,10 @@ public static void GenerateBuildTransitiveFiles(string stagingDir) Directory.CreateDirectory(buildTransitiveDir); var propsPath = Path.Join(buildTransitiveDir, $"{PackageMetadata.Id}.props"); var propsContent = """ - - - <_PresentationBuildTasksAssembly>$(MSBuildThisFileDirectory)..\tools\net8.0\PresentationBuildTasks.dll - + + + <_PresentationBuildTasksAssembly>$(MSBuildThisFileDirectory)..\tools\net8.0\PresentationBuildTasks.dll + """; File.WriteAllText(propsPath, propsContent); @@ -257,16 +257,20 @@ public static void GenerateBuildTransitiveTargets(string stagingDir) <_DotNetCampusWpfRuntimeIdentifier Condition="'$(RuntimeIdentifier)' == 'win-x86' Or '$(RuntimeIdentifier)' == 'win-x64'">$(RuntimeIdentifier) <_DotNetCampusWpfRuntimeIdentifier Condition="'$(_DotNetCampusWpfRuntimeIdentifier)' == '' And ('$(PlatformTarget)' == 'x64' Or '$(Platform)' == 'x64')">win-x64 - <_DotNetCampusWpfRuntimeIdentifier Condition="'$(_DotNetCampusWpfRuntimeIdentifier)' == '' And ('$(PlatformTarget)' == 'x86' Or '$(Platform)' == 'x86' Or '$(Platform)' == 'Win32')">win-x86 + <_DotNetCampusWpfRuntimeIdentifier Condition="'$(_DotNetCampusWpfRuntimeIdentifier)' == '' And ('$(PlatformTarget)' == 'x86' Or '$(Platform)' == 'x86' Or '$(Platform)' == 'Win32' Or '$(Prefer32Bit)' == 'true')">win-x86 + <_DotNetCampusWpfRuntimeIdentifier Condition="'$(_DotNetCampusWpfRuntimeIdentifier)' == '' And '$(NETCoreSdkRuntimeIdentifier)' == 'win-x64'">win-x64 + <_DotNetCampusWpfRuntimeIdentifier Condition="'$(_DotNetCampusWpfRuntimeIdentifier)' == '' And '$(NETCoreSdkRuntimeIdentifier)' == 'win-x86'">win-x86 - - - - - <_DotNetCampusWpfRuntimeDll Include="$(MSBuildThisFileDirectory)..\runtimes\$(_DotNetCampusWpfRuntimeIdentifier)\lib\net8.0\*.dll" /> - <_DotNetCampusWpfRuntimeDll Include="$(MSBuildThisFileDirectory)..\runtimes\$(_DotNetCampusWpfRuntimeIdentifier)\native\*.dll" /> + <_DotNetCampusWpfManagedRuntimeDll Include="$(MSBuildThisFileDirectory)..\runtimes\$(_DotNetCampusWpfRuntimeIdentifier)\lib\net8.0\*.dll" + Exclude="$(MSBuildThisFileDirectory)..\runtimes\$(_DotNetCampusWpfRuntimeIdentifier)\lib\net8.0\ijwhost.dll" /> + <_DotNetCampusWpfRuntimeDll Include="@(_DotNetCampusWpfManagedRuntimeDll)" /> + <_DotNetCampusWpfRuntimeDll Include="$(MSBuildThisFileDirectory)..\runtimes\$(_DotNetCampusWpfRuntimeIdentifier)\native\*.dll" /> + + $(MSBuildThisFileDirectory)..\runtimes\$(_DotNetCampusWpfRuntimeIdentifier)\lib\net8.0\DirectWriteForwarder.dll + true + @@ -288,6 +292,15 @@ public static void GenerateBuildTransitiveTargets(string stagingDir) MatchOnMetadata="Filename" /> + + + TargetFrameworks { get; } = ["net8.0", "net9.0"]; } diff --git a/eng/Builder/PackageTestApp/MainWindow.xaml.cs b/eng/Builder/PackageTestApp/MainWindow.xaml.cs index 8b86c6a30..3d1b4e50e 100644 --- a/eng/Builder/PackageTestApp/MainWindow.xaml.cs +++ b/eng/Builder/PackageTestApp/MainWindow.xaml.cs @@ -1,5 +1,7 @@ +using System.Globalization; using System.IO; -using System.Reflection; +using System.Reflection; +using System.Security.Cryptography; using System.Runtime.Versioning; using System.Windows; using System.Windows.Controls; @@ -8,8 +10,10 @@ namespace PackageTestApp; -public partial class MainWindow : Window -{ +public partial class MainWindow : Window +{ + private static readonly Version ExpectedWpfAssemblyVersion = new(42, 42, 42, 42424); + public MainWindow() { InitializeComponent(); @@ -26,16 +30,20 @@ private void ValidateAndClose() try { ValidateRuntimeVersion(); - ValidateWpfAssembly(typeof(DependencyObject).Assembly, "WindowsBase.dll"); - ValidateWpfAssembly(typeof(Visual).Assembly, "PresentationCore.dll"); - ValidateWpfAssembly(typeof(Application).Assembly, "PresentationFramework.dll"); + ValidateWpfAssembly(typeof(DependencyObject).Assembly, "WindowsBase.dll"); + ValidateWpfAssembly(typeof(Visual).Assembly, "PresentationCore.dll"); + ValidateWpfAssembly(typeof(Application).Assembly, "PresentationFramework.dll"); + Assembly directWriteForwarder = LoadAssembly("DirectWriteForwarder"); + ValidateWpfAssembly(directWriteForwarder, "DirectWriteForwarder.dll"); + ValidateDirectWriteItemizeAbi(directWriteForwarder); + ValidateTextShaping(); ValidateControls(); StatusTextBlock.Text = "Validation passed"; Console.WriteLine($"WPF XAML package probe completed on {Environment.ProcessPath}."); Application.Current.Shutdown(0); } - catch (Exception exception) when (exception is InvalidOperationException or IOException) + catch (Exception exception) when (exception is InvalidOperationException or IOException or MissingMethodException) { Console.Error.WriteLine(exception); StatusTextBlock.Text = "Validation failed"; @@ -43,6 +51,67 @@ private void ValidateAndClose() } } + private static void ValidateTextShaping() + { + FormattedText formattedText = new( + "WPF package text shaping probe", + CultureInfo.GetCultureInfo("en-US"), + FlowDirection.LeftToRight, + new Typeface("Segoe UI"), + 12, + Brushes.Black, + 1); + + if (formattedText.Width <= 0) + throw new InvalidOperationException("WPF text shaping returned an invalid width."); + + Console.WriteLine($"Validated WPF text shaping width {formattedText.Width:F2}."); + } + + private static Assembly LoadAssembly(string assemblyName) => + AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => + string.Equals(assembly.GetName().Name, assemblyName, StringComparison.Ordinal)) + ?? Assembly.Load(assemblyName); + + private static void ValidateDirectWriteItemizeAbi(Assembly directWriteForwarder) + { + Type textAnalyzer = directWriteForwarder.GetType("MS.Internal.Text.TextInterface.TextAnalyzer", throwOnError: true)!; + MethodInfo[] itemizeMethods = textAnalyzer + .GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + .Where(method => string.Equals(method.Name, "Itemize", StringComparison.Ordinal)) + .ToArray(); + + MethodInfo? itemize = itemizeMethods.SingleOrDefault(method => + { + ParameterInfo[] parameters = method.GetParameters(); + return parameters.Length == 13 && + parameters[0].ParameterType.IsPointer && + parameters[0].ParameterType.GetElementType() == typeof(char) && + parameters[1].ParameterType == typeof(uint) && + parameters[2].ParameterType == typeof(CultureInfo) && + parameters[4].ParameterType == typeof(bool) && + parameters[5].ParameterType == typeof(CultureInfo) && + parameters[6].ParameterType == typeof(bool) && + parameters[7].ParameterType == typeof(uint); + }); + + if (itemize is null) + { + string actualSignatures = string.Join( + Environment.NewLine, + itemizeMethods.Select(method => $" {FormatMethodSignature(method)}")); + throw new MissingMethodException( + "DirectWriteForwarder does not expose the 13-parameter TextAnalyzer.Itemize ABI required by PresentationCore." + + Environment.NewLine + actualSignatures); + } + + Console.WriteLine($"Validated DirectWrite ABI: {FormatMethodSignature(itemize)}"); + } + + private static string FormatMethodSignature(MethodInfo method) => + $"{method.ReturnType} {method.DeclaringType?.FullName}.{method.Name}(" + + string.Join(", ", method.GetParameters().Select(parameter => parameter.ParameterType.ToString())) + ")"; + private void ValidateControls() { RequireControl(ContentTabs, nameof(ContentTabs)); @@ -99,15 +168,24 @@ private static void ValidateWpfAssembly(Assembly assembly, string expectedFileNa $"{assembly.GetName().Name} was loaded from '{actualPath}' instead of package output '{expectedPath}'."); } - var targetFramework = assembly.GetCustomAttribute()?.FrameworkName; + Version? assemblyVersion = assembly.GetName().Version; + if (assemblyVersion != ExpectedWpfAssemblyVersion) + { + throw new InvalidOperationException( + $"{assembly.GetName().Name} must have package assembly version {ExpectedWpfAssemblyVersion}, actual: {assemblyVersion?.ToString() ?? "missing"}."); + } + + var targetFramework = assembly.GetCustomAttribute()?.FrameworkName; if (!string.Equals(targetFramework, ".NETCoreApp,Version=v8.0", StringComparison.Ordinal)) { throw new InvalidOperationException( $"{assembly.GetName().Name} must remain a .NET 8 assembly, actual target framework: {targetFramework ?? "missing"}."); } - Console.WriteLine( - $"Loaded {assembly.GetName().Name} {assembly.GetName().Version} ({targetFramework}) from {actualPath}."); + string sha256 = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(actualPath))); + Console.WriteLine( + $"Loaded {assembly.GetName().Name} {assembly.GetName().Version} ({targetFramework}) from {actualPath}; " + + $"MVID={assembly.ManifestModule.ModuleVersionId}; SHA256={sha256}."); } private void OnActionButtonClick(object sender, RoutedEventArgs e) diff --git a/eng/Builder/PackageTestService.cs b/eng/Builder/PackageTestService.cs index df33dfa7f..7252a9689 100644 --- a/eng/Builder/PackageTestService.cs +++ b/eng/Builder/PackageTestService.cs @@ -1,5 +1,6 @@ using System.IO.Compression; using System.Security.Cryptography; +using System.Text.Json; using System.Xml.Linq; namespace WpfReorganize.Builder; @@ -42,7 +43,7 @@ public static int Run(BuilderContext context, string? packageArg) { foreach (var rid in new[] { "win-x86", "win-x64" }) { - PublishAndValidatePackageTest( + BuildAndValidatePackageTest( testProject, targetFramework, rid, @@ -54,12 +55,12 @@ public static int Run(BuilderContext context, string? packageArg) } } - Log.Info("Package publish validation passed for all projects, target frameworks, and runtime identifiers."); + Log.Info("Package build and runtime validation passed for all projects, target frameworks, and runtime identifiers."); return 0; } -static void PublishAndValidatePackageTest( +static void BuildAndValidatePackageTest( PackageTestProject testProject, string targetFramework, string rid, @@ -68,31 +69,41 @@ static void PublishAndValidatePackageTest( string nugetConfigPath, IReadOnlyList runtimePackageDependencies) { - var publishDir = Path.Join(testRoot, "publish", testProject.Name, targetFramework, rid); - var restorePackagesDir = Path.Join(testRoot, "restore-packages"); - Directory.CreateDirectory(publishDir); - Log.Step($"Publishing {testProject.Name} for {targetFramework}/{rid}..."); - - var arguments = GetPublishArguments( + var projectDirectory = Path.GetDirectoryName(testProject.ProjectPath)!; + var outputDir = Path.Join(projectDirectory, "bin", "Release", targetFramework, rid); + var restorePackagesDir = Path.Join(testRoot, "restore-packages"); + Log.Step($"Building {testProject.Name} for {targetFramework}/{rid}..."); + + var arguments = GetBuildArguments( testProject.ProjectPath, targetFramework, rid, nugetConfigPath, - restorePackagesDir, - publishDir); - var result = ProcessRunner.Run("dotnet", arguments, Path.GetDirectoryName(testProject.ProjectPath)!); - if (result.ExitCode != 0) - { - Log.Error(result.Output); - throw new InvalidOperationException($"Package test publish failed for {testProject.Name} ({targetFramework}/{rid})"); - } - - ValidatePublishedPackageDlls(extractedPackageDir, publishDir, rid, testProject.Name, targetFramework); - foreach (var dependency in runtimePackageDependencies) - { - ValidatePublishedDependencyDll(publishDir, $"{dependency.Id}.dll", testProject.Name, targetFramework, rid); + restorePackagesDir); + var result = ProcessRunner.Run("dotnet", arguments, Path.GetDirectoryName(testProject.ProjectPath)!); + if (result.ExitCode != 0) + { + Log.Error(result.Output); + throw new InvalidOperationException($"Package test build failed for {testProject.Name} ({targetFramework}/{rid})"); + } + + ValidatePublishedPackageDlls(extractedPackageDir, outputDir, rid, testProject.Name, targetFramework); + ValidatePublishedFrameworkDependencies(outputDir, testProject.Name, targetFramework, rid); + ValidatePublishedRuntimeDependencies(outputDir, testProject.Name, targetFramework, rid); + ValidateRestoredPackageDependencies( + Path.Join(Path.GetDirectoryName(testProject.ProjectPath)!, "obj", "project.assets.json"), + runtimePackageDependencies, + testProject.Name, + targetFramework, + rid); + if (string.Equals(rid, "win-x64", StringComparison.OrdinalIgnoreCase)) + { + RunBuiltPackageProbe(testProject.ProjectPath, testProject.Name, targetFramework, rid, outputDir); + } + else + { + Log.Info($"Validated build outputs for {testProject.Name} ({targetFramework}/{rid}); runtime probe is limited to win-x64."); } - RunPublishedPackageProbe(testProject.Name, targetFramework, rid, publishDir); } static void ValidatePackageDependencies( @@ -221,21 +232,106 @@ static void ValidatePublishedPackageDlls( Log.Info($"Validated {expectedDlls.Count} package DLLs for {projectName} ({targetFramework}/{rid})"); } -static void ValidatePublishedDependencyDll( +internal static void ValidatePublishedFrameworkDependencies( + string publishDir, + string projectName, + string targetFramework, + string rid) +{ + var runtimeConfigPath = Path.Join(publishDir, $"{projectName}.runtimeconfig.json"); + if (!File.Exists(runtimeConfigPath)) + throw new InvalidOperationException($"Published runtime configuration is missing: {runtimeConfigPath}"); + + using var document = JsonDocument.Parse(File.ReadAllBytes(runtimeConfigPath)); + var runtimeOptions = document.RootElement.GetProperty("runtimeOptions"); + var frameworkNames = new List(); + if (runtimeOptions.TryGetProperty("framework", out var framework)) + frameworkNames.Add(framework.GetProperty("name").GetString() ?? string.Empty); + + if (runtimeOptions.TryGetProperty("frameworks", out var frameworks)) + { + frameworkNames.AddRange(frameworks.EnumerateArray().Select(item => + item.GetProperty("name").GetString() ?? string.Empty)); + } + + if (runtimeOptions.TryGetProperty("includedFrameworks", out var includedFrameworks)) + { + frameworkNames.AddRange(includedFrameworks.EnumerateArray().Select(item => + item.GetProperty("name").GetString() ?? string.Empty)); + } + + if (!frameworkNames.Contains("Microsoft.NETCore.App", StringComparer.Ordinal)) + { + throw new InvalidOperationException( + $"Published application must retain Microsoft.NETCore.App for {projectName} ({targetFramework}/{rid})."); + } + + var windowsDesktopFramework = frameworkNames.FirstOrDefault(name => + name.StartsWith("Microsoft.WindowsDesktop.App", StringComparison.Ordinal)); + if (windowsDesktopFramework is null) + { + throw new InvalidOperationException( + $"Published application must retain Microsoft.WindowsDesktop.App for {projectName} ({targetFramework}/{rid})."); + } + + Log.Info($"Validated framework dependencies for {projectName} ({targetFramework}/{rid}): {string.Join(", ", frameworkNames)}"); +} + +internal static void ValidatePublishedRuntimeDependencies( string publishDir, - string fileName, string projectName, string targetFramework, string rid) { - var dependencyPath = Path.Join(publishDir, fileName); - if (!File.Exists(dependencyPath)) + var depsPath = Path.Join(publishDir, $"{projectName}.deps.json"); + if (!File.Exists(depsPath)) + throw new InvalidOperationException($"Published dependency manifest is missing: {depsPath}"); + + using var document = JsonDocument.Parse(File.ReadAllBytes(depsPath)); + var containsDirectWriteForwarder = document.RootElement + .GetProperty("targets") + .EnumerateObject() + .SelectMany(target => target.Value.EnumerateObject()) + .Any(library => + library.Value.TryGetProperty("runtime", out var runtimeAssets) && + runtimeAssets.EnumerateObject().Any(asset => + string.Equals(Path.GetFileName(asset.Name), "DirectWriteForwarder.dll", StringComparison.OrdinalIgnoreCase))); + + if (!containsDirectWriteForwarder) { throw new InvalidOperationException( - $"Published NuGet dependency is missing for {projectName} ({targetFramework}/{rid}): {dependencyPath}"); + $"Published dependency manifest must contain DirectWriteForwarder.dll for {projectName} ({targetFramework}/{rid}); " + + "copying the file without registering it as a runtime asset does not override host assembly resolution."); } - Log.Info($"Validated published dependency {fileName} for {projectName} ({targetFramework}/{rid})"); + Log.Info($"Validated DirectWriteForwarder runtime dependency for {projectName} ({targetFramework}/{rid})"); +} + +internal static void ValidateRestoredPackageDependencies( + string assetsPath, + IReadOnlyList expectedDependencies, + string projectName, + string targetFramework, + string rid) +{ + if (!File.Exists(assetsPath)) + throw new InvalidOperationException($"Package restore assets are missing: {assetsPath}"); + + using var document = JsonDocument.Parse(File.ReadAllBytes(assetsPath)); + var libraries = document.RootElement.GetProperty("libraries"); + foreach (var dependency in expectedDependencies) + { + var libraryName = $"{dependency.Id}/{dependency.Version}"; + if (!libraries.TryGetProperty(libraryName, out var library) || + !library.TryGetProperty("type", out var type) || + !string.Equals(type.GetString(), "package", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"NuGet dependency was not restored for {projectName} ({targetFramework}/{rid}): {libraryName}"); + } + } + + Log.Info($"Validated {expectedDependencies.Count} restored NuGet dependencies for {projectName} ({targetFramework}/{rid})"); } static string FindRuntimeLibDirectory(string extractedPackageDir, string rid) @@ -260,25 +356,26 @@ static byte[] ComputeSha256(string path) return SHA256.HashData(stream); } -static void RunPublishedPackageProbe(string projectName, string targetFramework, string rid, string publishDir) -{ - var executablePath = Path.Join(publishDir, $"{projectName}.exe"); - if (!File.Exists(executablePath)) - throw new InvalidOperationException($"Published package test executable was not found: {executablePath}"); - - Log.Info($"Running {projectName} ({targetFramework}/{rid})..."); - var result = ProcessRunner.Run(executablePath, "", publishDir, TimeSpan.FromSeconds(30)); - if (result.ExitCode != 0) - { - Log.Error(result.Output); - throw new InvalidOperationException( - $"Published package test failed for {projectName} ({targetFramework}/{rid}) with exit code {result.ExitCode}"); - } - - if (!string.IsNullOrWhiteSpace(result.Output)) - Log.Info(result.Output.Trim()); - - Log.Info($"Probe completed for {projectName} ({targetFramework}/{rid}) in {result.Elapsed.TotalSeconds:F1}s"); +static void RunBuiltPackageProbe(string projectPath, string projectName, string targetFramework, string rid, string outputDir) +{ + var executablePath = Path.Join(outputDir, $"{projectName}.exe"); + if (!File.Exists(executablePath)) + throw new InvalidOperationException($"Built package test executable was not found: {executablePath}"); + + Log.Info($"Running {projectName} ({targetFramework}/{rid}) with dotnet run --no-build..."); + string arguments = $"run --project \"{projectPath}\" --configuration Release --framework {targetFramework} --runtime {rid} --no-build --no-restore"; + var result = ProcessRunner.Run("dotnet", arguments, Path.GetDirectoryName(projectPath)!, TimeSpan.FromSeconds(30)); + if (result.ExitCode != 0) + { + Log.Error(result.Output); + throw new InvalidOperationException( + $"Built package test failed for {projectName} ({targetFramework}/{rid}) with exit code {result.ExitCode}"); + } + + if (!string.IsNullOrWhiteSpace(result.Output)) + Log.Info(result.Output.Trim()); + + Log.Info($"Probe completed for {projectName} ({targetFramework}/{rid}) in {result.Elapsed.TotalSeconds:F1}s"); } static string ReadPackageVersion(string packagePath) @@ -390,14 +487,13 @@ static void CopyPackageTestProjectTemplate(string sourceDir, string destinationD } } -internal static string GetPublishArguments( +internal static string GetBuildArguments( string projectPath, string targetFramework, string rid, string nugetConfigPath, - string restorePackagesDir, - string publishDir) => - $"publish \"{projectPath}\" --configuration Release --framework {targetFramework} --runtime {rid} --self-contained true --configfile \"{nugetConfigPath}\" --packages \"{restorePackagesDir}\" --output \"{publishDir}\" --nologo --property:WpfRuntimeReferenceDiagnostics=true --property:GenerateTemporaryTargetAssemblyDebuggingInformation=true"; + string restorePackagesDir) => + $"build \"{projectPath}\" --configuration Release --framework {targetFramework} --runtime {rid} --configfile \"{nugetConfigPath}\" --packages \"{restorePackagesDir}\" --nologo --property:SelfContained=false --property:WpfRuntimeReferenceDiagnostics=true --property:GenerateTemporaryTargetAssemblyDebuggingInformation=true"; static string XmlEscape(string value) => value.Replace("&", "&", StringComparison.Ordinal) diff --git a/eng/WpfArcadeSdk/SystemResources.props b/eng/WpfArcadeSdk/SystemResources.props index b3000e924..bedd52289 100644 --- a/eng/WpfArcadeSdk/SystemResources.props +++ b/eng/WpfArcadeSdk/SystemResources.props @@ -1,19 +1,19 @@ - - - true - - - true - - + + + true + + + true + + true diff --git a/global.json b/global.json index b6eb43eb1..0b222256f 100644 --- a/global.json +++ b/global.json @@ -1,4 +1,4 @@ -{ +{ "sdk": { "version": "8.0.101", "rollForward": "latestFeature" diff --git a/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/DirectWriteForwarder.vcxproj b/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/DirectWriteForwarder.vcxproj index b32283a63..d5b15c434 100644 --- a/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/DirectWriteForwarder.vcxproj +++ b/src/Microsoft.DotNet.Wpf/src/DirectWriteForwarder/DirectWriteForwarder.vcxproj @@ -1,7 +1,7 @@ - - C:\Program Files\Microsoft Visual Studio\18\Professional\MSBuild\Microsoft\VC\v180\ + + C:\Program Files\Microsoft Visual Studio\18\Professional\MSBuild\Microsoft\VC\v180\ C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Microsoft\VC\v170\ @@ -32,10 +32,10 @@ x64 - - true - net8.0 - Unknown + + true + net8.0 + Unknown - - TargetFramework;TargetFrameworks + + TargetFramework;TargetFrameworks diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/PresentationFramework.csproj b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/PresentationFramework.csproj index 0bdeb9fa3..3b08b3b96 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/PresentationFramework.csproj +++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/PresentationFramework.csproj @@ -1,4 +1,4 @@ - + $(DefineConstants);FRAMEWORK_NATIVEMETHODS;COMMONDPS;PRESENTATIONFRAMEWORK_ONLY;PRESENTATIONFRAMEWORK;RIBBON_IN_FRAMEWORK @@ -1439,8 +1439,8 @@ - - TargetFramework;TargetFrameworks + + TargetFramework;TargetFrameworks diff --git a/src/Microsoft.DotNet.Wpf/src/ReachFramework/ReachFramework.csproj b/src/Microsoft.DotNet.Wpf/src/ReachFramework/ReachFramework.csproj index a08fbf997..1fcb4911c 100644 --- a/src/Microsoft.DotNet.Wpf/src/ReachFramework/ReachFramework.csproj +++ b/src/Microsoft.DotNet.Wpf/src/ReachFramework/ReachFramework.csproj @@ -1,4 +1,4 @@ - + $(DefineConstants);REACHFRAMEWORK; true @@ -373,8 +373,8 @@ - - TargetFramework;TargetFrameworks + + TargetFramework;TargetFrameworks diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Media/FormattedText.Tests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Media/FormattedText.Tests.cs new file mode 100644 index 000000000..c47fea213 --- /dev/null +++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Media/FormattedText.Tests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace System.Windows.Media; + +public sealed class FormattedTextTests +{ + [StaFact] + public void Constructor_WhenTextIsMeasured_DoesNotThrow() + { + FormattedText formattedText = new( + "WPF text shaping probe", + CultureInfo.GetCultureInfo("en-US"), + FlowDirection.LeftToRight, + new Typeface("Segoe UI"), + 12, + Brushes.Black, + 1); + + double width = formattedText.Width; + + width.Should().BeGreaterThan(0); + } +}