From f7022f6fe8855c71e0f43e7c649cfddcf6fa3b6f Mon Sep 17 00:00:00 2001 From: chinboy Date: Wed, 30 Sep 2026 07:29:48 +0800 Subject: [PATCH] =?UTF-8?q?perf(AOT):=20=E7=B2=BE=E7=AE=80=20Token=20?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=E7=AC=A6=E3=80=81=E6=94=BE=E5=AE=BD=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E5=B7=A5=E5=85=B7=E9=93=BE=E9=97=A8=E7=A6=81=E5=B9=B6?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E5=9F=BA=E7=BA=BF=E4=BD=93=E7=A7=AF=E6=A0=87?= =?UTF-8?q?=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 3 + build/AtomUI.Build.Tasks.Process.cs | 1 - build/AtomUI.Registration.targets | 6 +- .../foundations/aot-and-trimming.md | 32 ++++- docs/architecture/systems/theming/runtime.md | 9 +- docs/modules/typemap-linker/overview.md | 6 +- ...released-typemap-registration-migration.md | 11 ++ src/AtomUI.Build.Tasks/TypeMapPublishTasks.cs | 33 ++++-- .../Theme/Schema/TokenDescriptor.cs | 72 +++++++---- .../Theme/Schema/TokenValueAccessor.cs | 40 +++++++ .../Theme/Schema/TokenValueCodec.cs | 52 ++++++++ .../Registration/TypeMapRegistrationWriter.cs | 42 ++++--- .../ThemeSchema/GeneratedThemeSchemaWriter.cs | 98 ++++++++++++--- .../TypeMapPublishGateTests.cs | 48 +++++++- .../ThemeGeneratedDescriptorContractTests.cs | 112 ++++++++++++++++++ .../TypeMapRegistrationGeneratorTests.cs | 30 +++++ .../ThemeSchemaGeneratorTests.cs | 36 ++++-- .../TokenResourceKeyGeneratorTests.cs | 27 ++++- .../FluentBaseline/FluentBaseline.csproj | 10 ++ .../FluentBaseline/Program.cs | 23 ++++ tests/AtomUI.Registration.Fixtures/README.md | 8 ++ 21 files changed, 608 insertions(+), 91 deletions(-) create mode 100644 src/AtomUI.Core/Theme/Schema/TokenValueAccessor.cs create mode 100644 src/AtomUI.Core/Theme/Schema/TokenValueCodec.cs create mode 100644 tests/AtomUI.Registration.Fixtures/FluentBaseline/FluentBaseline.csproj create mode 100644 tests/AtomUI.Registration.Fixtures/FluentBaseline/Program.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7c3414b46..9081ebc0c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,6 +13,9 @@ + + + diff --git a/build/AtomUI.Build.Tasks.Process.cs b/build/AtomUI.Build.Tasks.Process.cs index 05a50ee46..a2a7e430f 100644 --- a/build/AtomUI.Build.Tasks.Process.cs +++ b/build/AtomUI.Build.Tasks.Process.cs @@ -287,7 +287,6 @@ namespace AtomUI.Build.Tasks { public sealed class ValidateRegistrationToolchainTask : IsolatedBuildTask { - public string SdkVersion { get; set; } = string.Empty; public string LinkerAssembly { get; set; } = string.Empty; public string NativeCompiler { get; set; } = string.Empty; } diff --git a/build/AtomUI.Registration.targets b/build/AtomUI.Registration.targets index 53d108557..4fe2afcb3 100644 --- a/build/AtomUI.Registration.targets +++ b/build/AtomUI.Registration.targets @@ -17,7 +17,7 @@ + NativeCompiler="$(IlcToolsPath)/ilc$(_ExeExt)" /> - + LinkerAssembly="$([System.IO.Path]::Combine($([System.IO.Path]::GetDirectoryName('$(ILLinkTasksAssembly)')), 'illink.dll'))" /> @@ -50,6 +49,7 @@ + .Instance`(Parse/Format)和按 Token 类生成的 `TokenValueAccessor` 子类(按 descriptor slot + switch 读写与投影,投影仍用属性静态类型选择 `ThemeResourceValue.Project` 重载)。accessor 只由所属 descriptor + 工厂引用,随未用控件一起裁剪;包级 lambda 静态缓存不再随全部 Token 保留。公开委托构造函数保留,旧生成代码无需重编。 - 可选 Own Token builder 的直接构造委托。 - Control CLR type 和 Own Token schema;descriptor 不包含 Global Token 消费白名单。 - 生成式 `XxxTokens.Identity`、`XxxTokenKey` 和 `XxxTokenResourceExtension`;Token key 必须强类型化,不能在 diff --git a/docs/modules/typemap-linker/overview.md b/docs/modules/typemap-linker/overview.md index 860f257b7..1e08b794b 100644 --- a/docs/modules/typemap-linker/overview.md +++ b/docs/modules/typemap-linker/overview.md @@ -92,9 +92,13 @@ custom-step 接口随工具链演进,发布构建只能使用已经验证的 后端另行读取当前已加载 ILLink 的完整程序集身份与 informational version,不能只相信调用者传入的字符串。 SDK 与实际 WASM workload/runtime assets 的组合检查由构建资产负责。当前组合固定为 SDK `10.0.300`、 ILLink `10.0.8`(informational version `10.0.8-servicing.26229.119+94ea82652cdd4e0f8046b5bd5becbd11461482ca`)、 -WASM SDK/runtime assets `10.0.10`,能力值为 `atomui-typemap-v1-illink-10.0.8`。NativeAOT 编译器为 `10.0.8`。 +WASM SDK/runtime assets `10.0.10`,能力值为 `atomui-typemap-v1-illink-10.0.8`。 这是一组已适配工具链,版本升级需要重新验证,不能仅修改版本门禁。 +上述精确组合只约束 Browser 后端,因为它读取 ILLink 的内部管线。桌面 trimmed CoreCLR 与 NativeAOT 只消费官方 +TypeMap 契约,不加载本工具,也不校验 SDK 版本字符串;它们只要求实际 ILLink(官方身份)与 NativeAOT 编译器的 +版本不低于已验证的 `10.0.8`,不设上限,因此运行时补丁前滚(例如 ILCompiler `10.0.9`)不会阻断桌面发布。 + 后端在筛选 marked accessor **之前**检查所有发现的标记包是否实际采用 `Link` action。 `Skip` 等 action 不保证方法有 Mark 记录,因此先筛 `IsMarked` 会把未转换包误判成零 slot。 工具不在 Mark 后改变 action,也不再次执行 Mark。 diff --git a/docs/releases/unreleased-typemap-registration-migration.md b/docs/releases/unreleased-typemap-registration-migration.md index 9ee2bba06..652d1d667 100644 --- a/docs/releases/unreleased-typemap-registration-migration.md +++ b/docs/releases/unreleased-typemap-registration-migration.md @@ -67,3 +67,14 @@ 第三方包需删除旧元数据、采用对应声明并用配套 Generator 普通重建;应用仍保留原包入口,预编译 adapter 不新增使用清单。 本次不改变 TypeMap ABI v1、资源 schema/fingerprint 或 Browser 转换协议,不拆分原生窗口包,也不扩大平台支持范围。 既有验证记录不证明本项迁移完成;本项验证关注行为、裁剪、增量正确性和分发,既有体积门槛与未达标记录继续保留。 + +## Token 描述符生成形态与桌面工具链门禁 + +普通 Generator 生成的 Token 描述符改用隐藏 ABI `TokenDescriptor.CreateGenerated`:按值类型共享 +`TokenValueCodec.Instance`,按 Token 类生成一个 `TokenValueAccessor`,不再为每个 Token 生成 parse/format/get/set/project +五个委托。公开委托构造函数与 `Parse`/`Format`/`GetValue`/`SetValue`/`ProjectResourceValue` 行为不变;用旧 Generator 构建的 +第三方包无需修改即可继续运行,用当前 Generator 重建后才获得体积与启动分配收益。新生成代码需要配套的 AtomUI.Core。 + +桌面 trimmed CoreCLR 与 NativeAOT 发布不再要求 SDK 恰好为 `10.0.300`,也不再要求 ILCompiler 与 ILLink 的某个构建完全一致, +只要求实际官方 ILLink 与 NativeAOT 编译器不低于已验证的 `10.0.8`;运行时补丁前滚不再触发 `ATOMUIREG006`。 +Browser trimmed/AOT 发布依赖 ILLink 内部管线,继续固定 SDK `10.0.300`、ILLink `10.0.8` 与 WASM assets `10.0.10`。 diff --git a/src/AtomUI.Build.Tasks/TypeMapPublishTasks.cs b/src/AtomUI.Build.Tasks/TypeMapPublishTasks.cs index cd8875f06..25ecb7bf5 100644 --- a/src/AtomUI.Build.Tasks/TypeMapPublishTasks.cs +++ b/src/AtomUI.Build.Tasks/TypeMapPublishTasks.cs @@ -13,6 +13,25 @@ internal static class TypeMapBuildContract internal const string Capability = "atomui-typemap-v1-illink-10.0.8"; internal const string LinkerIdentity = "illink, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"; internal const string LinkerVersion = "10.0.8-servicing.26229.119+94ea82652cdd4e0f8046b5bd5becbd11461482ca"; + // Desktop trimming consumes only the official TypeMap contract, so any official toolchain at or after the + // verified servicing build is accepted. The Browser backend depends on ILLink internals and keeps exact pins. + internal static readonly Version DesktopToolchainFloor = new(10, 0, 8); + private const string MicrosoftPublicKeyToken = "31BF3856AD364E35"; + + internal static bool IsSupportedDesktopToolchain(string informationalVersion) + { + var end = informationalVersion.IndexOfAny(['-', '+']); + var numeric = end < 0 ? informationalVersion : informationalVersion[..end]; + return Version.TryParse(numeric.Trim(), out var version) && version >= DesktopToolchainFloor; + } + + internal static bool IsSupportedDesktopLinker(string identity, string informationalVersion) + { + var name = new AssemblyName(identity); + return name.Name == "illink" && + Convert.ToHexString(name.GetPublicKeyToken() ?? []) == MicrosoftPublicKeyToken && + IsSupportedDesktopToolchain(informationalVersion); + } internal static string Hash(string path) => ResolveRegistrationToolsTask.Hash(path); internal static string InformationalVersion(string path) { @@ -205,17 +224,16 @@ public sealed class VerifyTypeMapReceiptTask : RegistrationBuildTask public sealed class ValidateRegistrationToolchainTask : RegistrationBuildTask { - public string SdkVersion { get; set; } = string.Empty; public string LinkerAssembly { get; set; } = string.Empty; public string NativeCompiler { get; set; } = string.Empty; public override bool Execute() { try { - if (SdkVersion != "10.0.300") throw new InvalidDataException($"Unsupported actual SDK '{SdkVersion}'. Expected 10.0.300."); - if (!string.IsNullOrEmpty(LinkerAssembly) && (AssemblyName.GetAssemblyName(LinkerAssembly).FullName != TypeMapBuildContract.LinkerIdentity || - TypeMapBuildContract.InformationalVersion(LinkerAssembly) != TypeMapBuildContract.LinkerVersion)) - throw new InvalidDataException($"Unsupported actual ILLink binary '{LinkerAssembly}'. Expected ILLink 10.0.8."); + if (!string.IsNullOrEmpty(LinkerAssembly) && !TypeMapBuildContract.IsSupportedDesktopLinker( + AssemblyName.GetAssemblyName(LinkerAssembly).FullName, TypeMapBuildContract.InformationalVersion(LinkerAssembly))) + throw new InvalidDataException( + $"Unsupported actual ILLink binary '{LinkerAssembly}'. Expected official ILLink {TypeMapBuildContract.DesktopToolchainFloor} or later."); if (!string.IsNullOrEmpty(NativeCompiler)) { using var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(NativeCompiler) @@ -225,8 +243,9 @@ public sealed class ValidateRegistrationToolchainTask : RegistrationBuildTask var version = process.StandardOutput.ReadToEnd().Trim(); var errors = process.StandardError.ReadToEnd(); process.WaitForExit(); - if (process.ExitCode != 0 || version != TypeMapBuildContract.LinkerVersion) - throw new InvalidDataException($"Unsupported actual NativeAOT compiler '{NativeCompiler}': {version} {errors}. Expected 10.0.8."); + if (process.ExitCode != 0 || !TypeMapBuildContract.IsSupportedDesktopToolchain(version)) + throw new InvalidDataException( + $"Unsupported actual NativeAOT compiler '{NativeCompiler}': {version} {errors}. Expected ILCompiler {TypeMapBuildContract.DesktopToolchainFloor} or later."); } return true; } diff --git a/src/AtomUI.Core/Theme/Schema/TokenDescriptor.cs b/src/AtomUI.Core/Theme/Schema/TokenDescriptor.cs index 4043184a0..86e494cb8 100644 --- a/src/AtomUI.Core/Theme/Schema/TokenDescriptor.cs +++ b/src/AtomUI.Core/Theme/Schema/TokenDescriptor.cs @@ -1,14 +1,12 @@ +using System.ComponentModel; using AtomUI.Theme.DesignTokens; namespace AtomUI.Theme.Schema; public sealed class TokenDescriptor { - private readonly Func _parser; - private readonly Func _formatter; - private readonly Func _getter; - private readonly Action _setter; - private readonly Func _resourceProjector; + private readonly TokenValueCodec _codec; + private readonly TokenValueAccessor _accessor; public TokenDescriptor( string name, @@ -32,43 +30,77 @@ public sealed class TokenDescriptor ArgumentNullException.ThrowIfNull(setter); ArgumentNullException.ThrowIfNull(resourceProjector); - Name = name; - Slot = slot; - Stage = stage; - ValueType = valueType; - ResourceKey = resourceKey; - _parser = parser; - _formatter = formatter; - _getter = getter; - _setter = setter; - _resourceProjector = resourceProjector; + Name = name; + Slot = slot; + Stage = stage; + ValueType = valueType; + ResourceKey = resourceKey; + _codec = new DelegateTokenValueCodec(valueType, parser, formatter); + _accessor = new DelegateTokenValueAccessor(getter, setter, resourceProjector); } + private TokenDescriptor( + string name, + int slot, + TokenStage stage, + TokenValueCodec codec, + object resourceKey, + TokenValueAccessor accessor) + { + SchemaIdentifier.Validate(name, nameof(name)); + ArgumentOutOfRangeException.ThrowIfNegative(slot); + ArgumentNullException.ThrowIfNull(codec); + ArgumentNullException.ThrowIfNull(resourceKey); + ArgumentNullException.ThrowIfNull(accessor); + + Name = name; + Slot = slot; + Stage = stage; + ValueType = codec.ValueType; + ResourceKey = resourceKey; + _codec = codec; + _accessor = accessor; + } + + /// + /// Generated-code entry point: the value codec is shared per value type and the accessor per token class, + /// so a descriptor carries no per-token delegates. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static TokenDescriptor CreateGenerated( + string name, + int slot, + TokenStage stage, + TokenValueCodec codec, + object resourceKey, + TokenValueAccessor accessor) => + new(name, slot, stage, codec, resourceKey, accessor); + public string Name { get; } public int Slot { get; } public TokenStage Stage { get; } public Type ValueType { get; } public object ResourceKey { get; } - public object? Parse(string value) => _parser(value); + public object? Parse(string value) => _codec.Parse(value); - public string Format(object? value) => _formatter(value); + public string Format(object? value) => _codec.Format(value); public object? GetValue(AbstractDesignToken token) { ArgumentNullException.ThrowIfNull(token); - return _getter(token); + return _accessor.GetValue(token, Slot); } public void SetValue(AbstractDesignToken token, object? value) { ArgumentNullException.ThrowIfNull(token); - _setter(token, value); + _accessor.SetValue(token, Slot, value); } public object? ProjectResourceValue(AbstractDesignToken token) { ArgumentNullException.ThrowIfNull(token); - return _resourceProjector(token); + return _accessor.ProjectResourceValue(token, Slot); } } diff --git a/src/AtomUI.Core/Theme/Schema/TokenValueAccessor.cs b/src/AtomUI.Core/Theme/Schema/TokenValueAccessor.cs new file mode 100644 index 000000000..171d13ad9 --- /dev/null +++ b/src/AtomUI.Core/Theme/Schema/TokenValueAccessor.cs @@ -0,0 +1,40 @@ +using System.ComponentModel; +using AtomUI.Theme.DesignTokens; + +namespace AtomUI.Theme.Schema; + +/// +/// Generated per-token-class accessor. slot is the owning descriptor's . +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public abstract class TokenValueAccessor +{ + public abstract object? GetValue(AbstractDesignToken token, int slot); + + public abstract void SetValue(AbstractDesignToken token, int slot, object? value); + + public abstract object? ProjectResourceValue(AbstractDesignToken token, int slot); +} + +internal sealed class DelegateTokenValueAccessor : TokenValueAccessor +{ + private readonly Func _getter; + private readonly Action _setter; + private readonly Func _resourceProjector; + + internal DelegateTokenValueAccessor( + Func getter, + Action setter, + Func resourceProjector) + { + _getter = getter; + _setter = setter; + _resourceProjector = resourceProjector; + } + + public override object? GetValue(AbstractDesignToken token, int slot) => _getter(token); + + public override void SetValue(AbstractDesignToken token, int slot, object? value) => _setter(token, value); + + public override object? ProjectResourceValue(AbstractDesignToken token, int slot) => _resourceProjector(token); +} diff --git a/src/AtomUI.Core/Theme/Schema/TokenValueCodec.cs b/src/AtomUI.Core/Theme/Schema/TokenValueCodec.cs new file mode 100644 index 000000000..3dbedfc00 --- /dev/null +++ b/src/AtomUI.Core/Theme/Schema/TokenValueCodec.cs @@ -0,0 +1,52 @@ +using System.ComponentModel; + +namespace AtomUI.Theme.Schema; + +[EditorBrowsable(EditorBrowsableState.Never)] +public abstract class TokenValueCodec +{ + private protected TokenValueCodec() + { + } + + internal abstract Type ValueType { get; } + + internal abstract object? Parse(string value); + + internal abstract string Format(object? value); +} + +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class TokenValueCodec : TokenValueCodec +{ + public static readonly TokenValueCodec Instance = new(); + + private TokenValueCodec() + { + } + + internal override Type ValueType => typeof(T); + + internal override object? Parse(string value) => ThemeTokenValueParser.Parse(value); + + internal override string Format(object? value) => ThemeTokenValueFormatter.Format((T)value!); +} + +internal sealed class DelegateTokenValueCodec : TokenValueCodec +{ + private readonly Func _parser; + private readonly Func _formatter; + + internal DelegateTokenValueCodec(Type valueType, Func parser, Func formatter) + { + ValueType = valueType; + _parser = parser; + _formatter = formatter; + } + + internal override Type ValueType { get; } + + internal override object? Parse(string value) => _parser(value); + + internal override string Format(object? value) => _formatter(value); +} diff --git a/src/AtomUI.Generator/Registration/TypeMapRegistrationWriter.cs b/src/AtomUI.Generator/Registration/TypeMapRegistrationWriter.cs index db28c2593..78fe1baa5 100644 --- a/src/AtomUI.Generator/Registration/TypeMapRegistrationWriter.cs +++ b/src/AtomUI.Generator/Registration/TypeMapRegistrationWriter.cs @@ -12,31 +12,28 @@ internal static class TypeMapRegistrationWriter Func? keyHash = null) { if (package.Controls.Count == 0 && package.Assets.Count == 0) return; - try + var groupName = "ControlPackageGroup_" + RegistrationNames.Hash(package.AssemblyIdentity); + var (mapEntries, collisions) = CreateMapEntries(package, groupName, keyHash ?? RegistrationNames.Hash); + if (collisions.Count != 0) { - context.AddSource( - "GeneratedTypeMapRegistration.g.cs", - GeneratedSourceText.From(Build(package, keyHash ?? RegistrationNames.Hash))); - } - catch (InvalidOperationException exception) when ( - exception.Message.StartsWith("TypeMap key collision", StringComparison.Ordinal)) - { - context.ReportDiagnostic(Diagnostic.Create( - AtomUIDiagnosticDescriptors.RegistrationIdentityConflict, - Location.None, - exception.Message)); + foreach (var collision in collisions) + { + context.ReportDiagnostic(Diagnostic.Create( + AtomUIDiagnosticDescriptors.RegistrationIdentityConflict, + Location.None, + collision)); + } + return; } + context.AddSource( + "GeneratedTypeMapRegistration.g.cs", + GeneratedSourceText.From(Build(package, groupName, mapEntries))); } - internal static string Build(RegistrationPackage package) => - Build(package, RegistrationNames.Hash); - - private static string Build(RegistrationPackage package, Func keyHash) + private static string Build(RegistrationPackage package, string groupName, IReadOnlyList mapEntries) { var source = new StringBuilder("// \n#nullable enable\n"); - var groupName = "ControlPackageGroup_" + RegistrationNames.Hash(package.AssemblyIdentity); var group = "global::" + package.Namespace + "." + groupName; - var mapEntries = CreateMapEntries(package, groupName, keyHash); source.Append("[assembly: global::AtomUI.Registration.ControlPackageMarker(").Append(L(package.Id)).Append(", typeof(").Append(group).AppendLine("), 1)]"); // Conditional TypeMap annotations are interpreted by the pinned ILLink/ILC implementations. // Their experimental/trim diagnostics are suppressed only at these generated declarations. @@ -136,7 +133,7 @@ internal static class TypeMapRegistrationWriter .Append(", GeneratedRegistrationFactories.").Append(RegistrationNames.AssetFactory(asset.Id)).AppendLine("));"); } private static string Identity(RegistrationOwner owner) => "global::AtomUI.Theme.Schema.ControlTokenIdentity.ForControl(typeof(" + owner.Type.Name + "), " + L(owner.Catalog) + ", " + L(owner.Id) + ")"; - private static IReadOnlyList CreateMapEntries( + private static (IReadOnlyList Entries, IReadOnlyList Collisions) CreateMapEntries( RegistrationPackage package, string groupName, Func keyHash) @@ -144,6 +141,7 @@ internal static class TypeMapRegistrationWriter var groupIdentity = package.AssemblyIdentity + "|" + package.Namespace + "." + groupName; var identities = new Dictionary(StringComparer.Ordinal); var entries = new List(); + var collisions = new List(); foreach (var control in package.Controls) foreach (var trigger in control.Triggers) { @@ -155,14 +153,14 @@ internal static class TypeMapRegistrationWriter keyHash); if (identities.TryGetValue(key, out var existing) && existing != fullIdentity) { - throw new InvalidOperationException( - $"TypeMap key collision '{key}' between '{existing}' and '{fullIdentity}'."); + collisions.Add($"TypeMap key collision '{key}' between '{existing}' and '{fullIdentity}'."); + continue; } identities[key] = fullIdentity; entries.Add(new(control, trigger, key)); } - return entries; + return (entries, collisions); } private sealed record TypeMapEntry(RegistrationControl Control, string Trigger, string Key); diff --git a/src/AtomUI.Generator/ThemeSchema/GeneratedThemeSchemaWriter.cs b/src/AtomUI.Generator/ThemeSchema/GeneratedThemeSchemaWriter.cs index c1adf8792..d1b1b197d 100644 --- a/src/AtomUI.Generator/ThemeSchema/GeneratedThemeSchemaWriter.cs +++ b/src/AtomUI.Generator/ThemeSchema/GeneratedThemeSchemaWriter.cs @@ -93,6 +93,7 @@ internal sealed class GeneratedThemeSchemaWriter token, slot, $"global::AtomUI.Theme.Resources.SharedTokenKind.{token.Name}", + GlobalTokenAccessorName, " ", ";"); source.AppendLine(" }"); @@ -111,6 +112,67 @@ internal sealed class GeneratedThemeSchemaWriter source.AppendLine(); } source.AppendLine("}"); + + if (_globalTokens.Count != 0) + { + source.AppendLine(); + WriteAccessor(source, GlobalTokenAccessorName, _globalTokens); + } + foreach (var control in _controls.Where(static control => control.HasDescriptor && control.HasOwnToken)) + { + var tokens = OrderedOwnTokens(control); + if (tokens.Count == 0) + { + continue; + } + source.AppendLine(); + WriteAccessor(source, GetControlTokenAccessorName(control), tokens); + } + } + + // One accessor per token class keeps descriptors free of per-token delegates; an accessor is referenced only + // from its own descriptor factory, so it is trimmed together with an unused control descriptor. + private static void WriteAccessor(StringBuilder source, string className, IReadOnlyList tokens) + { + const string tokenType = "global::AtomUI.Theme.DesignTokens.AbstractDesignToken"; + source.Append("internal sealed class ").Append(className).AppendLine(" : TokenValueAccessor"); + source.AppendLine("{"); + source.Append(" internal static readonly ").Append(className).Append(" Instance = new ") + .Append(className).AppendLine("();"); + source.AppendLine(); + source.Append(" public override object GetValue(").Append(tokenType).AppendLine(" token, int slot)"); + WriteSwitch(source, tokens, static (builder, token) => builder + .Append("return ((").Append(token.DeclaringType).Append(")token).").Append(token.Name).Append(';')); + source.AppendLine(); + source.Append(" public override void SetValue(").Append(tokenType).AppendLine(" token, int slot, object value)"); + WriteSwitch(source, tokens, static (builder, token) => builder + .Append("((").Append(token.DeclaringType).Append(")token).").Append(token.Name) + .Append(" = (").Append(token.ValueType).Append(")value!; return;")); + source.AppendLine(); + source.Append(" public override object ProjectResourceValue(").Append(tokenType).AppendLine(" token, int slot)"); + WriteSwitch(source, tokens, static (builder, token) => builder + .Append("return global::AtomUI.Theme.Schema.ThemeResourceValue.Project(((").Append(token.DeclaringType) + .Append(")token).").Append(token.Name).Append(");")); + source.AppendLine("}"); + } + + private static void WriteSwitch( + StringBuilder source, + IReadOnlyList tokens, + Action writeCase) + { + source.AppendLine(" {"); + source.AppendLine(" switch (slot)"); + source.AppendLine(" {"); + for (var slot = 0; slot < tokens.Count; slot++) + { + source.Append(" case ").Append(slot).Append(": "); + writeCase(source, tokens[slot]); + source.AppendLine(); + } + source.AppendLine(" default: throw new global::System.ArgumentOutOfRangeException(nameof(slot));"); + source.AppendLine(" }"); + source.AppendLine(" }"); } private void WriteControlDescriptor( @@ -137,11 +199,12 @@ internal sealed class GeneratedThemeSchemaWriter var typeName = FullyQualify(control.OwnToken!.GetFullyQualifiedTokenTypeName()); source.Append(indentation).AppendLine(" new TokenDescriptor[]"); source.Append(indentation).AppendLine(" {"); - var tokens = control.OwnSchemaTokens.OrderBy(static token => token.Name, StringComparer.Ordinal).ToArray(); - for (var slot = 0; slot < tokens.Length; slot++) + var tokens = OrderedOwnTokens(control); + var accessor = GetControlTokenAccessorName(control); + for (var slot = 0; slot < tokens.Count; slot++) { var resourceKey = $"global::{control.ControlNamespace}.DesignTokens.{control.TokenKindType}.{tokens[slot].Name}"; - WriteToken(source, tokens[slot], slot, resourceKey, indentation + " "); + WriteToken(source, tokens[slot], slot, resourceKey, accessor, indentation + " "); } source.Append(indentation).AppendLine(" },"); source.Append(indentation).Append(" static () => new ").Append(typeName).AppendLine("(),"); @@ -170,26 +233,22 @@ internal sealed class GeneratedThemeSchemaWriter SchemaTokenInfo token, int slot, string resourceKey, + string accessor, string indentation = " ", string suffix = ",") { var name = SymbolDisplay.FormatLiteral(token.Name, quote: true); - source.Append(indentation).Append("new TokenDescriptor(").Append(name).Append(", ").Append(slot) - .Append(", TokenStage.").Append(token.Stage).Append(", typeof(").Append(token.ValueType) - .Append("), ").Append(resourceKey).AppendLine(","); - source.Append(indentation).Append(" static value => ThemeTokenValueParser.Parse<") - .Append(token.ValueType).AppendLine(">(value),"); - source.Append(indentation).Append(" static value => ThemeTokenValueFormatter.Format((") - .Append(token.ValueType).AppendLine(")value!),"); - source.Append(indentation).Append(" static token => ((").Append(token.DeclaringType).Append(")token).") - .Append(token.Name).AppendLine(","); - source.Append(indentation).Append(" static (token, value) => ((").Append(token.DeclaringType).Append(")token).") - .Append(token.Name).Append(" = (").Append(token.ValueType).AppendLine(")value!,"); - source.Append(indentation).Append(" static token => ThemeResourceValue.Project(((") - .Append(token.DeclaringType).Append(")token).") - .Append(token.Name).Append("))").AppendLine(suffix); + source.Append(indentation).Append("TokenDescriptor.CreateGenerated(").Append(name).Append(", ").Append(slot) + .Append(", TokenStage.").Append(token.Stage) + .Append(", TokenValueCodec<").Append(token.ValueType).Append(">.Instance, ") + .Append(resourceKey).Append(", ").Append(accessor).Append(".Instance)").AppendLine(suffix); } + private static IReadOnlyList OrderedOwnTokens(ControlThemeInfo control) => + control.OwnSchemaTokens.OrderBy(static token => token.Name, StringComparer.Ordinal).ToArray(); + + private const string GlobalTokenAccessorName = "GeneratedGlobalTokenAccessor"; + private static string GetGlobalTokenDescriptorFactoryMethodName(SchemaTokenInfo token) { return $"CreateGlobalTokenDescriptor_{token.Name}"; @@ -200,6 +259,11 @@ internal sealed class GeneratedThemeSchemaWriter return $"CreateControlDescriptor_{ToIdentifier(control.ControlName)}_{ComputeHash(control.ControlMetadataName):X16}"; } + private static string GetControlTokenAccessorName(ControlThemeInfo control) + { + return $"GeneratedTokenAccessor_{ToIdentifier(control.ControlName)}_{ComputeHash(control.ControlMetadataName):X16}"; + } + private static string ToIdentifier(string value) { var builder = new StringBuilder(value.Length + 1); diff --git a/tests/AtomUI.Build.Tasks.Tests/TypeMapPublishGateTests.cs b/tests/AtomUI.Build.Tasks.Tests/TypeMapPublishGateTests.cs index 33f462aa1..5b1898b24 100644 --- a/tests/AtomUI.Build.Tasks.Tests/TypeMapPublishGateTests.cs +++ b/tests/AtomUI.Build.Tasks.Tests/TypeMapPublishGateTests.cs @@ -7,17 +7,61 @@ namespace AtomUI.Build.Tasks.Tests; public class TypeMapPublishGateTests { [Fact] - public void Incorrect_actual_linker_binary_is_rejected_even_with_supported_sdk() + public void Incorrect_actual_linker_binary_is_rejected() { var result = TaskProcessHost.Execute(new TaskRequest { TaskName = "ValidateRegistrationToolchainTask", - Properties = { ["SdkVersion"] = "10.0.300", ["LinkerAssembly"] = typeof(TaskProcessHost).Assembly.Location } + Properties = { ["LinkerAssembly"] = typeof(TaskProcessHost).Assembly.Location } }); result.Success.ShouldBeFalse(); result.Diagnostics.ShouldContain(d => d.Code == "ATOMUIREG006"); } + [Theory] + [InlineData("10.0.8-servicing.26229.119+94ea82652cdd4e0f8046b5bd5becbd11461482ca", true)] + [InlineData("10.0.9-servicing.26270.113+901ca941248413c79832d2fdbd709da0c4386353", true)] + [InlineData("11.0.0-preview.3.26201.1+0123456789abcdef", true)] + [InlineData("10.0.7-servicing.26170.101+0123456789abcdef", false)] + [InlineData("8.0.28+46295af5828b062bbbf93a9cef50fd8cb9fbcb09", false)] + [InlineData("not-a-version", false)] + public void Desktop_native_compiler_accepts_the_verified_servicing_floor_and_later(string reportedVersion, bool accepted) + { + Assert.SkipWhen(OperatingSystem.IsWindows(), "The fake compiler is a POSIX shell script."); + var directory = Directory.CreateTempSubdirectory("atomui-fake-ilc-"); + try + { + var compiler = Path.Combine(directory.FullName, "ilc"); + File.WriteAllText(compiler, "#!/bin/sh\necho '" + reportedVersion + "'\n"); + File.SetUnixFileMode(compiler, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + + var result = TaskProcessHost.Execute(new TaskRequest + { + TaskName = "ValidateRegistrationToolchainTask", + Properties = { ["NativeCompiler"] = compiler } + }); + + result.Success.ShouldBe(accepted); + if (!accepted) result.Diagnostics.ShouldContain(d => d.Code == "ATOMUIREG006"); + } + finally + { + directory.Delete(recursive: true); + } + } + + [Theory] + [InlineData("illink, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "10.0.9-servicing.26270.113+901ca941", true)] + [InlineData("illink, Version=11.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "11.0.0-preview.3.26201.1+01234567", true)] + [InlineData("illink, Version=10.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", "10.0.7-servicing.26170.101+01234567", false)] + [InlineData("illink, Version=10.0.0.0, Culture=neutral, PublicKeyToken=0123456789abcdef", "10.0.9-servicing.26270.113+901ca941", false)] + [InlineData("AtomUI.Build.Tasks, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.9", false)] + public void Desktop_linker_requires_official_identity_at_or_after_the_verified_floor( + string identity, string informationalVersion, bool accepted) + { + TypeMapBuildContract.IsSupportedDesktopLinker(identity, informationalVersion).ShouldBe(accepted); + } + [Fact] public void Missing_receipt_is_a_publish_error_even_with_no_reachable_slots() { diff --git a/tests/AtomUI.Core.Tests/Theme/ThemeGeneratedDescriptorContractTests.cs b/tests/AtomUI.Core.Tests/Theme/ThemeGeneratedDescriptorContractTests.cs index 3f0ff19cf..c7c4da2b7 100644 --- a/tests/AtomUI.Core.Tests/Theme/ThemeGeneratedDescriptorContractTests.cs +++ b/tests/AtomUI.Core.Tests/Theme/ThemeGeneratedDescriptorContractTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using AtomUI.Generated.AtomUICore; using AtomUI.Theme; using AtomUI.Theme.Algorithms; using AtomUI.Theme.Schema; @@ -68,6 +69,66 @@ public class ThemeGeneratedDescriptorContractTests descriptor.ResourceKey.ShouldBe(SchemaResourceKey.Scale); } + [Fact] + public void Generated_Global_Descriptors_Read_Write_Format_And_Project_Their_Own_Property() + { + var snapshot = ThemeTestSnapshotFactory.Compile(); + var descriptors = GeneratedThemeSchema.GetGlobalTokens(); + descriptors.Count.ShouldBeGreaterThan(200); + + foreach (var descriptor in descriptors) + { + var property = typeof(DesignToken).GetProperty(descriptor.Name).ShouldNotBeNull(descriptor.Name); + property.PropertyType.ShouldBe(descriptor.ValueType, descriptor.Name); + var compiled = snapshot.GlobalTokenValues.GetValue(descriptor.Slot); + var token = new DesignToken(); + + descriptor.SetValue(token, compiled); + + property.GetValue(token).ShouldBe(compiled, descriptor.Name); + descriptor.GetValue(token).ShouldBe(compiled, descriptor.Name); + var text = descriptor.Format(compiled); + text.ShouldBe(InvokeShared(typeof(ThemeTokenValueFormatter), "Format", descriptor.ValueType, compiled), descriptor.Name); + Outcome(() => descriptor.Parse(text)).ShouldBe( + Outcome(() => InvokeShared(typeof(ThemeTokenValueParser), "Parse", descriptor.ValueType, text)), descriptor.Name); + var projected = descriptor.ProjectResourceValue(token); + if (compiled is Color color) + { + projected.ShouldBeOfType(descriptor.Name).Color.ShouldBe(color, descriptor.Name); + } + else + { + projected.ShouldBe(compiled, descriptor.Name); + } + } + } + + [Fact] + public void Generated_Descriptor_Dispatches_Its_Slot_To_The_Shared_Accessor_And_Value_Codec() + { + var first = TokenDescriptor.CreateGenerated( + "First", 0, TokenStage.Seed, TokenValueCodec.Instance, SchemaResourceKey.Scale, PairAccessor.Instance); + var second = TokenDescriptor.CreateGenerated( + "Second", 1, TokenStage.Seed, TokenValueCodec.Instance, "Second", PairAccessor.Instance); + var token = new PairToken(); + + first.ValueType.ShouldBe(typeof(double)); + second.ValueType.ShouldBe(typeof(Color)); + first.Parse("1.5").ShouldBe(1.5); + second.Format(Color.Parse("#1677ff")).ShouldBe(ThemeTokenValueFormatter.Format(Color.Parse("#1677ff"))); + first.SetValue(token, 2.5); + second.SetValue(token, Color.Parse("#ff0000")); + + token.First.ShouldBe(2.5); + token.Second.ShouldBe(Color.Parse("#ff0000")); + first.GetValue(token).ShouldBe(2.5); + second.GetValue(token).ShouldBe(Color.Parse("#ff0000")); + first.ProjectResourceValue(token).ShouldBe(2.5); + second.ProjectResourceValue(token).ShouldBeOfType().Color.ShouldBe(Color.Parse("#ff0000")); + first.ResourceKey.ShouldBe(SchemaResourceKey.Scale); + Should.Throw(() => first.GetValue(null!)); + } + [Fact] public void Registry_Assigns_Deterministic_Control_Slots_And_Shares_Global_Schema() { @@ -156,6 +217,22 @@ public class ThemeGeneratedDescriptorContractTests .ShouldBe(color); } + private static object? InvokeShared(Type helper, string method, Type valueType, object? argument) => + helper.GetMethod(method)!.MakeGenericMethod(valueType).Invoke(null, [argument]); + + // Compares either the produced value or the type of the thrown exception. + private static object? Outcome(Func action) + { + try + { + return action(); + } + catch (Exception exception) + { + return (exception is System.Reflection.TargetInvocationException { InnerException: { } inner } ? inner : exception).GetType(); + } + } + private static TokenDescriptor NumberToken(string name, int slot, TokenStage stage) { return new TokenDescriptor( @@ -202,6 +279,41 @@ public class ThemeGeneratedDescriptorContractTests public double Scale { get; set; } } + private sealed class PairToken : AbstractDesignToken + { + public double First { get; set; } + public Color Second { get; set; } + } + + private sealed class PairAccessor : TokenValueAccessor + { + public static readonly PairAccessor Instance = new(); + + public override object? GetValue(AbstractDesignToken token, int slot) => slot switch + { + 0 => ((PairToken)token).First, + 1 => ((PairToken)token).Second, + _ => throw new ArgumentOutOfRangeException(nameof(slot)) + }; + + public override void SetValue(AbstractDesignToken token, int slot, object? value) + { + switch (slot) + { + case 0: ((PairToken)token).First = (double)value!; break; + case 1: ((PairToken)token).Second = (Color)value!; break; + default: throw new ArgumentOutOfRangeException(nameof(slot)); + } + } + + public override object? ProjectResourceValue(AbstractDesignToken token, int slot) => slot switch + { + 0 => ThemeResourceValue.Project(((PairToken)token).First), + 1 => ThemeResourceValue.Project(((PairToken)token).Second), + _ => throw new ArgumentOutOfRangeException(nameof(slot)) + }; + } + private sealed class SchemaControlToken : AbstractControlDesignToken { public double Value { get; set; } diff --git a/tests/AtomUI.Generator.Tests/Registration/TypeMapRegistrationGeneratorTests.cs b/tests/AtomUI.Generator.Tests/Registration/TypeMapRegistrationGeneratorTests.cs index 65463f30d..bcac40c65 100644 --- a/tests/AtomUI.Generator.Tests/Registration/TypeMapRegistrationGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/Registration/TypeMapRegistrationGeneratorTests.cs @@ -132,6 +132,36 @@ public class TypeMapRegistrationGeneratorTests result.Files.ShouldBeEmpty(); } + [Fact] + public void TypeMap_Key_Collisions_Are_All_Reported_Against_The_First_Identity() + { + RegistrationControl Control(string name) => new( + new RegistrationType("global::Demo." + name, "Demo." + name, "Demo." + name + ", Demo"), + null, + null, + new ValueArray(["global::Demo." + name]), + new ValueArray([]), + new ValueArray([])); + var package = new RegistrationPackage( + "Demo", + "Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", + "Demo.Generated", + new ValueArray([Control("Alpha"), Control("Beta"), Control("Gamma")]), + new ValueArray([]), + new ValueArray([])); + var output = new GenerationOutput(); + + TypeMapRegistrationWriter.Write(output, package, static _ => "0000000000000000"); + var result = output.Freeze(); + + var collisions = result.Diagnostics.Where(diagnostic => diagnostic.Descriptor.Id == "ATOMUIREG005").ToArray(); + collisions.Length.ShouldBe(2); + collisions.ShouldAllBe(diagnostic => diagnostic.Message.Contains("Demo.Alpha, Demo")); + collisions.ShouldContain(diagnostic => diagnostic.Message.Contains("Demo.Beta, Demo")); + collisions.ShouldContain(diagnostic => diagnostic.Message.Contains("Demo.Gamma, Demo")); + result.Files.ShouldBeEmpty(); + } + [Fact] public void Exports_Respect_Lexical_Scope_Named_Keys_And_Multiple_Targets() { diff --git a/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs b/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs index ababca5fc..5002f9bfa 100644 --- a/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs @@ -28,14 +28,17 @@ public class ThemeSchemaGeneratorTests source.ShouldContain("typeof(global::Demo.Button)"); source.ShouldContain("ControlTokenIdentity.ForControl(typeof(global::Demo.Button), \"AtomUI\", \"Button\")"); source.ShouldContain("global::AtomUI.Theme.Resources.SharedTokenKind"); - source.ShouldContain("new TokenDescriptor(\"Alpha\", 0, TokenStage.Seed"); - source.ShouldContain("new TokenDescriptor(\"Zeta\", 1, TokenStage.Alias"); - source.ShouldContain("new TokenDescriptor(\"Height\", 0, TokenStage.Control"); - source.ShouldContain("new TokenDescriptor(\"Label\", 1, TokenStage.Control"); + source.ShouldContain("TokenDescriptor.CreateGenerated(\"Alpha\", 0, TokenStage.Seed, TokenValueCodec.Instance"); + source.ShouldContain("TokenDescriptor.CreateGenerated(\"Zeta\", 1, TokenStage.Alias"); + source.ShouldContain("TokenDescriptor.CreateGenerated(\"Height\", 0, TokenStage.Control"); + source.ShouldContain("TokenDescriptor.CreateGenerated(\"Label\", 1, TokenStage.Control"); source.ShouldNotContain("SupportedGlobalToken"); - source.ShouldContain("ThemeTokenValueParser.Parse(value)"); - source.ShouldContain("ThemeTokenValueFormatter.Format((global::System.Double)value!)"); - source.ShouldContain("((global::Demo.ButtonToken)token).Height = (global::System.Double)value!"); + source.ShouldContain("case 0: ((global::Demo.ButtonToken)token).Height = (global::System.Double)value!; return;"); + source.ShouldContain("case 0: return global::AtomUI.Theme.Schema.ThemeResourceValue.Project(((global::Demo.ButtonToken)token).Height);"); + // Per-token delegates and their static caches scale with the whole package, not with retained controls. + source.ShouldNotContain("static value =>"); + source.ShouldNotContain("static token =>"); + source.ShouldNotContain("static (token, value) =>"); source.ShouldContain("static () => new global::Demo.ButtonToken()"); source.ShouldContain("((global::Demo.ButtonToken)token).CalculateTokenValues(appearance == global::AtomUI.Theme.ThemeAppearance.Dark)"); source.ShouldContain("new ThemeAlgorithmDescriptor(global::AtomUI.Theme.Algorithms.ThemeAlgorithm.Dark, 1, ThemeAppearanceEffect.Dark"); @@ -320,8 +323,27 @@ public class ThemeSchemaGeneratorTests public static ControlTokenIdentity ForControl(System.Type controlType, string catalog, string id) => new(catalog, id); } + public abstract class TokenValueCodec + { + } + + public sealed class TokenValueCodec : TokenValueCodec + { + public static readonly TokenValueCodec Instance = new(); + } + + public abstract class TokenValueAccessor + { + public abstract object? GetValue(AtomUI.Theme.DesignTokens.AbstractDesignToken token, int slot); + public abstract void SetValue(AtomUI.Theme.DesignTokens.AbstractDesignToken token, int slot, object? value); + public abstract object? ProjectResourceValue(AtomUI.Theme.DesignTokens.AbstractDesignToken token, int slot); + } + public sealed class TokenDescriptor { + public static TokenDescriptor CreateGenerated( + string name, int slot, TokenStage stage, TokenValueCodec codec, object resourceKey, TokenValueAccessor accessor) => null!; + public TokenDescriptor( string name, int slot, diff --git a/tests/AtomUI.Generator.Tests/TokenResourceKeyGeneratorTests.cs b/tests/AtomUI.Generator.Tests/TokenResourceKeyGeneratorTests.cs index 624260cdf..cf3bbc2f6 100644 --- a/tests/AtomUI.Generator.Tests/TokenResourceKeyGeneratorTests.cs +++ b/tests/AtomUI.Generator.Tests/TokenResourceKeyGeneratorTests.cs @@ -450,9 +450,9 @@ public class TokenResourceKeyGeneratorTests tokenResources.ShouldNotContain("CommonButtonTokenKind"); var schema = GetGeneratedSource(outputCompilation, "GeneratedThemeSchema.g.cs"); - schema.ShouldContain("static token => ((global::Demo.ButtonToken)token).ContentHeight"); - schema.ShouldContain("static token => ((global::Demo.ButtonToken)token).CornerRadius"); - schema.ShouldContain("static token => ((global::Demo.ButtonToken)token).Label"); + schema.ShouldContain("return ((global::Demo.ButtonToken)token).ContentHeight;"); + schema.ShouldContain("return ((global::Demo.ButtonToken)token).CornerRadius;"); + schema.ShouldContain("return ((global::Demo.ButtonToken)token).Label;"); schema.ShouldContain("static () => new global::Demo.ButtonToken()"); } @@ -1131,7 +1131,7 @@ public class TokenResourceKeyGeneratorTests resources.ShouldContain("ContentHeight = -1"); resources.ShouldContain("CornerRadius = -2"); var schema = GetGeneratedSource(platformOutput, "GeneratedThemeSchema.g.cs"); - schema.ShouldContain("static token => ((global::PlatformControls.ButtonToken)token).ContentHeight"); + schema.ShouldContain("return ((global::PlatformControls.ButtonToken)token).ContentHeight;"); } [Fact] @@ -1797,8 +1797,27 @@ public class TokenResourceKeyGeneratorTests public static ControlTokenIdentity ForControl(System.Type controlType, string catalog, string id) => new(catalog, id); } + public abstract class TokenValueCodec + { + } + + public sealed class TokenValueCodec : TokenValueCodec + { + public static readonly TokenValueCodec Instance = new(); + } + + public abstract class TokenValueAccessor + { + public abstract object? GetValue(AtomUI.Theme.DesignTokens.AbstractDesignToken token, int slot); + public abstract void SetValue(AtomUI.Theme.DesignTokens.AbstractDesignToken token, int slot, object? value); + public abstract object? ProjectResourceValue(AtomUI.Theme.DesignTokens.AbstractDesignToken token, int slot); + } + public sealed class TokenDescriptor { + public static TokenDescriptor CreateGenerated( + string name, int slot, TokenStage stage, TokenValueCodec codec, object resourceKey, TokenValueAccessor accessor) => null!; + public TokenDescriptor( string name, int slot, diff --git a/tests/AtomUI.Registration.Fixtures/FluentBaseline/FluentBaseline.csproj b/tests/AtomUI.Registration.Fixtures/FluentBaseline/FluentBaseline.csproj new file mode 100644 index 000000000..c97554bde --- /dev/null +++ b/tests/AtomUI.Registration.Fixtures/FluentBaseline/FluentBaseline.csproj @@ -0,0 +1,10 @@ + + + ExeAtomUI.Registration.FluentBaseline + + + + + + diff --git a/tests/AtomUI.Registration.Fixtures/FluentBaseline/Program.cs b/tests/AtomUI.Registration.Fixtures/FluentBaseline/Program.cs new file mode 100644 index 000000000..8ef3af56d --- /dev/null +++ b/tests/AtomUI.Registration.Fixtures/FluentBaseline/Program.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Themes.Fluent; +using Avalonia.Threading; +using Avalonia.VisualTree; + +AppBuilder.Configure().UsePlatformDetect().WithInterFont().SetupWithoutStarting(); +var button = new Button { Content = "TypeMap product" }; +var stack = new StackPanel(); +stack.Children.Add(button); +var window = new Window { Content = stack, Width = 600, Height = 500 }; +window.Show(); +Dispatcher.UIThread.RunJobs(); +if (!button.GetVisualDescendants().OfType().Any()) + throw new InvalidOperationException("BASELINE_FAIL Fluent Button template did not execute"); +window.Close(); +Console.WriteLine("BASELINE_PASS fluent"); + +internal sealed class BaselineApp : Application +{ + public override void Initialize() => Styles.Add(new FluentTheme()); +} diff --git a/tests/AtomUI.Registration.Fixtures/README.md b/tests/AtomUI.Registration.Fixtures/README.md index bcfa28cae..d3e8d625a 100644 --- a/tests/AtomUI.Registration.Fixtures/README.md +++ b/tests/AtomUI.Registration.Fixtures/README.md @@ -30,6 +30,14 @@ The current post-compatibility-fix macOS 26.6.2 / SDK 10.0.300 / osx-arm64 measu The 2026-09-29 combined reflection-retention and short-key pass produced 20,182,000 / 45,001,952 bytes for selected/full under the default profile, a 55.15% selected reduction. `OptimizationPreference=Size` produced 19,717,728 / 44,139,696 bytes, a 55.33% selected reduction. Every binary reported `PRODUCT_PASS selected`. The selected executables remain 1,307,632 bytes (1.25 MiB) and 843,360 bytes (0.80 MiB), respectively, above the 18 MiB gate. +Size acceptance no longer uses an absolute MiB gate; the 18 MiB figures above are historical. `FluentBaseline` is the official Avalonia template (FluentTheme + Inter) with the same 600×500 window, StackPanel and Button, built with the same fixture settings and no AtomUI package. Publish it with exactly the Minimal command line (it needs no `FixtureRealDesktop` switch) and run it until it prints `BASELINE_PASS fluent`: + +```sh +LIBRARY_PATH=/opt/homebrew/lib:/opt/homebrew/opt/openssl@3/lib dotnet publish tests/AtomUI.Registration.Fixtures/FluentBaseline/FluentBaseline.csproj -c Release -r osx-arm64 --self-contained true -p:PublishAot=true -o .artifacts/registration-product/fluent-baseline +``` + +The framework delta is Minimal selected minus FluentBaseline under one toolchain. The attribution, ratchet and mechanism-detection rules are owned by [AOT and trimming](../../docs/architecture/foundations/aot-and-trimming.md#81-体积评判标准). + The inspector's `selected` mode validates absence using metadata names, without rooting unused classes through `typeof`. Browser runtime success requires `PRODUCT_PASS browser` after the UI loads, not just a successful page load. Mono AOT may strip IL after the receipt gate; receipt byte hashes bind linked/pre-AOT files, while deployed metadata and runtime are checked separately. `PackageConsumers/Author` proves ordinary authoring from the independently packed `AtomUI.Generator` package, including analyzer dependencies and generated package bootstrap. `PackageConsumers/Host` exercises direct, transitive and precompiled binary consumers; its own Directory.Build files intentionally do not import repository tooling. `NegativeBrowser` deliberately removes both converter and gate in its test project to demonstrate the Mono runtime failure; this is never a supported consumer configuration. `ZeroBrowser` verifies referenced-but-disabled packages produce a valid zero-slot link receipt.