diff --git a/build/AtomUI.LinkedRegistration.targets b/build/AtomUI.LinkedRegistration.targets
index 18d634551..5b1d3efde 100644
--- a/build/AtomUI.LinkedRegistration.targets
+++ b/build/AtomUI.LinkedRegistration.targets
@@ -49,6 +49,36 @@
AtomUILinkedSidecar="true"
Visible="false" />
+
+
+ <_AtomUILinkedExtractableReference
+ Include="@(ReferencePath)"
+ Condition="'%(ReferencePath.FileName)' != '' and
+ $([System.String]::Copy('%(ReferencePath.FileName)').StartsWith('AtomUI.', System.StringComparison.Ordinal)) and
+ !Exists('%(ReferencePath.FullPath).atomui-link.json')" />
+
+
+
+
+
+
+
+
+ RemoveProperties="PublishAot;PublishTrimmed;RunAOTCompilation;NoBuild">
diff --git a/controlgallery/AtomUIGallery.Browser/BrowserGalleryApplication.cs b/controlgallery/AtomUIGallery.Browser/BrowserGalleryApplication.cs
index f66e9b47d..9e5189835 100644
--- a/controlgallery/AtomUIGallery.Browser/BrowserGalleryApplication.cs
+++ b/controlgallery/AtomUIGallery.Browser/BrowserGalleryApplication.cs
@@ -4,6 +4,7 @@ using AtomUI.Desktop.Controls;
using AtomUI.Fonts.AlibabaPuHuiTi;
using AtomUI.Localization;
using AtomUI.Theme;
+using AtomUI.Toolkits.GalleryBase;
using AtomUIGallery.Localization;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
@@ -29,6 +30,7 @@ public partial class BrowserGalleryApplication : Application
builder.UseDesktopExtras();
builder.UseDesktopColorPicker();
builder.UseDesktopDataGrid();
+ builder.UseGalleryBase(AtomUIGalleryModule.Configure);
builder.UseGalleryControls();
});
}
diff --git a/controlgallery/AtomUIGallery.Desktop/GalleryApplication.cs b/controlgallery/AtomUIGallery.Desktop/GalleryApplication.cs
index b44100f4f..6cdab25fd 100644
--- a/controlgallery/AtomUIGallery.Desktop/GalleryApplication.cs
+++ b/controlgallery/AtomUIGallery.Desktop/GalleryApplication.cs
@@ -1,8 +1,9 @@
-using System.Globalization;
+using System.Globalization;
using AtomUI;
using AtomUI.Desktop.Controls;
using AtomUI.Localization;
using AtomUI.Theme;
+using AtomUI.Toolkits.GalleryBase;
using AtomUIGallery.Localization;
using Avalonia.Controls.ApplicationLifetimes;
@@ -31,6 +32,7 @@ public partial class GalleryApplication : BaseGalleryApplication
builder.UseDesktopExtras();
builder.UseDesktopColorPicker();
builder.UseDesktopDataGrid();
+ builder.UseGalleryBase(AtomUIGalleryModule.Configure);
builder.UseGalleryControls();
});
}
diff --git a/controlgallery/AtomUIGallery.Desktop/scripts/PublishToLocal.ps1 b/controlgallery/AtomUIGallery.Desktop/scripts/PublishToLocal.ps1
index 2c1e77623..e6b9d74c1 100644
--- a/controlgallery/AtomUIGallery.Desktop/scripts/PublishToLocal.ps1
+++ b/controlgallery/AtomUIGallery.Desktop/scripts/PublishToLocal.ps1
@@ -252,6 +252,7 @@ if ($publishAotEnabled) {
"--no-restore",
"-p:GalleryPublishTrimmed=true",
"-p:GalleryPublishAot=true",
+ "-p:AtomUILinkedPublish=true",
"--disable-build-servers",
"-m:1",
"/nr:false",
@@ -275,6 +276,7 @@ if ($publishAotEnabled) {
$buildType,
"-p:GalleryPublishTrimmed=true",
"-p:GalleryPublishAot=false",
+ "-p:AtomUILinkedPublish=true",
"-p:PublishSingleFile=true"
)
}
diff --git a/controlgallery/AtomUIGallery/ThemeManagerBuilderExtensions.cs b/controlgallery/AtomUIGallery/ThemeManagerBuilderExtensions.cs
index f91e7cf8f..b0189740e 100644
--- a/controlgallery/AtomUIGallery/ThemeManagerBuilderExtensions.cs
+++ b/controlgallery/AtomUIGallery/ThemeManagerBuilderExtensions.cs
@@ -2,17 +2,20 @@ using AtomUI;
using AtomUI.Generated.AtomUIGallery;
using AtomUI.Theme;
using AtomUI.Theme.Definitions;
-using AtomUI.Toolkits.GalleryBase;
namespace AtomUIGallery;
public static class ThemeManagerBuilderExtensions
{
+ ///
+ /// Registers Gallery-level services shared by all hosts. The GalleryBase control package
+ /// entry (UseGalleryBase) must be invoked directly by the application project so the
+ /// linked-registration plan can see it.
+ ///
public static IAtomUIBuilder UseGalleryControls(this IAtomUIBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
GeneratedLanguageModuleRegistration.Register(builder.Localization);
- builder.UseGalleryBase(AtomUIGalleryModule.Configure);
builder.Theme.AddThemeDefinitionResolver(
new AvaloniaAssetThemeDefinitionResolver(
"AtomUIGallery.BuiltInThemes",
diff --git a/docs/architecture/foundations/aot-and-trimming.md b/docs/architecture/foundations/aot-and-trimming.md
index 076cf1278..5f0f6f6e2 100644
--- a/docs/architecture/foundations/aot-and-trimming.md
+++ b/docs/architecture/foundations/aot-and-trimming.md
@@ -156,8 +156,10 @@ dynamic、reflection、Loose AXAML 和插件输入触发 Package fallback。
| 静态 Control/AXAML 可确定 | 选择对应 Unit closure |
| 默认 Package 粒度 | 选择完整 Package Unit |
| Unit cycle | SCC 全选,每个 leaf fragment 调用一次 |
-| dynamic/reflection/Loose AXAML | 对应 Package full registrar |
+| Loose AXAML / 动态主题 | 对应 Package full registrar |
+| 无法静态解析的 C# 动态创建(`Activator.CreateInstance(Type)` 等) | 不扩大保留范围,报告 `ATOMUILINK010` 警告,由显式 root 覆盖 |
| Sidecar 缺失、陈旧或无法验证 | 对应 Package full registrar |
+| ProjectReference Sidecar 由 consumer 从普通构建的 assembly metadata 提取(`ExtractedManifest`) | 对应 Package full registrar,不产生诊断 |
| 分析预算超限 | 对应 Package full registrar |
| 未知 protocol major | 构建 Error |
| Fragment symbol 不存在 | 构建 Error |
@@ -236,6 +238,7 @@ ordinary Generator、linked-publish Generator、Build Tasks、Sidecar、PDB 和
| `ATOMUILINK007` | Loose AXAML 或动态主题导致 Package fallback | Warning |
| `ATOMUILINK008` | 检测到 Package 使用但缺少 registration entry | Error |
| `ATOMUILINK009` | Registration entry Attribute 或签名无效 | Error |
+| `ATOMUILINK010` | C# 动态创建无法静态解析,不扩大保留范围,需显式 root 覆盖 | Warning |
诊断必须包含 Package/Unit identity、稳定 reason 和可定位输入,不能只写“可能不兼容 AOT”。Fallback Warning 只在 linked publish
或显式 strict 验证中产生;strict 模式可以提升为 Error,但不改变保留范围。
diff --git a/docs/architecture/foundations/aot-linked-registration-pipeline.md b/docs/architecture/foundations/aot-linked-registration-pipeline.md
index 98cffb450..c18597055 100644
--- a/docs/architecture/foundations/aot-linked-registration-pipeline.md
+++ b/docs/architecture/foundations/aot-linked-registration-pipeline.md
@@ -110,6 +110,12 @@ Sidecar 是纯编译资产:
- Package 自己的 `.targets` 只在 linked build 中把它加入 `AdditionalFiles`。
- linked ProjectReference 在程序集复制到 `TargetPath` 后生成 `.atomui-link.json`;消费项目从已解析
`ReferencePath` 的 companion 路径收集它,不递归调用 ProjectReference target。
+- companion Sidecar 缺失时(引用库是普通构建,例如 `PublishAot`/`PublishTrimmed` 只在应用项目局部设置),消费项目直接从
+ 引用 assembly 的 metadata 记录提取 Sidecar 到自身 `obj`。普通库构建始终包含 Package/Unit/ControlMap/Axaml UnitEdge
+ 记录,但 C# UnitEdge 只由 linked 库构建计算,所以提取的 Sidecar 对每个 Package 附加 `ExtractedManifest` fallback:
+ Application Plan 对该 Package 保持 full fallback,且不产生诊断。publish 时以全局属性传入
+ `-p:PublishAot=true` / `-p:PublishTrimmed=true` / `-p:AtomUILinkedPublish=true`,引用库即自行产出完整 Sidecar,
+ 提取路径自动旁路。
- `CopyToOutputDirectory`、`CopyToPublishDirectory` 均为 `Never`。
- 不作为 EmbeddedResource,不进入运行时程序集、应用输出或 publish 目录。
@@ -259,7 +265,8 @@ Sidecar 不进入运行时。`UseXxxControls()` 仍拥有 Package Core、Provide
| --- | --- |
| 精确静态 usage | 选择 Unit closure |
| 默认 Package 粒度 | 选择单一完整 Package Unit |
-| dynamic/reflection/loose AXAML | 当前 Package full registrar |
+| loose AXAML / 动态主题 | 当前 Package full registrar |
+| 无法静态解析的 C# 动态创建 | 不扩大保留范围,报告 `ATOMUILINK010` 警告,由显式 root 覆盖 |
| sidecar 缺失、陈旧或无法绑定 | 当前相关 Package full registrar |
| 未知 protocol major | 构建 Error |
| fragment symbol 不存在 | 构建 Error |
diff --git a/docs/engineering/development/aot-programming-guidelines.md b/docs/engineering/development/aot-programming-guidelines.md
index 26d33412b..f49d4b7dd 100644
--- a/docs/engineering/development/aot-programming-guidelines.md
+++ b/docs/engineering/development/aot-programming-guidelines.md
@@ -292,9 +292,14 @@ Package、Unit、ControlMap 或 full fragment metadata。
Attribute,不要为了让 Common 参与应用计划而复制 `UseDesktopControls()` 的方法 identity,也不要引入跨 Package Unit 闭包。
类库只在被 linked 应用作为 ProjectReference 构建或执行 NuGet Pack 时生成 Usage Sidecar。普通 Debug/Release 必须跳过 usage
-分析和 Sidecar 生成。缺失或无法验证的 Sidecar 不能解释为没有 usage,只能让相关 Package full fallback。
-`ATOMUILINK002` 和 `ATOMUILINK007` 只在 linked publish 或显式 `AtomUIRegistrationStrict=true` 验证中显示;strict 模式用于
-CI 把自动 full fallback 提升为 error。
+分析和 Sidecar 生成。应用侧编译时如果 ProjectReference 旁路没有 Sidecar,consumer 直接从引用 assembly 的 metadata 记录提取
+(普通库构建始终包含 Package/Unit/ControlMap/Axaml UnitEdge 记录),用于诊断与入口校验;提取的 Sidecar 带
+`ExtractedManifest` fallback 标记,相关 Package 在 Application Plan 中保持 full fallback,不产生诊断。要获得 Unit 级裁剪,
+publish 时以全局属性传入 `-p:PublishAot=true` / `-p:PublishTrimmed=true`(命令行全局属性沿 ProjectReference 流动,引用库
+自行启用 linked 构建并产出含 C# UnitEdge 的完整 Sidecar);如果应用在项目文件里局部设置这些属性,则 publish 时补传
+`-p:AtomUILinkedPublish=true`。缺失或无法验证的 Sidecar 不能解释为没有 usage,只能让相关 Package full fallback。
+`ATOMUILINK002`、`ATOMUILINK007` 和 `ATOMUILINK010` 只在 linked publish 或显式 `AtomUIRegistrationStrict=true` 验证中显示;strict 模式用于
+CI 把自动 full fallback 或未覆盖的动态创建提升为 error。`ATOMUILINK010` 不触发 full fallback,只提示用显式 root 覆盖。
修改 Generator ABI、Sidecar schema、feature switch 或 Public fragment entry point 时,按 Public API 和协议 review,并运行
普通构建零 linked-analysis、trimmed JIT、NativeAOT 和非裁剪兼容验证。系统契约见
diff --git a/docs/modules/generator/linked-registration.md b/docs/modules/generator/linked-registration.md
index 349b1b1fe..6a9de41b8 100644
--- a/docs/modules/generator/linked-registration.md
+++ b/docs/modules/generator/linked-registration.md
@@ -161,7 +161,7 @@ writer 不生成 `AddDependencies`,不调用其他 Unit,也不调用 `TryEnt
诊断必须使用稳定 reason code,并包含 Package 和可用 source identity。至少区分:
- unresolved owner。
-- dynamic/reflection usage。
+- dynamic/reflection usage(不扩大保留范围,报告 `ATOMUILINK010`,由显式 root 覆盖)。
- loose or unresolved AXAML。
- missing/stale sidecar。
- unknown protocol major。
@@ -214,7 +214,7 @@ Package 与 entry 信息。SCC 发现和组件依赖排序必须使用显式栈
- Debug/普通 Release 的 `Csc` Analyzer item 不含 linked analyzer。
- linked AXAML target 在普通构建 skipped。
-- ProjectReference sidecar 只在 linked build 传递。
+- ProjectReference sidecar 只在 linked build 传递;companion 缺失时 consumer 从引用 assembly metadata 提取,提取结果带 `ExtractedManifest` fallback(full fallback、无诊断)。
- NuGet sidecar/consumer target 自动打包且不进入 runtime assets。
- sidecar 缺失、损坏或无法验证时只触发 Package fallback。
diff --git a/docs/reference/aot/linked-registration-sidecar.md b/docs/reference/aot/linked-registration-sidecar.md
index fca7ef2ca..f96267072 100644
--- a/docs/reference/aot/linked-registration-sidecar.md
+++ b/docs/reference/aot/linked-registration-sidecar.md
@@ -9,6 +9,7 @@
- 内容:单个 JSON object。
- NuGet 位置:`buildTransitive/AtomUI.LinkedRegistration/`。
- ProjectReference 位置:项目 `obj` 输出,由 MSBuild target item 传递。
+- ProjectReference 旁路缺失时,consumer 直接从引用 assembly 的 metadata 记录提取 Sidecar 到自身 `obj`(见 `ExtractedManifest`)。
- 不作为 `EmbeddedResource`、`Content`、runtime asset 或 publish asset。
Producer 必须使用结构化 JSON writer。Consumer 必须使用结构化 parser;不得用正则或字符串切割解析。
@@ -124,8 +125,16 @@ Fallback record 包含:
- `MissingSidecar`
- `StaleSidecar`
- `AnalysisBudgetExceeded`
+- `ExtractedManifest`
-Fallback 只能扩大保留范围。Consumer 不得忽略未知必需 reason,也不得把 fallback 降级为 Exact。
+Fallback 只能扩大保留范围。Consumer 不得忽略未知必需 reason,也不得把 fallback 降级为 Exact。例外有两个:
+
+- `DynamicInvocation`:提示性记录,Consumer 不扩大保留范围,只报告 `ATOMUILINK010` 警告,由显式
+ `AtomUIRegistrationUnitRoot` / `AtomUIPackageRoot` 覆盖。
+- `ExtractedManifest`:consumer 从普通构建的 ProjectReference assembly metadata 提取的 Sidecar 标记。
+ Package/Unit 清单完整,但 C# UnitEdge 只由 linked 库构建计算,所以 Consumer 必须对相关 Package 扩大为
+ full fallback,且不产生任何诊断。要获得完整裁剪,发布时以全局属性传入 `-p:PublishAot=true` /
+ `-p:PublishTrimmed=true` / `-p:AtomUILinkedPublish=true`,让引用库自行产出完整 Sidecar。
## 确定性
diff --git a/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs b/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs
index 7ad3fc2a2..219e327a7 100644
--- a/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs
+++ b/src/AtomUI.Build.Tasks/GenerateLinkedRegistrationSidecarTask.cs
@@ -21,6 +21,14 @@ public sealed class GenerateLinkedRegistrationSidecarTask : ITask
public string TargetFramework { get; set; } = string.Empty;
+ ///
+ /// When true, the Sidecar is extracted by a consumer from a normally-built referenced
+ /// assembly. C# unit edges are only emitted by linked library builds, so each declared
+ /// package gets an ExtractedManifest fallback marker: the plan widens to full fallback
+ /// instead of trimming with incomplete evidence.
+ ///
+ public bool ExtractedFallback { get; set; }
+
[Output]
public string SidecarPath { get; private set; } = string.Empty;
@@ -67,6 +75,19 @@ public sealed class GenerateLinkedRegistrationSidecarTask : ITask
return LogError(error);
}
sidecar.Assembly.TargetFramework = TargetFramework.Trim();
+ if (ExtractedFallback && sidecar.Packages.Length != 0)
+ {
+ sidecar.Fallbacks = sidecar.Fallbacks
+ .Concat(sidecar.Packages.Select(static package => new LinkedSidecarFallback
+ {
+ PackageId = package.Id,
+ Reason = LinkedRegistrationProtocol.FallbackReasonExtractedManifest,
+ Source = "",
+ Line = 0,
+ Column = 0
+ }))
+ .ToArray();
+ }
var bytes = LinkedRegistrationSidecarCodec.Write(sidecar);
if (analysisBudgetExceeded ||
bytes.Length > LinkedRegistrationAnalysisBudget.MaxSidecarBytes)
diff --git a/src/AtomUI.Core/AppBuilderExtensions.cs b/src/AtomUI.Core/AppBuilderExtensions.cs
index d4191b85a..d80f16ac7 100644
--- a/src/AtomUI.Core/AppBuilderExtensions.cs
+++ b/src/AtomUI.Core/AppBuilderExtensions.cs
@@ -48,8 +48,11 @@ public static class AppBuilderExtensions
///
public static AppBuilder WithAtomUIDefaultOptions(this AppBuilder appBuilder)
{
- return appBuilder
- .With(new AvaloniaNativePlatformOptions
+ // 按目标平台分支设置平台选项。NativeAOT 在 RID 发布时会把 OperatingSystem.IsXxx()
+ // 折叠为常量,非目标平台的分支及其引用的平台选项类型会被整体裁剪。
+ if (OperatingSystem.IsMacOS())
+ {
+ appBuilder = appBuilder.With(new AvaloniaNativePlatformOptions
{
RenderingMode =
[
@@ -57,8 +60,11 @@ public static class AppBuilderExtensions
AvaloniaNativeRenderingMode.Metal,
AvaloniaNativeRenderingMode.Software
]
- })
- .With(new Win32PlatformOptions
+ });
+ }
+ if (OperatingSystem.IsWindows())
+ {
+ appBuilder = appBuilder.With(new Win32PlatformOptions
{
RenderingMode =
[
@@ -66,11 +72,16 @@ public static class AppBuilderExtensions
Win32RenderingMode.Software
],
CompositionMode = [Win32CompositionMode.RedirectionSurface]
- })
- .With(new X11PlatformOptions
+ });
+ }
+ if (OperatingSystem.IsLinux())
+ {
+ appBuilder = appBuilder.With(new X11PlatformOptions
{
EnableDrawnDecorations = true
- })
+ });
+ }
+ return appBuilder
.With(new FontManagerOptions
{
FontFallbacks = [new FontFallback
diff --git a/src/AtomUI.Core/AtomUI.Core.csproj b/src/AtomUI.Core/AtomUI.Core.csproj
index f56d3eb14..b2f2d23ea 100644
--- a/src/AtomUI.Core/AtomUI.Core.csproj
+++ b/src/AtomUI.Core/AtomUI.Core.csproj
@@ -1,4 +1,4 @@
-
+
$(AtomUITargetFrameworks)
AtomUI
@@ -26,7 +26,6 @@
-
diff --git a/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs b/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs
index 6b4aedd92..53b5ca011 100644
--- a/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs
+++ b/src/AtomUI.Generator.LinkedPublish/LinkedRegistrationPackageManifestGenerator.cs
@@ -86,7 +86,7 @@ public sealed class LinkedRegistrationPackageManifestGenerator : IIncrementalGen
{
fallbacks.Add(new Fallback(
packageId,
- "DynamicInvocation",
+ LinkedRegistrationProtocol.FallbackReasonDynamicInvocation,
NormalizeSourcePath(candidate.Source, projectDirectory),
candidate.Line,
candidate.Column,
@@ -376,7 +376,12 @@ public sealed class LinkedRegistrationPackageManifestGenerator : IIncrementalGen
AnalyzerConfigOptionsProvider optionsProvider,
Fallback fallback)
{
- var descriptor = AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened;
+ var descriptor = string.Equals(
+ fallback.Reason,
+ LinkedRegistrationProtocol.FallbackReasonDynamicInvocation,
+ StringComparison.Ordinal)
+ ? AtomUIDiagnosticDescriptors.LinkedDynamicUsageUncovered
+ : AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened;
if (LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider))
{
descriptor = new DiagnosticDescriptor(
diff --git a/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticDescriptors.cs b/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticDescriptors.cs
index f9a090fb2..00a8c4eea 100644
--- a/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticDescriptors.cs
+++ b/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticDescriptors.cs
@@ -86,6 +86,15 @@ internal static class AtomUIDiagnosticDescriptors
isEnabledByDefault: true,
customTags: [WellKnownDiagnosticTags.Telemetry]);
+ public static readonly DiagnosticDescriptor LinkedDynamicUsageUncovered = new(
+ AtomUIDiagnosticIds.LinkedDynamicUsageUncovered,
+ "Dynamic AtomUI usage is not covered by the static registration plan",
+ "AtomUI dynamic usage '{0}' cannot be resolved statically; controls of Package '{1}' created only through this site are not registered. Add an AtomUIRegistrationUnitRoot when the Unit is known, or an AtomUIPackageRoot for fully dynamic usage; no action is required when this site never creates AtomUI controls.",
+ AtomUIDiagnosticCategories.LinkedRegistration,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ customTags: [WellKnownDiagnosticTags.Telemetry]);
+
public static readonly DiagnosticDescriptor AotMissingGeneratedAccessor = new(
AtomUIDiagnosticIds.AotMissingGeneratedAccessor,
"AOT-sensitive data member path requires generated accessor",
diff --git a/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticIds.cs b/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticIds.cs
index a7854e98a..88d2b8aa9 100644
--- a/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticIds.cs
+++ b/src/AtomUI.Generator/Diagnostics/AtomUIDiagnosticIds.cs
@@ -40,4 +40,5 @@ internal static class AtomUIDiagnosticIds
public const string LinkedLooseAxamlWidened = "ATOMUILINK007";
public const string LinkedPackageEntryMissing = "ATOMUILINK008";
public const string LinkedPackageEntryInvalid = "ATOMUILINK009";
+ public const string LinkedDynamicUsageUncovered = "ATOMUILINK010";
}
diff --git a/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs b/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs
index 902771b4c..f80f96e0c 100644
--- a/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs
+++ b/src/AtomUI.Generator/LinkedRegistration/ApplicationRegistrationPlanGenerator.cs
@@ -117,8 +117,16 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator
usage.Kind == LinkedUsageKind.PackageRoot &&
string.Equals(usage.Identity, packageId, StringComparison.Ordinal)) ||
catalog.Fallbacks.Any(fallback =>
- fallback.PackageId.Length == 0 ||
- string.Equals(fallback.PackageId, packageId, StringComparison.Ordinal));
+ // Unresolvable C# dynamic creation (DynamicInvocation) is reported as
+ // guidance (ATOMUILINK010) but never widens the plan: application-declared
+ // controls are already covered by base-type evidence, and purely
+ // string-driven creation is covered by explicit Unit/Package roots.
+ !string.Equals(
+ fallback.Reason,
+ LinkedRegistrationProtocol.FallbackReasonDynamicInvocation,
+ StringComparison.Ordinal) &&
+ (fallback.PackageId.Length == 0 ||
+ string.Equals(fallback.PackageId, packageId, StringComparison.Ordinal)));
if (isLegacy)
{
if (ValidateFragmentMethod(
@@ -231,12 +239,29 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator
.ThenBy(static item => item.Column)
.ThenBy(static item => item.Reason, StringComparer.Ordinal))
{
+ // ExtractedManifest marks a consumer-extracted ProjectReference Sidecar: the plan
+ // already widened to full fallback, and the delivery mode is not a code problem.
+ if (string.Equals(
+ fallback.Reason,
+ LinkedRegistrationProtocol.FallbackReasonExtractedManifest,
+ StringComparison.Ordinal))
+ {
+ continue;
+ }
if (fallback.PackageId.Length != 0 && !invokedPackageSet.Contains(fallback.PackageId))
{
continue;
}
+ var baseDescriptor = string.Equals(
+ fallback.Reason,
+ LinkedRegistrationProtocol.FallbackReasonDynamicInvocation,
+ StringComparison.Ordinal)
+ ? AtomUIDiagnosticDescriptors.LinkedDynamicUsageUncovered
+ : AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened;
context.ReportDiagnostic(Diagnostic.Create(
- GetFallbackDescriptor(LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider)),
+ GetFallbackDescriptor(
+ LinkedRegistrationOptions.IsRegistrationStrict(optionsProvider),
+ baseDescriptor),
Location.None,
fallback.Reason,
fallback.PackageId.Length == 0 ? "" : fallback.PackageId));
@@ -379,13 +404,12 @@ public sealed class ApplicationRegistrationPlanGenerator : IIncrementalGenerator
descriptor.CustomTags.ToArray());
}
- private static DiagnosticDescriptor GetFallbackDescriptor(bool strict)
+ private static DiagnosticDescriptor GetFallbackDescriptor(bool strict, DiagnosticDescriptor descriptor)
{
if (!strict)
{
- return AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened;
+ return descriptor;
}
- var descriptor = AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened;
return new DiagnosticDescriptor(
descriptor.Id,
descriptor.Title,
diff --git a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs
index 82c5db5d5..572d8b5f4 100644
--- a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs
+++ b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationProtocol.cs
@@ -12,4 +12,13 @@ internal static class LinkedRegistrationProtocol
internal const string UsageManifestKey = MetadataPrefix + "Usage.v1";
internal const string FallbackManifestKey = MetadataPrefix + "Fallback.v1";
internal const string PlanMarkerKey = MetadataPrefix + "Plan.v1";
+
+ internal const string FallbackReasonAnalysisBudgetExceeded = "AnalysisBudgetExceeded";
+ internal const string FallbackReasonDynamicInvocation = "DynamicInvocation";
+ internal const string FallbackReasonUnresolvedOwner = "UnresolvedOwner";
+ // Sidecar extracted by the consumer from assembly metadata of a normally-built
+ // ProjectReference: package/unit manifests are complete, but C# unit edges are only
+ // computed by linked library builds. The plan must widen to full fallback for these
+ // packages, and the reason is never surfaced as a diagnostic.
+ internal const string FallbackReasonExtractedManifest = "ExtractedManifest";
}
diff --git a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs
index 87829e279..c3c4b7951 100644
--- a/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs
+++ b/src/AtomUI.Generator/LinkedRegistration/LinkedRegistrationUsageGenerator.cs
@@ -387,14 +387,15 @@ public sealed class LinkedRegistrationUsageGenerator : IIncrementalGenerator
static packageId => packageId,
StringComparer.Ordinal))
{
- AddPackageFallback(
- packageId,
- dynamicUse.Source,
- dynamicUse.Line,
- dynamicUse.Column,
+ // C# dynamic creation sites no longer widen the package: controls declared
+ // in the application are already covered by base-type evidence, and purely
+ // string-driven creation is covered by explicit Unit/Package roots. Report
+ // guidance only.
+ ReportFallback(
+ AtomUIDiagnosticDescriptors.LinkedDynamicUsageUncovered,
dynamicUse.Location,
- AtomUIDiagnosticDescriptors.LinkedDynamicUsageWidened,
- dynamicUse.Identity);
+ dynamicUse.Identity,
+ packageId);
}
}
diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs
index ace8fa6e2..719918a9c 100644
--- a/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs
+++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/ApplicationRegistrationPlanGeneratorTests.cs
@@ -170,6 +170,62 @@ public sealed class ApplicationRegistrationPlanGeneratorTests
result.PlanSource.ShouldBeNull();
}
+ [Fact]
+ public void Dynamic_Invocation_Fallback_Warns_Without_Widening_The_Package()
+ {
+ var desktop = PlanGeneratorTestHost.StandardDesktopPackage();
+ var usage = PlanGeneratorTestHost.Usage(
+ new LinkedUsageManifestRecord(LinkedUsageKind.Control, "AtomUI.Desktop.Controls.Button", "View.axaml", 3, 4));
+ var dynamicFallback = UsageGeneratorTestHost.CreateManifestReference(
+ "Consumer.Usage.DynamicFallback",
+ new LinkedFallbackManifestRecord(
+ "AtomUI.Desktop.Controls",
+ LinkedRegistrationProtocol.FallbackReasonDynamicInvocation,
+ "ViewLocator.cs",
+ 27,
+ 29));
+
+ var result = PlanGeneratorTestHost.Run(
+ "AtomUI.Desktop.Entry.UseDesktopControls();",
+ [desktop, usage, dynamicFallback]);
+ var planSource = result.PlanSource;
+ planSource.ShouldNotBeNull();
+
+ planSource!.ShouldContain("global::GeneratedButtonUnitFragment.Add(packageBuilder);");
+ planSource.ShouldNotContain("global::GeneratedDatePickerUnitFragment.Add(packageBuilder);");
+ planSource.ShouldNotContain("global::GeneratedDesktopFullFragment.Register(");
+ result.Diagnostics.ShouldContain(static diagnostic =>
+ diagnostic.Id == "ATOMUILINK010" && diagnostic.Severity == DiagnosticSeverity.Warning);
+ }
+
+ [Fact]
+ public void Extracted_Manifest_Fallback_Widens_Without_A_Diagnostic()
+ {
+ var desktop = PlanGeneratorTestHost.StandardDesktopPackage();
+ var usage = PlanGeneratorTestHost.Usage(
+ new LinkedUsageManifestRecord(LinkedUsageKind.Control, "AtomUI.Desktop.Controls.Button", "View.axaml", 3, 4));
+ var extractedFallback = UsageGeneratorTestHost.CreateManifestReference(
+ "Consumer.ExtractedSidecar",
+ new LinkedFallbackManifestRecord(
+ "AtomUI.Desktop.Controls",
+ LinkedRegistrationProtocol.FallbackReasonExtractedManifest,
+ "",
+ 0,
+ 0));
+
+ var result = PlanGeneratorTestHost.Run(
+ "AtomUI.Desktop.Entry.UseDesktopControls();",
+ [desktop, usage, extractedFallback]);
+ var planSource = result.PlanSource.ShouldNotBeNull();
+
+ planSource.ShouldContain("global::GeneratedDesktopFullFragment.Register(");
+ planSource.ShouldNotContain("global::GeneratedButtonUnitFragment.Add(packageBuilder);");
+ result.Diagnostics.ShouldNotContain(static diagnostic =>
+ diagnostic.Id == "ATOMUILINK002" ||
+ diagnostic.Id == "ATOMUILINK007" ||
+ diagnostic.Id == "ATOMUILINK010");
+ }
+
[Fact]
public void Explicit_Unit_And_Package_Roots_Select_Precise_And_Full_Plans()
{
diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs
index e784dbe48..efc1e0c11 100644
--- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs
+++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationBuildAssetsTests.cs
@@ -252,6 +252,30 @@ public sealed class LinkedRegistrationBuildAssetsTests
condition.ShouldContain("'$(AtomUIRegistrationPackageId)' != ''");
}
+ [Fact]
+ public void Pack_Sidecar_Inner_Build_Does_Not_Inherit_Publish_Or_NoBuild_Globals()
+ {
+ // dotnet pack --no-build exports NoBuild=true as a global property; if the inner
+ // MSBuild call that emits the linked sidecar inherits it, the SDK fails the pack
+ // with NETSDK1085 and registration packages silently lose their sidecar assets.
+ var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets"));
+ var target = targets.Descendants("Target")
+ .Single(element =>
+ (string?)element.Attribute("Name") ==
+ "AtomUIPrepareLinkedRegistrationSidecarForPack");
+
+ var removeProperties = (string?)target.Descendants("MSBuild")
+ .Single()
+ .Attribute("RemoveProperties");
+ removeProperties.ShouldNotBeNull();
+ var removed = removeProperties.Split(';', StringSplitOptions.RemoveEmptyEntries |
+ StringSplitOptions.TrimEntries);
+ removed.ShouldContain("NoBuild");
+ removed.ShouldContain("PublishAot");
+ removed.ShouldContain("PublishTrimmed");
+ removed.ShouldContain("RunAOTCompilation");
+ }
+
[Fact]
public void ProjectReference_Sidecars_Are_Collected_From_Resolved_Assembly_Paths()
{
@@ -289,6 +313,30 @@ public sealed class LinkedRegistrationBuildAssetsTests
.ShouldBe("Build;GetAtomUILinkedRegistrationSidecar");
}
+ [Fact]
+ public void Missing_ProjectReference_Sidecars_Are_Extracted_With_A_Fallback_Marker()
+ {
+ var targets = XDocument.Load(GetRepoFile("build/AtomUI.LinkedRegistration.targets"));
+ var collectTarget = targets.Descendants("Target")
+ .Single(element =>
+ (string?)element.Attribute("Name") ==
+ "CollectAtomUIProjectReferenceSidecars");
+
+ var extractable = collectTarget.Descendants("_AtomUILinkedExtractableReference")
+ .ShouldHaveSingleItem();
+ ((string?)extractable.Attribute("Include")).ShouldBe("@(ReferencePath)");
+ ((string?)extractable.Attribute("Condition")).ShouldNotBeNull()
+ .ShouldContain("StartsWith('AtomUI.'");
+
+ var extraction = collectTarget.Descendants("AtomUI.Build.Tasks.GenerateLinkedRegistrationSidecarTask")
+ .ShouldHaveSingleItem();
+ ((string?)extraction.Attribute("ExtractedFallback")).ShouldBe("true");
+ ((string?)extraction.Attribute("ContinueOnError")).ShouldBe("WarnAndContinue");
+ var outputPath = (string?)extraction.Attribute("OutputPath");
+ outputPath.ShouldNotBeNull();
+ outputPath.ShouldContain("AtomUIExtractedSidecars");
+ }
+
[Fact]
public void Sidecars_Are_Emitted_After_The_Target_Assembly_Reaches_TargetPath()
{
diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationDiagnosticContractTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationDiagnosticContractTests.cs
index fde73f3b6..a02e53c1b 100644
--- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationDiagnosticContractTests.cs
+++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationDiagnosticContractTests.cs
@@ -22,9 +22,10 @@ public sealed class LinkedRegistrationDiagnosticContractTests
"ATOMUILINK006",
"ATOMUILINK007",
"ATOMUILINK008",
- "ATOMUILINK009"
+ "ATOMUILINK009",
+ "ATOMUILINK010"
]);
- descriptors.Select(static descriptor => descriptor.Id).Distinct().Count().ShouldBe(9);
+ descriptors.Select(static descriptor => descriptor.Id).Distinct().Count().ShouldBe(10);
}
[Fact]
@@ -41,6 +42,7 @@ public sealed class LinkedRegistrationDiagnosticContractTests
descriptors[6].DefaultSeverity.ShouldBe(DiagnosticSeverity.Warning);
descriptors[7].DefaultSeverity.ShouldBe(DiagnosticSeverity.Error);
descriptors[8].DefaultSeverity.ShouldBe(DiagnosticSeverity.Error);
+ descriptors[9].DefaultSeverity.ShouldBe(DiagnosticSeverity.Warning);
descriptors.ShouldAllBe(descriptor =>
descriptor.Category == AtomUIDiagnosticCategories.LinkedRegistration);
}
@@ -60,6 +62,8 @@ public sealed class LinkedRegistrationDiagnosticContractTests
messages[6].ShouldContain("AtomUIPackageRoot");
messages[7].ShouldContain("UseXxxControls");
messages[8].ShouldContain("registration entry");
+ messages[9].ShouldContain("AtomUIRegistrationUnitRoot");
+ messages[9].ShouldContain("AtomUIPackageRoot");
}
private static DiagnosticDescriptor[] GetDescriptors()
@@ -74,7 +78,8 @@ public sealed class LinkedRegistrationDiagnosticContractTests
AtomUIDiagnosticDescriptors.LinkedManifestVersionMismatch,
AtomUIDiagnosticDescriptors.LinkedLooseAxamlWidened,
AtomUIDiagnosticDescriptors.LinkedPackageEntryMissing,
- AtomUIDiagnosticDescriptors.LinkedPackageEntryInvalid
+ AtomUIDiagnosticDescriptors.LinkedPackageEntryInvalid,
+ AtomUIDiagnosticDescriptors.LinkedDynamicUsageUncovered
];
}
}
diff --git a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs
index e97cc902b..1cb903626 100644
--- a/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs
+++ b/tests/AtomUI.Generator.Tests/LinkedRegistration/LinkedRegistrationUsageGeneratorTests.cs
@@ -524,7 +524,7 @@ public sealed class LinkedRegistrationUsageGeneratorTests
}
[Fact]
- public void Unsupported_Dynamic_Use_Widens_Only_The_Affected_Package()
+ public void Unsupported_Dynamic_Use_Warns_Without_Widening()
{
var roots = UsageGeneratorTestHost.Axaml(
"obj/AtomUIAxamlUsage.xml",
@@ -535,10 +535,26 @@ public sealed class LinkedRegistrationUsageGeneratorTests
UsageGeneratorTestHost.LinkedEntryOptions,
additionalTexts: [roots]);
+ // The only PackageRoot comes from the explicit AtomUIPackageRoot declaration;
+ // the unresolvable dynamic creation site itself no longer widens the package.
result.Usages.Count(usage =>
usage.Kind == LinkedUsageKind.PackageRoot && usage.Identity == "Acme.Controls").ShouldBe(1);
result.Diagnostics.ShouldContain(diagnostic =>
- diagnostic.Id == "ATOMUILINK002" && diagnostic.Severity == DiagnosticSeverity.Warning);
+ diagnostic.Id == "ATOMUILINK010" && diagnostic.Severity == DiagnosticSeverity.Warning);
+ result.Diagnostics.ShouldNotContain(diagnostic => diagnostic.Id == "ATOMUILINK002");
+ }
+
+ [Fact]
+ public void Unsupported_Dynamic_Use_With_Invoked_Entry_Warns_Without_A_Package_Root()
+ {
+ var result = UsageGeneratorTestHost.Run(
+ ["using System; public sealed class Consumer { public static void Run() { Other.Controls.Entry.UseOtherControls(); } public object? Create(Type type) => Activator.CreateInstance(type); }"],
+ [UsageGeneratorTestHost.AcmePackage, UsageGeneratorTestHost.OtherPackage],
+ UsageGeneratorTestHost.LinkedEntryOptions);
+
+ result.Usages.ShouldNotContain(usage => usage.Kind == LinkedUsageKind.PackageRoot);
+ result.Diagnostics.ShouldContain(diagnostic =>
+ diagnostic.Id == "ATOMUILINK010" && diagnostic.Severity == DiagnosticSeverity.Warning);
}
[Fact]
@@ -662,7 +678,7 @@ public sealed class LinkedRegistrationUsageGeneratorTests
warning.Usages.ShouldBe(strict.Usages);
strict.Diagnostics.ShouldContain(diagnostic =>
- diagnostic.Id == "ATOMUILINK002" && diagnostic.Severity == DiagnosticSeverity.Error);
+ diagnostic.Id == "ATOMUILINK010" && diagnostic.Severity == DiagnosticSeverity.Error);
}
[Fact]
diff --git a/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs b/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs
index 365fef6c0..0f87d01ae 100644
--- a/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs
+++ b/tests/AtomUI.Generator.Tests/ThemeSchemaGeneratorTests.cs
@@ -459,7 +459,7 @@ public class ThemeSchemaGeneratorTests
}
[Fact]
- public void Directory_Granularity_Unresolved_Dynamic_Control_Creation_Forces_Package_Fallback()
+ public void Directory_Granularity_Unresolved_Dynamic_Control_Creation_Warns_Without_Widening()
{
var compilation = CreateCompilation(
"""
@@ -486,7 +486,7 @@ public class ThemeSchemaGeneratorTests
"Directory");
var diagnostic = diagnostics.ShouldHaveSingleItem();
- diagnostic.Id.ShouldBe("ATOMUILINK002");
+ diagnostic.Id.ShouldBe("ATOMUILINK010");
diagnostic.Location.GetLineSpan().Path.ShouldBe("Button/Button.cs");
var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs");
source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Fallback.v1\"");
@@ -494,7 +494,7 @@ public class ThemeSchemaGeneratorTests
}
[Fact]
- public void Directory_Granularity_Dynamic_Invocation_Forces_Package_Fallback()
+ public void Directory_Granularity_Dynamic_Invocation_Warns_Without_Widening()
{
var compilation = CreateCompilation(
"""
@@ -513,7 +513,7 @@ public class ThemeSchemaGeneratorTests
"Directory");
var diagnostic = diagnostics.ShouldHaveSingleItem();
- diagnostic.Id.ShouldBe("ATOMUILINK002");
+ diagnostic.Id.ShouldBe("ATOMUILINK010");
diagnostic.Location.GetLineSpan().Path.ShouldBe("Button/Button.cs");
var source = GetGeneratedSource(outputCompilation, "LinkedRegistrationPackageAnalysis.g.cs");
source.ShouldContain("AssemblyMetadata(\"AtomUI.Linked.Fallback.v1\"");