From b80f46b6fab015d658e8bd4a34dade100eacb8b1 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:46:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(build):=20=E6=B7=BB=E5=8A=A0=20GFramew?= =?UTF-8?q?ork=20=E6=A8=A1=E5=9D=97=E5=8C=96=E5=85=A8=E5=B1=80=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E7=A9=BA=E9=97=B4=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 NuGet 包中实现可选的 transitive global usings 功能 - 添加 XML 配置方式启用模块级自动命名空间导入 - 支持通过 GFrameworkExcludedUsing 排除特定命名空间 - 为所有运行时模块生成对应的 buildTransitive props 文件 - 添加 Python 脚本自动生成和验证命名空间配置 - 在文档中添加新的安装配置说明 - 创建单元测试验证生成脚本的同步状态 --- .../GFramework.Core.Abstractions.csproj | 5 + ...GeWuYou.GFramework.Core.Abstractions.props | 47 ++++ .../TransitiveGlobalUsingsGenerationTests.cs | 74 ++++++ GFramework.Core/GFramework.Core.csproj | 5 + .../GeWuYou.GFramework.Core.props | 53 +++++ .../GFramework.Ecs.Arch.Abstractions.csproj | 6 +- ...You.GFramework.Ecs.Arch.Abstractions.props | 10 + .../GFramework.Ecs.Arch.csproj | 6 +- .../GeWuYou.GFramework.Ecs.Arch.props | 13 + .../GFramework.Game.Abstractions.csproj | 5 + ...GeWuYou.GFramework.Game.Abstractions.props | 19 ++ GFramework.Game/GFramework.Game.csproj | 5 + .../GeWuYou.GFramework.Game.props | 21 ++ GFramework.Godot/GFramework.Godot.csproj | 5 + .../GeWuYou.GFramework.Godot.props | 22 ++ README.md | 26 ++ docs/zh-CN/getting-started/installation.md | 29 ++- global-usings.modules.json | 161 +++++++++++++ scripts/generate-module-global-usings.py | 222 ++++++++++++++++++ 19 files changed, 722 insertions(+), 12 deletions(-) create mode 100644 GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props create mode 100644 GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs create mode 100644 GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props create mode 100644 GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props create mode 100644 GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props create mode 100644 GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props create mode 100644 GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props create mode 100644 GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props create mode 100644 global-usings.modules.json create mode 100644 scripts/generate-module-global-usings.py diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj index f0cec86..d5f1779 100644 --- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj +++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj @@ -27,4 +27,9 @@ + + + + + diff --git a/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props b/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props new file mode 100644 index 0000000..67cca44 --- /dev/null +++ b/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props @@ -0,0 +1,47 @@ + + + + + + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Architectures"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Bases"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Command"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Concurrency"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Configuration"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Controller"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Coroutine"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Command"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Notification"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Query"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Request"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Data"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Enums"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Environment"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Events"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Ioc"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Lifecycle"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Localization"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Logging"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Model"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Pause"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Pool"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Properties"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Property"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Query"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Registries"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Resource"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Rule"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Serializer"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.State"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.StateManagement"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Storage"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Systems"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Time"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Utility"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Utility.Numeric"/> + <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Versioning"/> + <_GFramework_Core_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> + + + diff --git a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs new file mode 100644 index 0000000..e840a86 --- /dev/null +++ b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using System.IO; + +namespace GFramework.Core.Tests.Packaging; + +/// +/// 验证模块级可选 Global Usings 的生成脚本与仓库中的已提交产物保持同步。 +/// 该测试用于防止新增模块、命名空间清单或打包声明发生漂移后静默进入仓库。 +/// +[TestFixture] +public class TransitiveGlobalUsingsGenerationTests +{ + /// + /// 验证生成脚本的检查模式可以在当前仓库状态下通过。 + /// 如果此断言失败,说明清单、生成的 props 文件或 csproj 打包声明至少有一处未同步。 + /// + [Test] + public void GenerateModuleGlobalUsingsScript_CheckMode_Should_Pass() + { + var repositoryRoot = FindRepositoryRoot(); + using var process = StartScriptCheck(repositoryRoot); + + var standardOutput = process.StandardOutput.ReadToEnd(); + var standardError = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + Assert.That( + process.ExitCode, + Is.EqualTo(0), + $"Expected module global usings generation to be up to date.{System.Environment.NewLine}" + + $"stdout:{System.Environment.NewLine}{standardOutput}{System.Environment.NewLine}" + + $"stderr:{System.Environment.NewLine}{standardError}"); + } + + /// + /// 启动生成脚本的检查模式。 + /// 该模式不会修改仓库内容,只验证仓库中的生成产物是否已与当前规则对齐。 + /// + /// 仓库根目录。 + /// 已启动的进程实例。 + private static Process StartScriptCheck(string repositoryRoot) + { + var startInfo = new ProcessStartInfo("python3", "scripts/generate-module-global-usings.py --check") + { + WorkingDirectory = repositoryRoot, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + + return Process.Start(startInfo) + ?? throw new InvalidOperationException("Failed to start the module global usings generation check."); + } + + /// + /// 从测试输出目录向上回溯,定位包含解决方案文件的仓库根目录。 + /// + /// 仓库根目录绝对路径。 + private static string FindRepositoryRoot() + { + var currentDirectory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + + while (currentDirectory != null) + { + var solutionPath = Path.Combine(currentDirectory.FullName, "GFramework.sln"); + if (File.Exists(solutionPath)) + return currentDirectory.FullName; + + currentDirectory = currentDirectory.Parent; + } + + throw new DirectoryNotFoundException("Could not locate the repository root for GFramework."); + } +} \ No newline at end of file diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index bc78025..0ef4bcd 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -14,4 +14,9 @@ + + + + + diff --git a/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props b/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props new file mode 100644 index 0000000..b666aa9 --- /dev/null +++ b/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props @@ -0,0 +1,53 @@ + + + + + + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Architectures"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Command"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Concurrency"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Configuration"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Constants"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine.Extensions"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine.Instructions"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Behaviors"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Command"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Notification"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Query"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Request"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Environment"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Events"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Events.Filters"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Extensions"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Async"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Control"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Functions"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Pipe"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Ioc"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Localization"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Localization.Formatters"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Appenders"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Filters"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Formatters"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Model"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Pause"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Pool"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Property"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Query"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Resource"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Rule"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Services"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Services.Modules"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.State"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.StateManagement"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Systems"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Time"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Utility"/> + <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Utility.Numeric"/> + <_GFramework_Core_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> + + + diff --git a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj index b937843..d9550a3 100644 --- a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj +++ b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj @@ -9,5 +9,9 @@ - + + + + + diff --git a/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props b/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props new file mode 100644 index 0000000..5816a35 --- /dev/null +++ b/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props @@ -0,0 +1,10 @@ + + + + + + <_GFramework_Ecs_Arch_Abstractions_TransitiveUsing Include="GFramework.Ecs.Arch.Abstractions"/> + <_GFramework_Ecs_Arch_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> + + + diff --git a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj index 2b58a0e..68a8916 100644 --- a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj +++ b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj @@ -19,5 +19,9 @@ - + + + + + diff --git a/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props b/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props new file mode 100644 index 0000000..1776e61 --- /dev/null +++ b/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props @@ -0,0 +1,13 @@ + + + + + + <_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch"/> + <_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Components"/> + <_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Extensions"/> + <_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Systems"/> + <_GFramework_Ecs_Arch_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> + + + diff --git a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj index 409352d..62aef63 100644 --- a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj +++ b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj @@ -28,4 +28,9 @@ runtime; build; native; contentfiles; analyzers + + + + + diff --git a/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props b/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props new file mode 100644 index 0000000..f7319ff --- /dev/null +++ b/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props @@ -0,0 +1,19 @@ + + + + + + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Asset"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Data"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Data.Events"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Enums"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Routing"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Scene"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Setting"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Setting.Data"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Storage"/> + <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.UI"/> + <_GFramework_Game_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> + + + diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj index 4c3cce7..37bc8a8 100644 --- a/GFramework.Game/GFramework.Game.csproj +++ b/GFramework.Game/GFramework.Game.csproj @@ -14,4 +14,9 @@ + + + + + diff --git a/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props b/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props new file mode 100644 index 0000000..b7749c7 --- /dev/null +++ b/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props @@ -0,0 +1,21 @@ + + + + + + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Data"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Extensions"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Routing"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Scene"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Scene.Handler"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Serializer"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Setting"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Setting.Events"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.State"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Storage"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.UI"/> + <_GFramework_Game_TransitiveUsing Include="GFramework.Game.UI.Handler"/> + <_GFramework_Game_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> + + + diff --git a/GFramework.Godot/GFramework.Godot.csproj b/GFramework.Godot/GFramework.Godot.csproj index 5d79219..44e037c 100644 --- a/GFramework.Godot/GFramework.Godot.csproj +++ b/GFramework.Godot/GFramework.Godot.csproj @@ -22,4 +22,9 @@ + + + + + diff --git a/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props b/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props new file mode 100644 index 0000000..ac14337 --- /dev/null +++ b/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props @@ -0,0 +1,22 @@ + + + + + + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Architectures"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Coroutine"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Data"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Extensions"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Extensions.Signal"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Logging"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Pause"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Pool"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Scene"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Setting"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Setting.Data"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Storage"/> + <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.UI"/> + <_GFramework_Godot_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> + + + diff --git a/README.md b/README.md index e9bb84a..fc7a8fe 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,32 @@ dotnet add package GeWuYou.GFramework.Godot dotnet add package GeWuYou.GFramework.SourceGenerators ``` +## 可选模块导入 + +发布后的运行时包支持可选的模块级自动导入,但默认关闭,避免在普通项目里无意污染命名空间。 + +在 NuGet 消费项目中显式开启: + +```xml + + true + +``` + +启用后,项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入其推荐命名空间。 + +如果某几个命名空间不想导入,可以局部排除: + +```xml + + + + +``` + +> 该能力面向 NuGet 包消费场景。若你在本地解决方案中直接使用 `ProjectReference`,仍建议保留自己的 `GlobalUsings.cs` 或手写 +`using`。 + ## 仓库结构 ```text diff --git a/docs/zh-CN/getting-started/installation.md b/docs/zh-CN/getting-started/installation.md index 74020b1..c6b8618 100644 --- a/docs/zh-CN/getting-started/installation.md +++ b/docs/zh-CN/getting-started/installation.md @@ -88,19 +88,28 @@ dotnet add package GeWuYou.GFramework.SourceGenerators ### 1. 基础配置 -创建 `GlobalUsings.cs` 文件: +如果你通过 NuGet 包使用 GFramework,并且希望自动导入已安装模块的推荐命名空间,可以在项目文件中显式开启: -```csharp -global using GFramework.Core; -global using GFramework.Core.Architecture; -global using GFramework.Core.Command; -global using GFramework.Core.Events; -global using GFramework.Core.Model; -global using GFramework.Core.Property; -global using GFramework.Core.System; -global using GFramework.Core.Utility; +```xml + + true + ``` +启用后,当前项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入对应命名空间。 + +如果你想排除局部导入,可以继续在项目文件中添加排除项: + +```xml + + + + + +``` + +如果你使用的是本地 `ProjectReference`,或者希望完全手动控制导入范围,仍然可以继续维护自己的 `GlobalUsings.cs` 文件。 + ### 2. Godot 项目配置 如果使用 Godot 集成,需要在项目设置中启用 C# 支持: diff --git a/global-usings.modules.json b/global-usings.modules.json new file mode 100644 index 0000000..6db53da --- /dev/null +++ b/global-usings.modules.json @@ -0,0 +1,161 @@ +{ + "modules": [ + { + "project": "GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj", + "namespaces": [ + "GFramework.Core.Abstractions.Architectures", + "GFramework.Core.Abstractions.Bases", + "GFramework.Core.Abstractions.Command", + "GFramework.Core.Abstractions.Concurrency", + "GFramework.Core.Abstractions.Configuration", + "GFramework.Core.Abstractions.Controller", + "GFramework.Core.Abstractions.Coroutine", + "GFramework.Core.Abstractions.Cqrs", + "GFramework.Core.Abstractions.Cqrs.Command", + "GFramework.Core.Abstractions.Cqrs.Notification", + "GFramework.Core.Abstractions.Cqrs.Query", + "GFramework.Core.Abstractions.Cqrs.Request", + "GFramework.Core.Abstractions.Data", + "GFramework.Core.Abstractions.Enums", + "GFramework.Core.Abstractions.Environment", + "GFramework.Core.Abstractions.Events", + "GFramework.Core.Abstractions.Ioc", + "GFramework.Core.Abstractions.Lifecycle", + "GFramework.Core.Abstractions.Localization", + "GFramework.Core.Abstractions.Logging", + "GFramework.Core.Abstractions.Model", + "GFramework.Core.Abstractions.Pause", + "GFramework.Core.Abstractions.Pool", + "GFramework.Core.Abstractions.Properties", + "GFramework.Core.Abstractions.Property", + "GFramework.Core.Abstractions.Query", + "GFramework.Core.Abstractions.Registries", + "GFramework.Core.Abstractions.Resource", + "GFramework.Core.Abstractions.Rule", + "GFramework.Core.Abstractions.Serializer", + "GFramework.Core.Abstractions.State", + "GFramework.Core.Abstractions.StateManagement", + "GFramework.Core.Abstractions.Storage", + "GFramework.Core.Abstractions.Systems", + "GFramework.Core.Abstractions.Time", + "GFramework.Core.Abstractions.Utility", + "GFramework.Core.Abstractions.Utility.Numeric", + "GFramework.Core.Abstractions.Versioning" + ] + }, + { + "project": "GFramework.Core/GFramework.Core.csproj", + "namespaces": [ + "GFramework.Core.Architectures", + "GFramework.Core.Command", + "GFramework.Core.Concurrency", + "GFramework.Core.Configuration", + "GFramework.Core.Constants", + "GFramework.Core.Coroutine", + "GFramework.Core.Coroutine.Extensions", + "GFramework.Core.Coroutine.Instructions", + "GFramework.Core.Cqrs.Behaviors", + "GFramework.Core.Cqrs.Command", + "GFramework.Core.Cqrs.Notification", + "GFramework.Core.Cqrs.Query", + "GFramework.Core.Cqrs.Request", + "GFramework.Core.Environment", + "GFramework.Core.Events", + "GFramework.Core.Events.Filters", + "GFramework.Core.Extensions", + "GFramework.Core.Functional", + "GFramework.Core.Functional.Async", + "GFramework.Core.Functional.Control", + "GFramework.Core.Functional.Functions", + "GFramework.Core.Functional.Pipe", + "GFramework.Core.Ioc", + "GFramework.Core.Localization", + "GFramework.Core.Localization.Formatters", + "GFramework.Core.Logging", + "GFramework.Core.Logging.Appenders", + "GFramework.Core.Logging.Filters", + "GFramework.Core.Logging.Formatters", + "GFramework.Core.Model", + "GFramework.Core.Pause", + "GFramework.Core.Pool", + "GFramework.Core.Property", + "GFramework.Core.Query", + "GFramework.Core.Resource", + "GFramework.Core.Rule", + "GFramework.Core.Services", + "GFramework.Core.Services.Modules", + "GFramework.Core.State", + "GFramework.Core.StateManagement", + "GFramework.Core.Systems", + "GFramework.Core.Time", + "GFramework.Core.Utility", + "GFramework.Core.Utility.Numeric" + ] + }, + { + "project": "GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj", + "namespaces": [ + "GFramework.Game.Abstractions.Asset", + "GFramework.Game.Abstractions.Data", + "GFramework.Game.Abstractions.Data.Events", + "GFramework.Game.Abstractions.Enums", + "GFramework.Game.Abstractions.Routing", + "GFramework.Game.Abstractions.Scene", + "GFramework.Game.Abstractions.Setting", + "GFramework.Game.Abstractions.Setting.Data", + "GFramework.Game.Abstractions.Storage", + "GFramework.Game.Abstractions.UI" + ] + }, + { + "project": "GFramework.Game/GFramework.Game.csproj", + "namespaces": [ + "GFramework.Game.Data", + "GFramework.Game.Extensions", + "GFramework.Game.Routing", + "GFramework.Game.Scene", + "GFramework.Game.Scene.Handler", + "GFramework.Game.Serializer", + "GFramework.Game.Setting", + "GFramework.Game.Setting.Events", + "GFramework.Game.State", + "GFramework.Game.Storage", + "GFramework.Game.UI", + "GFramework.Game.UI.Handler" + ] + }, + { + "project": "GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj", + "namespaces": [ + "GFramework.Ecs.Arch.Abstractions" + ] + }, + { + "project": "GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj", + "namespaces": [ + "GFramework.Ecs.Arch", + "GFramework.Ecs.Arch.Components", + "GFramework.Ecs.Arch.Extensions", + "GFramework.Ecs.Arch.Systems" + ] + }, + { + "project": "GFramework.Godot/GFramework.Godot.csproj", + "namespaces": [ + "GFramework.Godot.Architectures", + "GFramework.Godot.Coroutine", + "GFramework.Godot.Data", + "GFramework.Godot.Extensions", + "GFramework.Godot.Extensions.Signal", + "GFramework.Godot.Logging", + "GFramework.Godot.Pause", + "GFramework.Godot.Pool", + "GFramework.Godot.Scene", + "GFramework.Godot.Setting", + "GFramework.Godot.Setting.Data", + "GFramework.Godot.Storage", + "GFramework.Godot.UI" + ] + } + ] +} diff --git a/scripts/generate-module-global-usings.py b/scripts/generate-module-global-usings.py new file mode 100644 index 0000000..ba040cc --- /dev/null +++ b/scripts/generate-module-global-usings.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import json +import re +from dataclasses import dataclass +from pathlib import Path + + +ROOT_DIR = Path(__file__).resolve().parent.parent +CONFIG_PATH = ROOT_DIR / "global-usings.modules.json" +AUTO_GENERATED_START = "" +AUTO_GENERATED_END = "" + + +@dataclass(frozen=True) +class ModuleConfig: + project_path: Path + namespaces: tuple[str, ...] + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Generate optional transitive global usings for packable GFramework runtime modules.", + ) + parser.add_argument( + "--check", + action="store_true", + help="Validate that generated files are up to date instead of writing them.", + ) + return parser.parse_args() + + +def load_text(path: Path) -> tuple[str, str]: + raw = path.read_bytes() + if raw.startswith(b"\xef\xbb\xbf"): + return raw.decode("utf-8-sig"), "utf-8-sig" + + return raw.decode("utf-8"), "utf-8" + + +def write_text(path: Path, content: str, encoding: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding=encoding) + + +def load_modules() -> list[ModuleConfig]: + data = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) + modules: list[ModuleConfig] = [] + + for entry in data["modules"]: + project_path = ROOT_DIR / entry["project"] + namespaces = tuple(dict.fromkeys(entry["namespaces"])) + modules.append(ModuleConfig(project_path=project_path, namespaces=namespaces)) + + return modules + + +def discover_runtime_modules() -> list[Path]: + projects: list[Path] = [] + + for project_path in sorted(ROOT_DIR.rglob("*.csproj")): + if "obj" in project_path.parts or "bin" in project_path.parts: + continue + + if project_path.name == "GFramework.csproj": + continue + + project_name = project_path.stem + if not project_name.startswith("GFramework."): + continue + + if ".Tests" in project_name or "SourceGenerators" in project_name: + continue + + csproj_text, _ = load_text(project_path) + if re.search(r"\s*false\s*", csproj_text, re.IGNORECASE): + continue + + projects.append(project_path) + + return projects + + +def resolve_package_id(project_path: Path) -> str: + project_text, _ = load_text(project_path) + match = re.search(r"(.*?)", project_text, re.DOTALL) + if match is None: + return project_path.stem + + package_id = match.group(1).strip() + return package_id.replace("$(AssemblyName)", project_path.stem) + + +def sanitize_identifier(text: str) -> str: + return re.sub(r"[^A-Za-z0-9_]", "_", text) + + +def render_props(module: ModuleConfig) -> str: + item_name = f"_{sanitize_identifier(module.project_path.stem)}_TransitiveUsing" + lines = [ + "", + " ", + " ", + " ", + " ", + ] + + for namespace in module.namespaces: + lines.append(f" <{item_name} Include=\"{namespace}\" />") + + lines.extend( + [ + f" <{item_name} Remove=\"@(GFrameworkExcludedUsing)\" />", + f" ", + " ", + "", + "", + ], + ) + return "\n".join(lines) + + +def render_pack_block(package_id: str) -> str: + props_path = f"buildTransitive\\{package_id}.props" + return "\n".join( + [ + f" {AUTO_GENERATED_START}", + " ", + f" ", + " ", + f" {AUTO_GENERATED_END}", + "", + ], + ) + + +def update_csproj(project_path: Path, package_id: str) -> tuple[str, str]: + project_text, encoding = load_text(project_path) + block = render_pack_block(package_id) + pattern = re.compile( + rf"\s*{re.escape(AUTO_GENERATED_START)}.*?{re.escape(AUTO_GENERATED_END)}\s*", + re.DOTALL, + ) + + if pattern.search(project_text): + updated = pattern.sub(lambda _: f"\n{block}", project_text) + else: + updated = project_text.replace("", f"{block}") + + return updated, encoding + + +def validate_module_coverage(modules: list[ModuleConfig]) -> None: + configured_projects = {module.project_path.resolve() for module in modules} + discovered_projects = {project.resolve() for project in discover_runtime_modules()} + + missing = sorted(project.relative_to(ROOT_DIR) for project in discovered_projects - configured_projects) + extra = sorted(project.relative_to(ROOT_DIR) for project in configured_projects - discovered_projects) + + if missing or extra: + messages: list[str] = [] + if missing: + messages.append("Unconfigured runtime modules:\n - " + "\n - ".join(str(path) for path in missing)) + if extra: + messages.append("Configured modules that are not eligible runtime packages:\n - " + + "\n - ".join(str(path) for path in extra)) + raise SystemExit("\n".join(messages)) + + +def check_or_write(path: Path, expected: str, encoding: str, check_only: bool, changed: list[Path]) -> None: + if path.exists(): + current, _ = load_text(path) + if current == expected: + return + elif check_only: + changed.append(path) + return + + if check_only: + changed.append(path) + return + + write_text(path, expected, encoding) + changed.append(path) + + +def main() -> None: + args = parse_args() + modules = load_modules() + validate_module_coverage(modules) + changed: list[Path] = [] + + for module in modules: + package_id = resolve_package_id(module.project_path) + props_path = module.project_path.parent / "buildTransitive" / f"{package_id}.props" + props_content = render_props(module) + check_or_write(props_path, props_content, "utf-8", args.check, changed) + + updated_project, encoding = update_csproj(module.project_path, package_id) + check_or_write(module.project_path, updated_project, encoding, args.check, changed) + + if args.check: + if changed: + relative_paths = "\n".join(f" - {path.relative_to(ROOT_DIR)}" for path in changed) + raise SystemExit(f"Generated module global usings are out of date:\n{relative_paths}") + + print("Module global usings are up to date.") + return + + if changed: + for path in changed: + print(f"Updated {path.relative_to(ROOT_DIR)}") + return + + print("No module global usings changes were needed.") + + +if __name__ == "__main__": + main() From 2d1d1a43b640da30cbb7835df9a895e5606a1bfd Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:24:52 +0800 Subject: [PATCH 2/3] =?UTF-8?q?chore(build):=20=E7=A7=BB=E9=99=A4=E9=9D=99?= =?UTF-8?q?=E6=80=81=E5=85=A8=E5=B1=80using=E9=85=8D=E7=BD=AE=E5=B9=B6?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=87=AA=E5=8A=A8=E5=8C=96=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除所有手动维护的 buildTransitive props 文件 - 从项目文件中移除静态的 global usings 配置 - 删除废弃的 global-usings.modules.json 清单文件 - 移除旧的 TransitiveGlobalUsingsGenerationTests 测试 - 添加新的 TransitiveGlobalUsingsPackagingTests 验证自动化生成 - 在 Directory.Build.targets 中集成 MSBuild 自动化生成任务 - 实现基于源码扫描的动态命名空间发现机制 --- Directory.Build.targets | 171 ++++++++++++++ .../GFramework.Core.Abstractions.csproj | 6 +- ...GeWuYou.GFramework.Core.Abstractions.props | 47 ---- .../TransitiveGlobalUsingsGenerationTests.cs | 74 ------ .../TransitiveGlobalUsingsPackagingTests.cs | 56 +++++ GFramework.Core/GFramework.Core.csproj | 6 +- .../GeWuYou.GFramework.Core.props | 53 ----- .../GFramework.Ecs.Arch.Abstractions.csproj | 6 +- ...You.GFramework.Ecs.Arch.Abstractions.props | 10 - .../GFramework.Ecs.Arch.csproj | 6 +- .../GeWuYou.GFramework.Ecs.Arch.props | 13 - .../GFramework.Game.Abstractions.csproj | 6 +- ...GeWuYou.GFramework.Game.Abstractions.props | 19 -- GFramework.Game/GFramework.Game.csproj | 6 +- .../GeWuYou.GFramework.Game.props | 21 -- GFramework.Godot/GFramework.Godot.csproj | 6 +- .../GeWuYou.GFramework.Godot.props | 22 -- global-usings.modules.json | 161 ------------- scripts/generate-module-global-usings.py | 222 ------------------ 19 files changed, 234 insertions(+), 677 deletions(-) create mode 100644 Directory.Build.targets delete mode 100644 GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props delete mode 100644 GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs create mode 100644 GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs delete mode 100644 GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props delete mode 100644 GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props delete mode 100644 GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props delete mode 100644 GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props delete mode 100644 GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props delete mode 100644 GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props delete mode 100644 global-usings.modules.json delete mode 100644 scripts/generate-module-global-usings.py diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..1b1ad0a --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,171 @@ + + + + + + + + + + + + + (global::System.StringComparer.Ordinal); + var exactExclusions = new global::System.Collections.Generic.HashSet(global::System.StringComparer.Ordinal); + var prefixExclusions = new global::System.Collections.Generic.List(); + var namespacePattern = new global::System.Text.RegularExpressions.Regex( + @"^\s*namespace\s+([A-Za-z_][A-Za-z0-9_.]*)\s*(?:;|\{)", + global::System.Text.RegularExpressions.RegexOptions.Compiled); + + if (ExcludedNamespaces != null) + { + foreach (var excludedNamespace in ExcludedNamespaces) + { + if (!string.IsNullOrWhiteSpace(excludedNamespace.ItemSpec)) + { + exactExclusions.Add(excludedNamespace.ItemSpec.Trim()); + } + } + } + + if (ExcludedNamespacePrefixes != null) + { + foreach (var excludedPrefix in ExcludedNamespacePrefixes) + { + if (!string.IsNullOrWhiteSpace(excludedPrefix.ItemSpec)) + { + prefixExclusions.Add(excludedPrefix.ItemSpec.Trim()); + } + } + } + + foreach (var sourceFile in SourceFiles) + { + var path = sourceFile.ItemSpec; + if (!global::System.IO.File.Exists(path)) + { + continue; + } + + foreach (var line in global::System.IO.File.ReadLines(path)) + { + var match = namespacePattern.Match(line); + if (!match.Success) + { + continue; + } + + var namespaceName = match.Groups[1].Value; + if (!namespaceName.StartsWith("GFramework.", global::System.StringComparison.Ordinal)) + { + continue; + } + + if (exactExclusions.Contains(namespaceName)) + { + continue; + } + + var excludedByPrefix = false; + foreach (var prefix in prefixExclusions) + { + if (namespaceName.StartsWith(prefix, global::System.StringComparison.Ordinal)) + { + excludedByPrefix = true; + break; + } + } + + if (!excludedByPrefix) + { + discoveredNamespaces.Add(namespaceName); + } + } + } + + static string Escape(string value) + { + return global::System.Security.SecurityElement.Escape(value) ?? value; + } + + var directory = global::System.IO.Path.GetDirectoryName(OutputFile); + if (!string.IsNullOrEmpty(directory)) + { + global::System.IO.Directory.CreateDirectory(directory); + } + + var builder = new global::System.Text.StringBuilder(); + var msbuildPropertyOpen = new string(new[] { '$', '(' }); + var msbuildItemOpen = new string(new[] { '@', '(' }); + builder.AppendLine(""); + builder.AppendLine(" "); + builder.AppendLine(" "); + builder.AppendLine(" "); + builder.Append(" "); + + foreach (var namespaceName in discoveredNamespaces) + { + builder.Append(" <"); + builder.Append(NamespaceItemName); + builder.Append(" Include=\""); + builder.Append(Escape(namespaceName)); + builder.AppendLine("\" />"); + } + + builder.Append(" <"); + builder.Append(NamespaceItemName); + builder.Append(" Remove=\""); + builder.Append(msbuildItemOpen); + builder.AppendLine("GFrameworkExcludedUsing)\" />"); + builder.Append(" "); + builder.AppendLine(" "); + builder.AppendLine(""); + + global::System.IO.File.WriteAllText(OutputFile, builder.ToString(), new global::System.Text.UTF8Encoding(false)); + Log.LogMessage(global::Microsoft.Build.Framework.MessageImportance.Low, + $"Generated {discoveredNamespaces.Count} transitive global usings for {OutputFile}."); + ]]> + + + + + <_GFrameworkTransitiveGlobalUsingsEnabled Condition="'$(EnableGFrameworkPackageTransitiveGlobalUsings)' == 'true' and '$(IsPackable)' != 'false'">true + <_GFrameworkTransitiveGlobalUsingsPrimaryTargetFramework Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(TargetFrameworks)' != ''">$([System.String]::Copy('$(TargetFrameworks)').Split(';')[0]) + <_GFrameworkTransitiveGlobalUsingsGenerationBuild Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and ('$(TargetFrameworks)' == '' or '$(TargetFramework)' == '$(_GFrameworkTransitiveGlobalUsingsPrimaryTargetFramework)')">true + <_GFrameworkTransitiveGlobalUsingsPackageId Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(PackageId)' != ''">$(PackageId) + <_GFrameworkTransitiveGlobalUsingsPackageId Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(_GFrameworkTransitiveGlobalUsingsPackageId)' == ''">$(AssemblyName) + <_GFrameworkTransitiveGlobalUsingsOutputFile Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true'">$(BaseIntermediateOutputPath)gframework/$(_GFrameworkTransitiveGlobalUsingsPackageId).props + <_GFrameworkTransitiveGlobalUsingsItemName Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true'">_$([System.Text.RegularExpressions.Regex]::Replace('$(MSBuildProjectName)', '[^A-Za-z0-9_]', '_'))_TransitiveUsing + + + + + + + + + + + diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj index d5f1779..83dea9d 100644 --- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj +++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj @@ -10,6 +10,7 @@ T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute enable true + true @@ -27,9 +28,4 @@ - - - - - diff --git a/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props b/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props deleted file mode 100644 index 67cca44..0000000 --- a/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Architectures"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Bases"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Command"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Concurrency"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Configuration"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Controller"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Coroutine"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Command"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Notification"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Query"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Request"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Data"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Enums"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Environment"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Events"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Ioc"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Lifecycle"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Localization"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Logging"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Model"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Pause"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Pool"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Properties"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Property"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Query"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Registries"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Resource"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Rule"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Serializer"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.State"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.StateManagement"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Storage"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Systems"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Time"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Utility"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Utility.Numeric"/> - <_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Versioning"/> - <_GFramework_Core_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> - - - diff --git a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs deleted file mode 100644 index e840a86..0000000 --- a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Diagnostics; -using System.IO; - -namespace GFramework.Core.Tests.Packaging; - -/// -/// 验证模块级可选 Global Usings 的生成脚本与仓库中的已提交产物保持同步。 -/// 该测试用于防止新增模块、命名空间清单或打包声明发生漂移后静默进入仓库。 -/// -[TestFixture] -public class TransitiveGlobalUsingsGenerationTests -{ - /// - /// 验证生成脚本的检查模式可以在当前仓库状态下通过。 - /// 如果此断言失败,说明清单、生成的 props 文件或 csproj 打包声明至少有一处未同步。 - /// - [Test] - public void GenerateModuleGlobalUsingsScript_CheckMode_Should_Pass() - { - var repositoryRoot = FindRepositoryRoot(); - using var process = StartScriptCheck(repositoryRoot); - - var standardOutput = process.StandardOutput.ReadToEnd(); - var standardError = process.StandardError.ReadToEnd(); - process.WaitForExit(); - - Assert.That( - process.ExitCode, - Is.EqualTo(0), - $"Expected module global usings generation to be up to date.{System.Environment.NewLine}" + - $"stdout:{System.Environment.NewLine}{standardOutput}{System.Environment.NewLine}" + - $"stderr:{System.Environment.NewLine}{standardError}"); - } - - /// - /// 启动生成脚本的检查模式。 - /// 该模式不会修改仓库内容,只验证仓库中的生成产物是否已与当前规则对齐。 - /// - /// 仓库根目录。 - /// 已启动的进程实例。 - private static Process StartScriptCheck(string repositoryRoot) - { - var startInfo = new ProcessStartInfo("python3", "scripts/generate-module-global-usings.py --check") - { - WorkingDirectory = repositoryRoot, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false - }; - - return Process.Start(startInfo) - ?? throw new InvalidOperationException("Failed to start the module global usings generation check."); - } - - /// - /// 从测试输出目录向上回溯,定位包含解决方案文件的仓库根目录。 - /// - /// 仓库根目录绝对路径。 - private static string FindRepositoryRoot() - { - var currentDirectory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); - - while (currentDirectory != null) - { - var solutionPath = Path.Combine(currentDirectory.FullName, "GFramework.sln"); - if (File.Exists(solutionPath)) - return currentDirectory.FullName; - - currentDirectory = currentDirectory.Parent; - } - - throw new DirectoryNotFoundException("Could not locate the repository root for GFramework."); - } -} \ No newline at end of file diff --git a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs new file mode 100644 index 0000000..2ca6a9c --- /dev/null +++ b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs @@ -0,0 +1,56 @@ +using System.IO; + +namespace GFramework.Core.Tests.Packaging; + +/// +/// 验证运行时模块在构建期间会自动生成 transitive global usings 资产。 +/// 该测试覆盖命名空间自动发现、框架侧过滤和消费者侧排除钩子的最终构建产物。 +/// +[TestFixture] +public class TransitiveGlobalUsingsPackagingTests +{ + /// + /// 验证 GFramework.Core 在构建后会生成 transitive global usings props, + /// 且 props 内容来自源码自动发现,并保留消费者侧排除机制。 + /// + [Test] + public void CoreBuild_Should_Generate_AutoDiscovered_TransitiveGlobalUsingsProps() + { + var repositoryRoot = FindRepositoryRoot(); + var propsPath = Path.Combine( + repositoryRoot, + "GFramework.Core", + "obj", + "gframework", + "GeWuYou.GFramework.Core.props"); + + Assert.That(File.Exists(propsPath), Is.True, $"Expected generated props to exist: {propsPath}"); + var propsContent = File.ReadAllText(propsPath); + + Assert.That(propsContent, Does.Contain("GFramework.Core.Extensions")); + Assert.That(propsContent, Does.Contain("GFramework.Core.Architectures")); + Assert.That(propsContent, Does.Contain("GFramework.Core.Coroutine.Extensions")); + Assert.That(propsContent, Does.Contain("Remove=\"@(GFrameworkExcludedUsing)\"")); + Assert.That(propsContent, Does.Not.Contain("System.Runtime.CompilerServices")); + } + + /// + /// 从测试输出目录向上回溯,定位包含解决方案文件的仓库根目录。 + /// + /// 仓库根目录绝对路径。 + private static string FindRepositoryRoot() + { + var currentDirectory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + + while (currentDirectory != null) + { + var solutionPath = Path.Combine(currentDirectory.FullName, "GFramework.sln"); + if (File.Exists(solutionPath)) + return currentDirectory.FullName; + + currentDirectory = currentDirectory.Parent; + } + + throw new DirectoryNotFoundException("Could not locate the repository root for GFramework."); + } +} \ No newline at end of file diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index 0ef4bcd..c450b44 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -6,6 +6,7 @@ disable enable true + true @@ -14,9 +15,4 @@ - - - - - diff --git a/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props b/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props deleted file mode 100644 index b666aa9..0000000 --- a/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Architectures"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Command"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Concurrency"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Configuration"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Constants"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine.Extensions"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine.Instructions"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Behaviors"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Command"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Notification"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Query"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Request"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Environment"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Events"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Events.Filters"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Extensions"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Async"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Control"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Functions"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Pipe"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Ioc"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Localization"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Localization.Formatters"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Appenders"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Filters"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Formatters"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Model"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Pause"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Pool"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Property"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Query"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Resource"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Rule"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Services"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Services.Modules"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.State"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.StateManagement"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Systems"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Time"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Utility"/> - <_GFramework_Core_TransitiveUsing Include="GFramework.Core.Utility.Numeric"/> - <_GFramework_Core_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> - - - diff --git a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj index d9550a3..94337e8 100644 --- a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj +++ b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj @@ -4,14 +4,10 @@ GeWuYou.$(AssemblyName) true enable + true - - - - - diff --git a/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props b/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props deleted file mode 100644 index 5816a35..0000000 --- a/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - <_GFramework_Ecs_Arch_Abstractions_TransitiveUsing Include="GFramework.Ecs.Arch.Abstractions"/> - <_GFramework_Ecs_Arch_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> - - - diff --git a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj index 68a8916..a79bec7 100644 --- a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj +++ b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj @@ -7,6 +7,7 @@ enable true true + true @@ -19,9 +20,4 @@ - - - - - diff --git a/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props b/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props deleted file mode 100644 index 1776e61..0000000 --- a/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - <_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch"/> - <_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Components"/> - <_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Extensions"/> - <_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Systems"/> - <_GFramework_Ecs_Arch_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> - - - diff --git a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj index 62aef63..b4a8890 100644 --- a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj +++ b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj @@ -10,6 +10,7 @@ T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute enable true + true @@ -28,9 +29,4 @@ runtime; build; native; contentfiles; analyzers - - - - - diff --git a/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props b/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props deleted file mode 100644 index f7319ff..0000000 --- a/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Asset"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Data"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Data.Events"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Enums"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Routing"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Scene"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Setting"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Setting.Data"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Storage"/> - <_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.UI"/> - <_GFramework_Game_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> - - - diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj index 37bc8a8..5bb94d9 100644 --- a/GFramework.Game/GFramework.Game.csproj +++ b/GFramework.Game/GFramework.Game.csproj @@ -6,6 +6,7 @@ disable enable true + true @@ -14,9 +15,4 @@ - - - - - diff --git a/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props b/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props deleted file mode 100644 index b7749c7..0000000 --- a/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Data"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Extensions"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Routing"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Scene"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Scene.Handler"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Serializer"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Setting"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Setting.Events"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.State"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.Storage"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.UI"/> - <_GFramework_Game_TransitiveUsing Include="GFramework.Game.UI.Handler"/> - <_GFramework_Game_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> - - - diff --git a/GFramework.Godot/GFramework.Godot.csproj b/GFramework.Godot/GFramework.Godot.csproj index 44e037c..29c065c 100644 --- a/GFramework.Godot/GFramework.Godot.csproj +++ b/GFramework.Godot/GFramework.Godot.csproj @@ -6,6 +6,7 @@ enable net8.0;net9.0;net10.0 true + true $(MSBuildProjectDirectory) @@ -22,9 +23,4 @@ - - - - - diff --git a/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props b/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props deleted file mode 100644 index ac14337..0000000 --- a/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Architectures"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Coroutine"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Data"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Extensions"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Extensions.Signal"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Logging"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Pause"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Pool"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Scene"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Setting"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Setting.Data"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Storage"/> - <_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.UI"/> - <_GFramework_Godot_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> - - - diff --git a/global-usings.modules.json b/global-usings.modules.json deleted file mode 100644 index 6db53da..0000000 --- a/global-usings.modules.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "modules": [ - { - "project": "GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj", - "namespaces": [ - "GFramework.Core.Abstractions.Architectures", - "GFramework.Core.Abstractions.Bases", - "GFramework.Core.Abstractions.Command", - "GFramework.Core.Abstractions.Concurrency", - "GFramework.Core.Abstractions.Configuration", - "GFramework.Core.Abstractions.Controller", - "GFramework.Core.Abstractions.Coroutine", - "GFramework.Core.Abstractions.Cqrs", - "GFramework.Core.Abstractions.Cqrs.Command", - "GFramework.Core.Abstractions.Cqrs.Notification", - "GFramework.Core.Abstractions.Cqrs.Query", - "GFramework.Core.Abstractions.Cqrs.Request", - "GFramework.Core.Abstractions.Data", - "GFramework.Core.Abstractions.Enums", - "GFramework.Core.Abstractions.Environment", - "GFramework.Core.Abstractions.Events", - "GFramework.Core.Abstractions.Ioc", - "GFramework.Core.Abstractions.Lifecycle", - "GFramework.Core.Abstractions.Localization", - "GFramework.Core.Abstractions.Logging", - "GFramework.Core.Abstractions.Model", - "GFramework.Core.Abstractions.Pause", - "GFramework.Core.Abstractions.Pool", - "GFramework.Core.Abstractions.Properties", - "GFramework.Core.Abstractions.Property", - "GFramework.Core.Abstractions.Query", - "GFramework.Core.Abstractions.Registries", - "GFramework.Core.Abstractions.Resource", - "GFramework.Core.Abstractions.Rule", - "GFramework.Core.Abstractions.Serializer", - "GFramework.Core.Abstractions.State", - "GFramework.Core.Abstractions.StateManagement", - "GFramework.Core.Abstractions.Storage", - "GFramework.Core.Abstractions.Systems", - "GFramework.Core.Abstractions.Time", - "GFramework.Core.Abstractions.Utility", - "GFramework.Core.Abstractions.Utility.Numeric", - "GFramework.Core.Abstractions.Versioning" - ] - }, - { - "project": "GFramework.Core/GFramework.Core.csproj", - "namespaces": [ - "GFramework.Core.Architectures", - "GFramework.Core.Command", - "GFramework.Core.Concurrency", - "GFramework.Core.Configuration", - "GFramework.Core.Constants", - "GFramework.Core.Coroutine", - "GFramework.Core.Coroutine.Extensions", - "GFramework.Core.Coroutine.Instructions", - "GFramework.Core.Cqrs.Behaviors", - "GFramework.Core.Cqrs.Command", - "GFramework.Core.Cqrs.Notification", - "GFramework.Core.Cqrs.Query", - "GFramework.Core.Cqrs.Request", - "GFramework.Core.Environment", - "GFramework.Core.Events", - "GFramework.Core.Events.Filters", - "GFramework.Core.Extensions", - "GFramework.Core.Functional", - "GFramework.Core.Functional.Async", - "GFramework.Core.Functional.Control", - "GFramework.Core.Functional.Functions", - "GFramework.Core.Functional.Pipe", - "GFramework.Core.Ioc", - "GFramework.Core.Localization", - "GFramework.Core.Localization.Formatters", - "GFramework.Core.Logging", - "GFramework.Core.Logging.Appenders", - "GFramework.Core.Logging.Filters", - "GFramework.Core.Logging.Formatters", - "GFramework.Core.Model", - "GFramework.Core.Pause", - "GFramework.Core.Pool", - "GFramework.Core.Property", - "GFramework.Core.Query", - "GFramework.Core.Resource", - "GFramework.Core.Rule", - "GFramework.Core.Services", - "GFramework.Core.Services.Modules", - "GFramework.Core.State", - "GFramework.Core.StateManagement", - "GFramework.Core.Systems", - "GFramework.Core.Time", - "GFramework.Core.Utility", - "GFramework.Core.Utility.Numeric" - ] - }, - { - "project": "GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj", - "namespaces": [ - "GFramework.Game.Abstractions.Asset", - "GFramework.Game.Abstractions.Data", - "GFramework.Game.Abstractions.Data.Events", - "GFramework.Game.Abstractions.Enums", - "GFramework.Game.Abstractions.Routing", - "GFramework.Game.Abstractions.Scene", - "GFramework.Game.Abstractions.Setting", - "GFramework.Game.Abstractions.Setting.Data", - "GFramework.Game.Abstractions.Storage", - "GFramework.Game.Abstractions.UI" - ] - }, - { - "project": "GFramework.Game/GFramework.Game.csproj", - "namespaces": [ - "GFramework.Game.Data", - "GFramework.Game.Extensions", - "GFramework.Game.Routing", - "GFramework.Game.Scene", - "GFramework.Game.Scene.Handler", - "GFramework.Game.Serializer", - "GFramework.Game.Setting", - "GFramework.Game.Setting.Events", - "GFramework.Game.State", - "GFramework.Game.Storage", - "GFramework.Game.UI", - "GFramework.Game.UI.Handler" - ] - }, - { - "project": "GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj", - "namespaces": [ - "GFramework.Ecs.Arch.Abstractions" - ] - }, - { - "project": "GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj", - "namespaces": [ - "GFramework.Ecs.Arch", - "GFramework.Ecs.Arch.Components", - "GFramework.Ecs.Arch.Extensions", - "GFramework.Ecs.Arch.Systems" - ] - }, - { - "project": "GFramework.Godot/GFramework.Godot.csproj", - "namespaces": [ - "GFramework.Godot.Architectures", - "GFramework.Godot.Coroutine", - "GFramework.Godot.Data", - "GFramework.Godot.Extensions", - "GFramework.Godot.Extensions.Signal", - "GFramework.Godot.Logging", - "GFramework.Godot.Pause", - "GFramework.Godot.Pool", - "GFramework.Godot.Scene", - "GFramework.Godot.Setting", - "GFramework.Godot.Setting.Data", - "GFramework.Godot.Storage", - "GFramework.Godot.UI" - ] - } - ] -} diff --git a/scripts/generate-module-global-usings.py b/scripts/generate-module-global-usings.py deleted file mode 100644 index ba040cc..0000000 --- a/scripts/generate-module-global-usings.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import argparse -import json -import re -from dataclasses import dataclass -from pathlib import Path - - -ROOT_DIR = Path(__file__).resolve().parent.parent -CONFIG_PATH = ROOT_DIR / "global-usings.modules.json" -AUTO_GENERATED_START = "" -AUTO_GENERATED_END = "" - - -@dataclass(frozen=True) -class ModuleConfig: - project_path: Path - namespaces: tuple[str, ...] - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Generate optional transitive global usings for packable GFramework runtime modules.", - ) - parser.add_argument( - "--check", - action="store_true", - help="Validate that generated files are up to date instead of writing them.", - ) - return parser.parse_args() - - -def load_text(path: Path) -> tuple[str, str]: - raw = path.read_bytes() - if raw.startswith(b"\xef\xbb\xbf"): - return raw.decode("utf-8-sig"), "utf-8-sig" - - return raw.decode("utf-8"), "utf-8" - - -def write_text(path: Path, content: str, encoding: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding=encoding) - - -def load_modules() -> list[ModuleConfig]: - data = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) - modules: list[ModuleConfig] = [] - - for entry in data["modules"]: - project_path = ROOT_DIR / entry["project"] - namespaces = tuple(dict.fromkeys(entry["namespaces"])) - modules.append(ModuleConfig(project_path=project_path, namespaces=namespaces)) - - return modules - - -def discover_runtime_modules() -> list[Path]: - projects: list[Path] = [] - - for project_path in sorted(ROOT_DIR.rglob("*.csproj")): - if "obj" in project_path.parts or "bin" in project_path.parts: - continue - - if project_path.name == "GFramework.csproj": - continue - - project_name = project_path.stem - if not project_name.startswith("GFramework."): - continue - - if ".Tests" in project_name or "SourceGenerators" in project_name: - continue - - csproj_text, _ = load_text(project_path) - if re.search(r"\s*false\s*", csproj_text, re.IGNORECASE): - continue - - projects.append(project_path) - - return projects - - -def resolve_package_id(project_path: Path) -> str: - project_text, _ = load_text(project_path) - match = re.search(r"(.*?)", project_text, re.DOTALL) - if match is None: - return project_path.stem - - package_id = match.group(1).strip() - return package_id.replace("$(AssemblyName)", project_path.stem) - - -def sanitize_identifier(text: str) -> str: - return re.sub(r"[^A-Za-z0-9_]", "_", text) - - -def render_props(module: ModuleConfig) -> str: - item_name = f"_{sanitize_identifier(module.project_path.stem)}_TransitiveUsing" - lines = [ - "", - " ", - " ", - " ", - " ", - ] - - for namespace in module.namespaces: - lines.append(f" <{item_name} Include=\"{namespace}\" />") - - lines.extend( - [ - f" <{item_name} Remove=\"@(GFrameworkExcludedUsing)\" />", - f" ", - " ", - "", - "", - ], - ) - return "\n".join(lines) - - -def render_pack_block(package_id: str) -> str: - props_path = f"buildTransitive\\{package_id}.props" - return "\n".join( - [ - f" {AUTO_GENERATED_START}", - " ", - f" ", - " ", - f" {AUTO_GENERATED_END}", - "", - ], - ) - - -def update_csproj(project_path: Path, package_id: str) -> tuple[str, str]: - project_text, encoding = load_text(project_path) - block = render_pack_block(package_id) - pattern = re.compile( - rf"\s*{re.escape(AUTO_GENERATED_START)}.*?{re.escape(AUTO_GENERATED_END)}\s*", - re.DOTALL, - ) - - if pattern.search(project_text): - updated = pattern.sub(lambda _: f"\n{block}", project_text) - else: - updated = project_text.replace("", f"{block}") - - return updated, encoding - - -def validate_module_coverage(modules: list[ModuleConfig]) -> None: - configured_projects = {module.project_path.resolve() for module in modules} - discovered_projects = {project.resolve() for project in discover_runtime_modules()} - - missing = sorted(project.relative_to(ROOT_DIR) for project in discovered_projects - configured_projects) - extra = sorted(project.relative_to(ROOT_DIR) for project in configured_projects - discovered_projects) - - if missing or extra: - messages: list[str] = [] - if missing: - messages.append("Unconfigured runtime modules:\n - " + "\n - ".join(str(path) for path in missing)) - if extra: - messages.append("Configured modules that are not eligible runtime packages:\n - " + - "\n - ".join(str(path) for path in extra)) - raise SystemExit("\n".join(messages)) - - -def check_or_write(path: Path, expected: str, encoding: str, check_only: bool, changed: list[Path]) -> None: - if path.exists(): - current, _ = load_text(path) - if current == expected: - return - elif check_only: - changed.append(path) - return - - if check_only: - changed.append(path) - return - - write_text(path, expected, encoding) - changed.append(path) - - -def main() -> None: - args = parse_args() - modules = load_modules() - validate_module_coverage(modules) - changed: list[Path] = [] - - for module in modules: - package_id = resolve_package_id(module.project_path) - props_path = module.project_path.parent / "buildTransitive" / f"{package_id}.props" - props_content = render_props(module) - check_or_write(props_path, props_content, "utf-8", args.check, changed) - - updated_project, encoding = update_csproj(module.project_path, package_id) - check_or_write(module.project_path, updated_project, encoding, args.check, changed) - - if args.check: - if changed: - relative_paths = "\n".join(f" - {path.relative_to(ROOT_DIR)}" for path in changed) - raise SystemExit(f"Generated module global usings are out of date:\n{relative_paths}") - - print("Module global usings are up to date.") - return - - if changed: - for path in changed: - print(f"Updated {path.relative_to(ROOT_DIR)}") - return - - print("No module global usings changes were needed.") - - -if __name__ == "__main__": - main() From 20f1c987eba5b30298875f505a129b518faa9ee2 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:29:45 +0800 Subject: [PATCH 3/3] =?UTF-8?q?refactor(tests):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E5=BC=95=E7=94=A8=E6=B5=8B=E8=AF=95=E7=9A=84?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=8F=91=E7=8E=B0=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加基于真实类型的命名空间常量定义,避免硬编码字符串 - 引入 Architecture、Extensions 和 CoroutineExtensions 命名空间自动发现 - 将 FindRepositoryRoot 方法重构为 ResolveRepositoryRoot 并使用 CallerFilePath - 更新断言逻辑以使用动态命名空间变量而非字面量字符串 - 移除对解决方案文件扫描的依赖,改用相对路径计算仓库根目录 - 添加方法参数验证和异常处理增强代码健壮性 --- .../TransitiveGlobalUsingsPackagingTests.cs | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs index 2ca6a9c..78bda4b 100644 --- a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs +++ b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs @@ -1,4 +1,7 @@ using System.IO; +using System.Runtime.CompilerServices; +using GFramework.Core.Architectures; +using GFramework.Core.Coroutine.Extensions; namespace GFramework.Core.Tests.Packaging; @@ -9,6 +12,27 @@ namespace GFramework.Core.Tests.Packaging; [TestFixture] public class TransitiveGlobalUsingsPackagingTests { + /// + /// 使用真实类型派生架构命名空间,避免测试断言和命名空间重构脱节。 + /// + private static readonly string ArchitectureNamespace = typeof(Architecture).Namespace + ?? throw new InvalidOperationException( + "Architecture namespace should not be null."); + + /// + /// 使用真实类型派生扩展命名空间,避免对字面量命名空间字符串的重复维护。 + /// + private static readonly string ExtensionsNamespace = typeof(ContextAwareEnvironmentExtensions).Namespace + ?? throw new InvalidOperationException( + "Extensions namespace should not be null."); + + /// + /// 使用真实类型派生协程扩展命名空间,确保断言和源码自动发现保持一致。 + /// + private static readonly string CoroutineExtensionsNamespace = typeof(CoroutineExtensions).Namespace + ?? throw new InvalidOperationException( + "Coroutine extensions namespace should not be null."); + /// /// 验证 GFramework.Core 在构建后会生成 transitive global usings props, /// 且 props 内容来自源码自动发现,并保留消费者侧排除机制。 @@ -16,7 +40,7 @@ public class TransitiveGlobalUsingsPackagingTests [Test] public void CoreBuild_Should_Generate_AutoDiscovered_TransitiveGlobalUsingsProps() { - var repositoryRoot = FindRepositoryRoot(); + var repositoryRoot = ResolveRepositoryRoot(); var propsPath = Path.Combine( repositoryRoot, "GFramework.Core", @@ -27,30 +51,30 @@ public class TransitiveGlobalUsingsPackagingTests Assert.That(File.Exists(propsPath), Is.True, $"Expected generated props to exist: {propsPath}"); var propsContent = File.ReadAllText(propsPath); - Assert.That(propsContent, Does.Contain("GFramework.Core.Extensions")); - Assert.That(propsContent, Does.Contain("GFramework.Core.Architectures")); - Assert.That(propsContent, Does.Contain("GFramework.Core.Coroutine.Extensions")); + Assert.That(propsContent, Does.Contain(ExtensionsNamespace)); + Assert.That(propsContent, Does.Contain(ArchitectureNamespace)); + Assert.That(propsContent, Does.Contain(CoroutineExtensionsNamespace)); Assert.That(propsContent, Does.Contain("Remove=\"@(GFrameworkExcludedUsing)\"")); Assert.That(propsContent, Does.Not.Contain("System.Runtime.CompilerServices")); } /// - /// 从测试输出目录向上回溯,定位包含解决方案文件的仓库根目录。 + /// 基于当前测试源文件的已知位置解析仓库根目录。 + /// 这里不扫描解决方案文件,避免测试对仓库布局演进产生额外脆弱性。 /// + /// 由编译器注入的当前测试源文件绝对路径。 /// 仓库根目录绝对路径。 - private static string FindRepositoryRoot() + private static string ResolveRepositoryRoot([CallerFilePath] string sourceFilePath = "") { - var currentDirectory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); - - while (currentDirectory != null) + if (string.IsNullOrWhiteSpace(sourceFilePath)) { - var solutionPath = Path.Combine(currentDirectory.FullName, "GFramework.sln"); - if (File.Exists(solutionPath)) - return currentDirectory.FullName; - - currentDirectory = currentDirectory.Parent; + throw new InvalidOperationException("Caller file path is required to resolve the repository root."); } - throw new DirectoryNotFoundException("Could not locate the repository root for GFramework."); + var sourceDirectory = Path.GetDirectoryName(sourceFilePath) + ?? throw new DirectoryNotFoundException( + $"Could not determine the directory for source file path: {sourceFilePath}"); + + return Path.GetFullPath(Path.Combine(sourceDirectory, "..", "..")); } } \ No newline at end of file