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 f0cec86..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 diff --git a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs new file mode 100644 index 0000000..78bda4b --- /dev/null +++ b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs @@ -0,0 +1,80 @@ +using System.IO; +using System.Runtime.CompilerServices; +using GFramework.Core.Architectures; +using GFramework.Core.Coroutine.Extensions; + +namespace GFramework.Core.Tests.Packaging; + +/// +/// 验证运行时模块在构建期间会自动生成 transitive global usings 资产。 +/// 该测试覆盖命名空间自动发现、框架侧过滤和消费者侧排除钩子的最终构建产物。 +/// +[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 内容来自源码自动发现,并保留消费者侧排除机制。 + /// + [Test] + public void CoreBuild_Should_Generate_AutoDiscovered_TransitiveGlobalUsingsProps() + { + var repositoryRoot = ResolveRepositoryRoot(); + 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(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 ResolveRepositoryRoot([CallerFilePath] string sourceFilePath = "") + { + if (string.IsNullOrWhiteSpace(sourceFilePath)) + { + throw new InvalidOperationException("Caller file path is required to resolve the repository root."); + } + + 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 diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index bc78025..c450b44 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -6,6 +6,7 @@ disable enable true + true diff --git a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj index b937843..94337e8 100644 --- a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj +++ b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj @@ -4,10 +4,10 @@ GeWuYou.$(AssemblyName) true enable + true - diff --git a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj index 2b58a0e..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,5 +20,4 @@ - diff --git a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj index 409352d..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 diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj index 4c3cce7..5bb94d9 100644 --- a/GFramework.Game/GFramework.Game.csproj +++ b/GFramework.Game/GFramework.Game.csproj @@ -6,6 +6,7 @@ disable enable true + true diff --git a/GFramework.Godot/GFramework.Godot.csproj b/GFramework.Godot/GFramework.Godot.csproj index 5d79219..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) 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# 支持: