Merge pull request #135 from GeWuYou/feat/build-modular-global-usings

Feat/build modular global usings
This commit is contained in:
gewuyou 2026-03-24 23:27:17 +08:00 committed by GitHub
commit b3d52a0865
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 303 additions and 12 deletions

171
Directory.Build.targets Normal file
View File

@ -0,0 +1,171 @@
<Project>
<!--
为 GFramework 运行时包生成可选的模块级 transitive global usings。
该逻辑只在明确启用的可打包项目中生效,并在构建/打包期间自动扫描源码命名空间。
-->
<UsingTask TaskName="GenerateGFrameworkTransitiveGlobalUsingsProps"
TaskFactory="RoslynCodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<SourceFiles ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true"/>
<ExcludedNamespaces ParameterType="Microsoft.Build.Framework.ITaskItem[]"/>
<ExcludedNamespacePrefixes ParameterType="Microsoft.Build.Framework.ITaskItem[]"/>
<OutputFile ParameterType="System.String" Required="true"/>
<NamespaceItemName ParameterType="System.String" Required="true"/>
</ParameterGroup>
<Task>
<Code Type="Fragment"
Language="cs"><![CDATA[
var discoveredNamespaces = new global::System.Collections.Generic.SortedSet<string>(global::System.StringComparer.Ordinal);
var exactExclusions = new global::System.Collections.Generic.HashSet<string>(global::System.StringComparer.Ordinal);
var prefixExclusions = new global::System.Collections.Generic.List<string>();
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("<Project>");
builder.AppendLine(" <!-- This file is generated by GFramework's MSBuild transitive global usings pipeline. -->");
builder.AppendLine(" <!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->");
builder.AppendLine(" <!-- Add <GFrameworkExcludedUsing Include=\"Namespace\" /> to opt out of specific namespaces. -->");
builder.Append(" <ItemGroup Condition=\"'");
builder.Append(msbuildPropertyOpen);
builder.AppendLine("EnableGFrameworkGlobalUsings)' == 'true'\">");
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(" <Using Include=\"");
builder.Append(msbuildItemOpen);
builder.Append(NamespaceItemName);
builder.AppendLine(")\" />");
builder.AppendLine(" </ItemGroup>");
builder.AppendLine("</Project>");
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}.");
]]></Code>
</Task>
</UsingTask>
<PropertyGroup>
<_GFrameworkTransitiveGlobalUsingsEnabled Condition="'$(EnableGFrameworkPackageTransitiveGlobalUsings)' == 'true' and '$(IsPackable)' != 'false'">true</_GFrameworkTransitiveGlobalUsingsEnabled>
<_GFrameworkTransitiveGlobalUsingsPrimaryTargetFramework Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(TargetFrameworks)' != ''">$([System.String]::Copy('$(TargetFrameworks)').Split(';')[0])</_GFrameworkTransitiveGlobalUsingsPrimaryTargetFramework>
<_GFrameworkTransitiveGlobalUsingsGenerationBuild Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and ('$(TargetFrameworks)' == '' or '$(TargetFramework)' == '$(_GFrameworkTransitiveGlobalUsingsPrimaryTargetFramework)')">true</_GFrameworkTransitiveGlobalUsingsGenerationBuild>
<_GFrameworkTransitiveGlobalUsingsPackageId Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(PackageId)' != ''">$(PackageId)</_GFrameworkTransitiveGlobalUsingsPackageId>
<_GFrameworkTransitiveGlobalUsingsPackageId Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(_GFrameworkTransitiveGlobalUsingsPackageId)' == ''">$(AssemblyName)</_GFrameworkTransitiveGlobalUsingsPackageId>
<_GFrameworkTransitiveGlobalUsingsOutputFile Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true'">$(BaseIntermediateOutputPath)gframework/$(_GFrameworkTransitiveGlobalUsingsPackageId).props</_GFrameworkTransitiveGlobalUsingsOutputFile>
<_GFrameworkTransitiveGlobalUsingsItemName Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true'">_$([System.Text.RegularExpressions.Regex]::Replace('$(MSBuildProjectName)', '[^A-Za-z0-9_]', '_'))_TransitiveUsing</_GFrameworkTransitiveGlobalUsingsItemName>
</PropertyGroup>
<ItemGroup Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true'">
<None Include="$(_GFrameworkTransitiveGlobalUsingsOutputFile)"
Pack="true"
PackagePath="buildTransitive"
Visible="false"/>
</ItemGroup>
<Target Name="GenerateGFrameworkModuleTransitiveGlobalUsings"
Condition="'$(_GFrameworkTransitiveGlobalUsingsGenerationBuild)' == 'true'"
BeforeTargets="CoreCompile;GenerateNuspec">
<GenerateGFrameworkTransitiveGlobalUsingsProps
SourceFiles="@(Compile->'%(FullPath)')"
ExcludedNamespaces="@(GFrameworkTransitiveUsingExclude)"
ExcludedNamespacePrefixes="@(GFrameworkTransitiveUsingExcludePrefix)"
OutputFile="$(_GFrameworkTransitiveGlobalUsingsOutputFile)"
NamespaceItemName="$(_GFrameworkTransitiveGlobalUsingsItemName)"/>
</Target>
</Project>

