mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-16 21:21:03 +08:00
perf(AOT): 精简 Token 描述符、放宽桌面工具链门禁并改用基线体积标准
This commit is contained in:
@@ -13,6 +13,9 @@
|
||||
<PackageVersion Include="Avalonia.X11" Version="$(AvaloniaVersion)"/>
|
||||
<PackageVersion Include="Avalonia.Wayland" Version="$(AvaloniaVersion)"/>
|
||||
<PackageVersion Include="Avalonia.Native" Version="$(AvaloniaVersion)"/>
|
||||
<!-- Official-template size baseline only (tests/AtomUI.Registration.Fixtures/FluentBaseline). -->
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)"/>
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)"/>
|
||||
<PackageVersion Include="NWayland" Version="0.11.0"/>
|
||||
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0"/>
|
||||
<PackageVersion Include="AvaloniaEdit.TextMate" Version="12.0.0"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</UsingTask>
|
||||
<Target Name="AtomUIValidateNativeCompiler" BeforeTargets="IlcCompile" Condition="'$(PublishAot)' == 'true'">
|
||||
<AtomUI.Build.Tasks.ValidateRegistrationToolchainTask TaskAssembly="$(AtomUIBuildTasksAssembly)" DotNetPath="$(DOTNET_HOST_PATH)"
|
||||
SdkVersion="$(NETCoreSdkVersion)" NativeCompiler="$(IlcToolsPath)/ilc$(_ExeExt)" />
|
||||
NativeCompiler="$(IlcToolsPath)/ilc$(_ExeExt)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="AtomUIResolveRegistrationGenerator" BeforeTargets="CoreCompile" DependsOnTargets="ResolveReferences"
|
||||
@@ -39,9 +39,8 @@
|
||||
Ordinary builds (even with inherited PublishTrimmed) retain the ordinary full collector. -->
|
||||
<Target Name="AtomUISetRegistrationPublishMode" BeforeTargets="PrepareForILLink"
|
||||
Condition="'$(OutputType)' == 'Exe' or '$(OutputType)' == 'WinExe'">
|
||||
<Error Condition="'$(NETCoreSdkVersion)' != '10.0.300'" Code="ATOMUIREG006" Text="AtomUI registration publish supports SDK 10.0.300; actual SDK is $(NETCoreSdkVersion)." />
|
||||
<AtomUI.Build.Tasks.ValidateRegistrationToolchainTask TaskAssembly="$(AtomUIBuildTasksAssembly)" DotNetPath="$(DOTNET_HOST_PATH)"
|
||||
SdkVersion="$(NETCoreSdkVersion)" LinkerAssembly="$([System.IO.Path]::Combine($([System.IO.Path]::GetDirectoryName('$(ILLinkTasksAssembly)')), 'illink.dll'))" />
|
||||
LinkerAssembly="$([System.IO.Path]::Combine($([System.IO.Path]::GetDirectoryName('$(ILLinkTasksAssembly)')), 'illink.dll'))" />
|
||||
<ItemGroup>
|
||||
<RuntimeHostConfigurationOption Remove="AtomUI.Registration.Trimmed" />
|
||||
<RuntimeHostConfigurationOption Include="AtomUI.Registration.Trimmed" Value="true" Trim="true" />
|
||||
@@ -50,6 +49,7 @@
|
||||
|
||||
<Target Name="AtomUIPrepareTypeMapBackend" AfterTargets="PrepareForILLink" BeforeTargets="_RunILLink"
|
||||
Condition="'$(RuntimeIdentifier)' == 'browser-wasm' and '$(PublishTrimmed)' == 'true' and ('$(OutputType)' == 'Exe' or '$(OutputType)' == 'WinExe')">
|
||||
<Error Condition="'$(NETCoreSdkVersion)' != '10.0.300'" Code="ATOMUIREG006" Text="AtomUI Browser TypeMap publish supports SDK 10.0.300; actual SDK is $(NETCoreSdkVersion)." />
|
||||
<Error Condition="'$(TargetFramework)' != 'net10.0-browser' or '$(TrimMode)' != 'full' or '$(RunILLink)' == 'false'" Code="ATOMUIREG006" Text="AtomUI Browser TypeMap requires net10.0-browser with actual full ILLink processing." />
|
||||
<MSBuild Projects="$(AtomUITypeMapBackendProject)" Targets="Build" BuildInParallel="false"
|
||||
RemoveProperties="RuntimeIdentifier;PublishAot;PublishTrimmed;RunAOTCompilation;SelfContained;TargetFramework;_IsPublishing"
|
||||
|
||||
@@ -34,6 +34,15 @@ Desktop 全部资产工厂生成文本与迁移前一致,包括有效平台域
|
||||
19,717,728 / 44,139,696 bytes,selected 缩小 55.33%。两组均通过运行检查和 40% 相对门槛;默认 selected 距
|
||||
18 MiB 门槛仍差 1,307,632 bytes(1.25 MiB),Size selected 仍差 843,360 bytes(0.80 MiB),因此绝对体积门槛仍未通过。
|
||||
|
||||
上述 MiB 门槛已由 [8.1 体积评判标准](#81-体积评判标准) 取代,以上数字作为历史记录保留。2026-09-29 Token 描述符改用共享
|
||||
codec/accessor 后,同口径(macOS 26.6.2、SDK 10.0.300、ILCompiler 10.0.8、Avalonia 12.1.3、osx-arm64、默认配置、仅
|
||||
AlibabaSans)实测:Minimal selected 19,715,664 bytes(较 20,182,000 减少 466,336),full 注册 43,304,544 bytes,
|
||||
selected 为 full 的 45.53%;FluentBaseline 16,573,248 bytes;框架增量 Δ = 3,142,416 bytes(AtomUI/Fluent 1.19,仅报告)。
|
||||
三者均运行通过。此 Δ 记为首个棘轮记录,但归因门槛尚未通过:已归因的大项为 AtomUI 程序集约 1.42 MB(其中语言核心的
|
||||
标准语言定义与 LanguageTags 约 250 KB)和默认图片服务网络栈约 1.0 MB(Net.Http/Security、Cryptography、Asn1、Sockets),
|
||||
Fluent 侧则多出主题与 Avalonia 模板约 2.44 MB;两个已知缺口是 Desktop 10 个未用控件语言目录(估算 30–40 KB,
|
||||
仍在 complete 阶段整包注册)以及 Regex、Immutable、Numerics 等较小 BCL 项尚未追到具体使用方。Size 档本轮未重测。
|
||||
|
||||
此前 Gallery Tab overflow shadow 渲染理论在原始基线及迁移分支均出现过失败,尚未确定成因;
|
||||
本次受影响渲染组 77/77 通过,但没有通过修改阴影实现来消除此历史问题。
|
||||
该失败不通过修改 Tab 几何或削弱像素断言绕过。完整 Browser Gallery、Windows/Linux/iOS 与较旧 macOS 的运行验证
|
||||
@@ -185,10 +194,25 @@ Common 在 Desktop 前、扩展包在基础包后的顺序由真实入口调用
|
||||
fixture 必须实际初始化 ThemeManager、解析资源、创建模板并切换主题。只构造 builder 或输出排序后的注册快照不能证明
|
||||
冻结时序、资源优先级和 UI 行为。负向保留断言不能通过 `typeof(UnusedControl)` 将被检查类型自行保留。
|
||||
|
||||
体积比较固定 SDK、RID、字体、配置与样例;记录 NativeAOT 主程序和排除调试符号后的 payload,不能相减不同实验程序大小。
|
||||
最小控件样例的 NativeAOT 主程序相对同口径 full 注册主程序至少缩小 `40%`。
|
||||
桌面固定 `osx-arm64` Button/Window 样例移除中文字体后,主程序第一阶段上限为 `18 MiB`,后续目标为 `16 MiB`
|
||||
或同场景 Fluent 的 125%;新增未使用控件后的主程序增量不得超过 `256 KiB`。改变门槛需要同口径的新证据。
|
||||
### 8.1 体积评判标准
|
||||
|
||||
体积不使用绝对 MiB 门槛。主程序中 BCL、运行时与 Avalonia 占大头,会随 SDK/Avalonia 升级变化且不受 AtomUI 控制;
|
||||
固定数值既区分不了“AtomUI 变大”和“工具链变了”,也说明不了剩余字节是否合理。标准只衡量 AtomUI 增加了什么、是否都有来由。
|
||||
|
||||
所有比较固定 SDK、RID、Avalonia 版本、配置、字体策略与原生库路径;记录 NativeAOT 主程序和排除调试符号后的 payload,
|
||||
只在同一口径的程序之间相减。
|
||||
|
||||
| 条目 | 类型 | 规则 |
|
||||
| --- | --- | --- |
|
||||
| 对照组 | 定义 | `tests/AtomUI.Registration.Fixtures/FluentBaseline`:官方 Avalonia 模板(FluentTheme + Inter),与 Minimal 相同的 600×500 窗口、StackPanel 与一个 Button,相同 fixture 构建设置,不含 AtomUI |
|
||||
| 框架增量 Δ | 指标 | Minimal selected 主程序 − FluentBaseline 主程序 |
|
||||
| 归因 | 硬门槛 | ILC map 中 Δ 必须归到已使用控件的契约或文档列明的 Package Core 能力(主题/Token 运行时、注册、语言核心、默认图片服务及其网络栈、默认字体);未用控件的类型、代理、工厂与本地化目录由 Inspector `selected` 模式证明缺失;归不进去的字节是缺陷,修复而不是放宽 |
|
||||
| 棘轮 | 硬门槛 | 验收记录 Δ 与工具链身份(SDK、ILC、Avalonia);同一工具链下 Δ 只能持平或下降,上升必须写明新增的具名能力并重新记录;升级工具链时两侧同时重测,比较 Δ 而不是绝对大小 |
|
||||
| 机制检测 | 硬门槛 | selected ≤ 同口径 full 注册的 60%;新增同目录未用控件后其代码与工厂缺失,主程序增量 ≤ 256 KiB。两者检测裁剪机制是否失效(正常约 45% 与数十 KiB,失效时接近 100% 与整控件体积),不是体积目标 |
|
||||
| 报告项 | 不设门槛 | AtomUI/Fluent 比值、`OptimizationPreference=Size` 数据 |
|
||||
|
||||
macOS arm64 的 Mach-O 段按 16 KiB 页对齐,主程序字节存在页量化;细粒度比较用 map 节点长度之和,主程序字节按页容差比较。
|
||||
改变规则需要同口径的新证据;本节取代此前的 18 MiB、16 MiB 与“同场景 Fluent 的 125%”门槛。
|
||||
|
||||
上线前还须审计源码、项目、脚本、正式文档、nupkg 和构建产物:旧应用 usage/Sidecar/UnitEdge/SCC/Plan、旧 analyzer、
|
||||
旧 linked MSBuild 传播均退出活动路径。不能只禁用 target 而继续编译分发旧工具。
|
||||
|
||||
@@ -288,7 +288,7 @@ Global Token snapshot。
|
||||
```text
|
||||
ThemeSchemaRegistry
|
||||
+-- GlobalTokenSchema
|
||||
| \-- token name, kind, stage, value type, parser, setter, getter, resource key
|
||||
| \-- token name, kind, stage, value codec, token accessor, resource key
|
||||
+-- ControlTokenDescriptor[ControlTokenIdentity]
|
||||
| \-- exact CLR type, factory, own token schema, evaluator, resource projector
|
||||
+-- ControlThemeAssetManifest
|
||||
@@ -336,8 +336,11 @@ descriptor 和资源扩展,只省略 Own Token builder/schema:
|
||||
- 稳定的 `ControlTokenIdentity`,以 catalog/control id 保持字符串相等语义;生成身份附带实际使用和校验的 owner Type。
|
||||
- 发现无参数 `[ControlDesignToken]` 标记的可选 Own Token 类型;标记的抽象类型只贡献
|
||||
定义,标记的 `sealed` 具体类型是终端 Own Token。Attribute 和抽象定义层都不参与 Control identity 或资产关联。
|
||||
- Own Token name 对应的强类型赋值委托查找表;没有 Own Token 时为空。
|
||||
- Own Token value 到 Avalonia resource value 的投影;没有 Own Token 时为空。
|
||||
- Own Token 的强类型读写与 Avalonia resource value 投影;没有 Own Token 时为空。生成代码不为每个 Token 生成委托:
|
||||
隐藏 ABI `TokenDescriptor.CreateGenerated(name, slot, stage, codec, resourceKey, accessor)` 接收按值类型共享的
|
||||
`TokenValueCodec<T>.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 必须强类型化,不能在
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -67,3 +67,14 @@
|
||||
第三方包需删除旧元数据、采用对应声明并用配套 Generator 普通重建;应用仍保留原包入口,预编译 adapter 不新增使用清单。
|
||||
本次不改变 TypeMap ABI v1、资源 schema/fingerprint 或 Browser 转换协议,不拆分原生窗口包,也不扩大平台支持范围。
|
||||
既有验证记录不证明本项迁移完成;本项验证关注行为、裁剪、增量正确性和分发,既有体积门槛与未达标记录继续保留。
|
||||
|
||||
## Token 描述符生成形态与桌面工具链门禁
|
||||
|
||||
普通 Generator 生成的 Token 描述符改用隐藏 ABI `TokenDescriptor.CreateGenerated`:按值类型共享
|
||||
`TokenValueCodec<T>.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`。
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using System.ComponentModel;
|
||||
using AtomUI.Theme.DesignTokens;
|
||||
|
||||
namespace AtomUI.Theme.Schema;
|
||||
|
||||
public sealed class TokenDescriptor
|
||||
{
|
||||
private readonly Func<string, object?> _parser;
|
||||
private readonly Func<object?, string> _formatter;
|
||||
private readonly Func<AbstractDesignToken, object?> _getter;
|
||||
private readonly Action<AbstractDesignToken, object?> _setter;
|
||||
private readonly Func<AbstractDesignToken, object?> _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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.ComponentModel;
|
||||
using AtomUI.Theme.DesignTokens;
|
||||
|
||||
namespace AtomUI.Theme.Schema;
|
||||
|
||||
/// <summary>
|
||||
/// Generated per-token-class accessor. <c>slot</c> is the owning descriptor's <see cref="TokenDescriptor.Slot"/>.
|
||||
/// </summary>
|
||||
[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<AbstractDesignToken, object?> _getter;
|
||||
private readonly Action<AbstractDesignToken, object?> _setter;
|
||||
private readonly Func<AbstractDesignToken, object?> _resourceProjector;
|
||||
|
||||
internal DelegateTokenValueAccessor(
|
||||
Func<AbstractDesignToken, object?> getter,
|
||||
Action<AbstractDesignToken, object?> setter,
|
||||
Func<AbstractDesignToken, object?> 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);
|
||||
}
|
||||
@@ -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<T> : TokenValueCodec
|
||||
{
|
||||
public static readonly TokenValueCodec<T> Instance = new();
|
||||
|
||||
private TokenValueCodec()
|
||||
{
|
||||
}
|
||||
|
||||
internal override Type ValueType => typeof(T);
|
||||
|
||||
internal override object? Parse(string value) => ThemeTokenValueParser.Parse<T>(value);
|
||||
|
||||
internal override string Format(object? value) => ThemeTokenValueFormatter.Format((T)value!);
|
||||
}
|
||||
|
||||
internal sealed class DelegateTokenValueCodec : TokenValueCodec
|
||||
{
|
||||
private readonly Func<string, object?> _parser;
|
||||
private readonly Func<object?, string> _formatter;
|
||||
|
||||
internal DelegateTokenValueCodec(Type valueType, Func<string, object?> parser, Func<object?, string> 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);
|
||||
}
|
||||
@@ -12,31 +12,28 @@ internal static class TypeMapRegistrationWriter
|
||||
Func<string, string>? 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<string, string> keyHash)
|
||||
private static string Build(RegistrationPackage package, string groupName, IReadOnlyList<TypeMapEntry> mapEntries)
|
||||
{
|
||||
var source = new StringBuilder("// <auto-generated />\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<TypeMapEntry> CreateMapEntries(
|
||||
private static (IReadOnlyList<TypeMapEntry> Entries, IReadOnlyList<string> Collisions) CreateMapEntries(
|
||||
RegistrationPackage package,
|
||||
string groupName,
|
||||
Func<string, string> keyHash)
|
||||
@@ -144,6 +141,7 @@ internal static class TypeMapRegistrationWriter
|
||||
var groupIdentity = package.AssemblyIdentity + "|" + package.Namespace + "." + groupName;
|
||||
var identities = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var entries = new List<TypeMapEntry>();
|
||||
var collisions = new List<string>();
|
||||
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);
|
||||
|
||||
@@ -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<SchemaTokenInfo> 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<SchemaTokenInfo> tokens,
|
||||
Action<StringBuilder, SchemaTokenInfo> 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<SchemaTokenInfo> 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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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<ImmutableSolidColorBrush>(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<double>.Instance, SchemaResourceKey.Scale, PairAccessor.Instance);
|
||||
var second = TokenDescriptor.CreateGenerated(
|
||||
"Second", 1, TokenStage.Seed, TokenValueCodec<Color>.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<ImmutableSolidColorBrush>().Color.ShouldBe(Color.Parse("#ff0000"));
|
||||
first.ResourceKey.ShouldBe(SchemaResourceKey.Scale);
|
||||
Should.Throw<ArgumentNullException>(() => 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<object?> 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; }
|
||||
|
||||
@@ -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<string>(["global::Demo." + name]),
|
||||
new ValueArray<string>([]),
|
||||
new ValueArray<string>([]));
|
||||
var package = new RegistrationPackage(
|
||||
"Demo",
|
||||
"Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||
"Demo.Generated",
|
||||
new ValueArray<RegistrationControl>([Control("Alpha"), Control("Beta"), Control("Gamma")]),
|
||||
new ValueArray<RegistrationAsset>([]),
|
||||
new ValueArray<string>([]));
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -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<global::System.Double>.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<global::System.Double>(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<T> : TokenValueCodec
|
||||
{
|
||||
public static readonly TokenValueCodec<T> 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,
|
||||
|
||||
@@ -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<T> : TokenValueCodec
|
||||
{
|
||||
public static readonly TokenValueCodec<T> 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,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Size reference for the Minimal fixture: the official Avalonia template (FluentTheme + Inter) with the same
|
||||
Window/Button content and the same fixture build settings, but no AtomUI package. -->
|
||||
<PropertyGroup><OutputType>Exe</OutputType><AssemblyName>AtomUI.Registration.FluentBaseline</AssemblyName></PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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<BaselineApp>().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<ContentPresenter>().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());
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user