View File

@ -10,6 +10,7 @@
<MeziantouPolyfill_IncludedPolyfills>T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute</MeziantouPolyfill_IncludedPolyfills> <MeziantouPolyfill_IncludedPolyfills>T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute</MeziantouPolyfill_IncludedPolyfills>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup> </PropertyGroup>
<!-- 引入必要的命名空间 --> <!-- 引入必要的命名空间 -->

View File

@ -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;
/// <summary>
/// 验证运行时模块在构建期间会自动生成 transitive global usings 资产。
/// 该测试覆盖命名空间自动发现、框架侧过滤和消费者侧排除钩子的最终构建产物。
/// </summary>
[TestFixture]
public class TransitiveGlobalUsingsPackagingTests
{
/// <summary>
/// 使用真实类型派生架构命名空间,避免测试断言和命名空间重构脱节。
/// </summary>
private static readonly string ArchitectureNamespace = typeof(Architecture).Namespace
?? throw new InvalidOperationException(
"Architecture namespace should not be null.");
/// <summary>
/// 使用真实类型派生扩展命名空间,避免对字面量命名空间字符串的重复维护。
/// </summary>
private static readonly string ExtensionsNamespace = typeof(ContextAwareEnvironmentExtensions).Namespace
?? throw new InvalidOperationException(
"Extensions namespace should not be null.");
/// <summary>
/// 使用真实类型派生协程扩展命名空间,确保断言和源码自动发现保持一致。
/// </summary>
private static readonly string CoroutineExtensionsNamespace = typeof(CoroutineExtensions).Namespace
?? throw new InvalidOperationException(
"Coroutine extensions namespace should not be null.");
/// <summary>
/// 验证 GFramework.Core 在构建后会生成 transitive global usings props
/// 且 props 内容来自源码自动发现,并保留消费者侧排除机制。
/// </summary>
[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"));
}
/// <summary>
/// 基于当前测试源文件的已知位置解析仓库根目录。
/// 这里不扫描解决方案文件,避免测试对仓库布局演进产生额外脆弱性。
/// </summary>
/// <param name="sourceFilePath">由编译器注入的当前测试源文件绝对路径。</param>
/// <returns>仓库根目录绝对路径。</returns>
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, "..", ".."));
}
}

View File

@ -6,6 +6,7 @@
<ImplicitUsings>disable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/> <ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj"/>

View File

@ -4,10 +4,10 @@
<PackageId>GeWuYou.$(AssemblyName)</PackageId> <PackageId>GeWuYou.$(AssemblyName)</PackageId>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/> <ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -7,6 +7,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo> <GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -19,5 +20,4 @@
<PackageReference Include="Arch" Version="2.1.0"/> <PackageReference Include="Arch" Version="2.1.0"/>
<PackageReference Include="Arch.System" Version="1.1.0"/> <PackageReference Include="Arch.System" Version="1.1.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -10,6 +10,7 @@
<MeziantouPolyfill_IncludedPolyfills>T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute</MeziantouPolyfill_IncludedPolyfills> <MeziantouPolyfill_IncludedPolyfills>T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute</MeziantouPolyfill_IncludedPolyfills>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/> <ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj"/>

View File

@ -6,6 +6,7 @@
<ImplicitUsings>disable</ImplicitUsings> <ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/> <ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>

View File

@ -6,6 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnableGFrameworkPackageTransitiveGlobalUsings>true</EnableGFrameworkPackageTransitiveGlobalUsings>
<!-- Godot.SourceGenerators expects this property from Godot.NET.Sdk. <!-- Godot.SourceGenerators expects this property from Godot.NET.Sdk.
Provide a safe default so source generators can run in plain SDK-style builds as well. --> Provide a safe default so source generators can run in plain SDK-style builds as well. -->
<GodotProjectDir Condition="'$(GodotProjectDir)' == ''">$(MSBuildProjectDirectory)</GodotProjectDir> <GodotProjectDir Condition="'$(GodotProjectDir)' == ''">$(MSBuildProjectDirectory)</GodotProjectDir>

View File

@ -72,6 +72,32 @@ dotnet add package GeWuYou.GFramework.Godot
dotnet add package GeWuYou.GFramework.SourceGenerators dotnet add package GeWuYou.GFramework.SourceGenerators
``` ```
## 可选模块导入
发布后的运行时包支持可选的模块级自动导入,但默认关闭,避免在普通项目里无意污染命名空间。
在 NuGet 消费项目中显式开启:
```xml
<PropertyGroup>
<EnableGFrameworkGlobalUsings>true</EnableGFrameworkGlobalUsings>
</PropertyGroup>
```
启用后,项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入其推荐命名空间。
如果某几个命名空间不想导入,可以局部排除:
```xml
<ItemGroup>
<GFrameworkExcludedUsing Include="GFramework.Core.Environment" />
<GFrameworkExcludedUsing Include="GFramework.Godot.Extensions" />
</ItemGroup>
```
> 该能力面向 NuGet 包消费场景。若你在本地解决方案中直接使用 `ProjectReference`,仍建议保留自己的 `GlobalUsings.cs` 或手写
`using`
## 仓库结构 ## 仓库结构
```text ```text

View File

@ -88,19 +88,28 @@ dotnet add package GeWuYou.GFramework.SourceGenerators
### 1. 基础配置 ### 1. 基础配置
创建 `GlobalUsings.cs` 文件 如果你通过 NuGet 包使用 GFramework并且希望自动导入已安装模块的推荐命名空间可以在项目文件中显式开启
```csharp ```xml
global using GFramework.Core; <PropertyGroup>
global using GFramework.Core.Architecture; <EnableGFrameworkGlobalUsings>true</EnableGFrameworkGlobalUsings>
global using GFramework.Core.Command; </PropertyGroup>
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;
``` ```
启用后,当前项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入对应命名空间。
如果你想排除局部导入,可以继续在项目文件中添加排除项:
```xml
<ItemGroup>
<GFrameworkExcludedUsing Include="GFramework.Core.Environment"/>
<GFrameworkExcludedUsing Include="GFramework.Godot.Extensions"/>
</ItemGroup>
```
如果你使用的是本地 `ProjectReference`,或者希望完全手动控制导入范围,仍然可以继续维护自己的 `GlobalUsings.cs` 文件。
### 2. Godot 项目配置 ### 2. Godot 项目配置
如果使用 Godot 集成,需要在项目设置中启用 C# 支持: 如果使用 Godot 集成,需要在项目设置中启用 C# 支持: