Repository navigation
[tools] PreserveSmartEnumConversionsStep: emit root-descriptor XML instead of modifying (hot) reloadable assemblies. Fixes #26073 - #26087
Conversation
β¦ML instead of modifying user assemblies PreserveSmartEnumConversionsStep was the only assembly-preparer step that modified user (reloadable) assemblies: when a [BindAs] smart enum's conversion methods (GetConstant/GetValue) live in a trimmed framework assembly, it injected [DynamicDependency] attributes into the referencing assembly. This breaks Hot Reload, which requires that user assemblies aren't modified. When the new $(HotReloadCompatibleBuild) property is enabled and we're running in the assembly-preparer, emit an ILLink root-descriptor XML that unconditionally preserves the framework-side conversion methods instead, and wire it into the trimmer via TrimmerRootDescriptor. The current attribute-injection behaviour is kept otherwise (release builds, and the trimmer pass), so app size is unchanged for non-Hot-Reload builds. Fixes #26073 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
β¦criptor class The XML root-descriptor generation was duplicated between PreserveSmartEnumConversionsStep (added for the Hot Reload fix) and the existing ApplyPreserveAttributeStep: both built the assembly -> type -> member model, computed method signatures, and serialized the ILLink descriptor XML. Extract that logic into a new, independently unit-testable XmlDescriptor class (PreserveMethod/PreserveField/PreserveType*/CreateXml/Save) and use it from both steps. XmlDescriptor.Save only rewrites the file when its contents actually change, so an unchanged descriptor doesn't needlessly invalidate incremental builds. Added unit tests for XmlDescriptor in the assembly-preparer test project. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The Debug + PrepareAssemblies test leg builds with the assembly-preparer and HotReloadCompatibleBuild=true (the default for Debug). In that configuration smart enum conversions are preserved via a root-descriptor XML instead of by injecting attributes into user assemblies. Root descriptors can't express conditional preservation, so unused smart enums (e.g. AVMediaTypes) are intentionally kept rather than trimmed. Add a PREPARE_ASSEMBLIES compile constant for the prepare-assemblies test variation and use it (together with DEBUG) to assert the smart enums are preserved in that configuration, while keeping the trim assertions for all other configurations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Updates the .NET linker/assembly-preparer pipeline to preserve [BindAs] smart-enum conversion methods without modifying user (Hot Reloadβreloadable) assemblies, by emitting an ILLink root-descriptor XML and wiring it into the trimmer inputs. This also refactors existing root-descriptor generation into a shared XmlDescriptor helper with unit coverage.
Changes:
- Add
$(HotReloadCompatibleBuild)MSBuild property (defaulting to debug state) and flow it into linker/assembly-preparer configuration. - When Hot Reload compatible + assembly-preparer is active,
PreserveSmartEnumConversionsStepemits a root-descriptor XML instead of injecting[DynamicDependency]into user assemblies, and MSBuild wires it viaTrimmerRootDescriptor. - Refactor descriptor XML generation into
XmlDescriptorand add unit tests for both the helper and the new Hot Reload behavior.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tools/dotnet-linker/XmlDescriptor.cs | New shared helper to build/save ILLink root-descriptor XML for preserved members. |
| tools/dotnet-linker/PreserveSmartEnumConversionsStep.cs | Adds Hot Reloadβcompatible path that collects conversion methods and emits a root-descriptor XML instead of modifying assemblies. |
| tools/dotnet-linker/LinkerConfiguration.cs | Adds config key flow for HotReloadCompatibleBuild into Application. |
| tools/dotnet-linker/ApplyPreserveAttributeStep.cs | Switches XML descriptor emission to use the shared XmlDescriptor helper. |
| tools/common/Application.cs | Introduces Application.HotReloadCompatibleBuild flag. |
| tools/assembly-preparer/assembly-preparer.csproj | Includes XmlDescriptor.cs in the assembly-preparer build. |
| tests/linker/link all/PreserveTest.cs | Adjust linker expectations in Debug+PrepareAssemblies to account for unconditional preservation via root-descriptor XML. |
| tests/common/test-variations.csproj | Defines PREPARE_ASSEMBLIES constant for the prepare-assemblies variation. |
| tests/assembly-preparer/XmlDescriptorTest.cs | Adds unit tests for XmlDescriptor generation and save semantics. |
| tests/assembly-preparer/PreserveSmartEnumConversionsTest.cs | Adds test ensuring no user-assembly modification and correct XML emission when HotReloadCompatibleBuild is enabled. |
| tests/assembly-preparer/BaseClass.cs | Allows passing extra config lines into the preparer test harness. |
| dotnet/targets/Xamarin.Shared.Sdk.targets | Defaults HotReloadCompatibleBuild, flows it to the preparer, and wires the emitted descriptor into TrimmerRootDescriptor. |
| docs/building-apps/build-properties.md | Documents HotReloadCompatibleBuild. |
Instead of using the DEBUG define as a proxy for the hot-reload-compatible build state, define a HOTRELOAD_COMPATIBLE_BUILD compile constant (in shared-dotnet.csproj, for all test projects) directly from the $(HotReloadCompatibleBuild) MSBuild property. This has to be done in a target because the property is computed in the platform SDK targets, which are imported after shared-dotnet.csproj. SmartEnumTest now asserts the unused smart enums are preserved when both HOTRELOAD_COMPATIBLE_BUILD and PREPARE_ASSEMBLIES are defined (the only configuration where the assembly-preparer emits the unconditional root-descriptor XML), and keeps the trim assertions otherwise. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
β¦plicit members CreateTypeElement only emitted preserve="fields" when the type had no explicit fields/methods, otherwise it fell back to preserve="nothing". That's reachable: in ApplyPreserveAttributeStep an enum can be preserved with its fields (via PreserveTypeFields) and also have an individual method preserved on it (via PreserveConditional) - which dropped preserve="fields" and stopped preserving the enum's fields. Always emit preserve="fields" when the type was registered via PreserveTypeFields and keep any explicit <field>/<method> elements as children. The trimmer applies the preserve attribute and then processes the child elements additively (see ILLink DescriptorMarker.ProcessType), so both are preserved. Added a regression test (PreserveTypeFields_WithMethod_KeepsPreserveFields). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
β¦tenumconversions-e38dd9
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Pull request was converted to draft
* assembly-preparer BaseClass.AssertPrepareCode dropped its extraConfig argument instead of forwarding it to CreatePreparer, so HotReloadCompatibleBuildTest never actually set HotReloadCompatibleBuild=true and the assembly-preparer injected [DynamicDependency] attributes (which the test asserts against) instead of emitting the root-descriptor XML. Forward extraConfig. * BundledResources ResourcesTest.Bundled asserted that bundled resources are always stripped on macOS/Mac Catalyst. In a hot-reload-compatible build (the default for Debug) resource stripping is skipped for reloadable user assemblies, so the resources remain embedded. Expect resources to be present when HOTRELOAD_COMPATIBLE_BUILD is defined. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c159d2ff-39c8-4cd3-bea9-882318a1d75e
β¦hot-reload-preservesmartenumconversions-e38dd9
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The previous fix assumed a hot-reload-compatible build always retains bundled resources, but resource stripping is only skipped for reloadable (non-linked) user assemblies. In "link all" the BundledResources assembly is linked, so its resources are still stripped, and the test failed there. Detect at runtime whether the BundledResources assembly was linked (via an unused LinkerSentinel type that survives trimming only when the assembly isn't linked, mirroring TestRuntime.IsLinkAll), and expect the resources to be retained only for a hot-reload-compatible build of a non-linked assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c159d2ff-39c8-4cd3-bea9-882318a1d75e
β¦se it in ResourcesTest The BundledResources ResourcesTest needs to know whether its own assembly was linked (in a hot-reload-compatible build resource stripping is skipped for reloadable/non-linked user assemblies, so bundled resources remain embedded unless the assembly was linked). TestRuntime.IsLinkAll already answers this, but the full TestRuntime.cs can't be compiled into BundledResources because it re-exports types (Xamarin.Utils.ApplePlatform) that clash with other referenced test assemblies in the "link all" app. Move the dependency-free linker-detection members (IsLinkAll, IsLinkAny and the LinkerSentinel helper) into a small partial file TestRuntime.LinkAll.cs that can be compiled on its own, include it alongside TestRuntime.cs in every consuming test project, and compile only it (not the full TestRuntime.cs) into BundledResources. ResourcesTest now uses TestRuntime.IsLinkAll instead of a local sentinel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c159d2ff-39c8-4cd3-bea9-882318a1d75e
On MacCatalyst multi-RID Debug builds, bindings-framework-test.dll/.pdb now
appear once at the top level of MonoBundle instead of in per-RID .xamarin/{rid}/
subfolders.
This is the intended effect of the hot-reload-compatible build (the default for
Debug): the assembly-preparer no longer modifies reloadable user assemblies.
Previously it injected smart-enum conversion [DynamicDependency] attributes into
bindings-framework-test, which made the assembly differ per RID (so it was placed
in per-RID subfolders). Now the assembly is identical across RIDs and gets
deduplicated into a single top-level copy, matching how MonoTouch.Dialog and
Touch.Client are already handled for MacCatalyst Debug builds.
Update the expectation to use a single-RID (top-level) placement for
bindings-framework-test on MacCatalyst Debug builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c159d2ff-39c8-4cd3-bea9-882318a1d75e
β¦hot-reload-preservesmartenumconversions-e38dd9
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
β API diff for current PR / commitNET (empty diffs)β API diff vs stableNET (empty diffs)βΉοΈ Generator diffGenerator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes) Pipeline on Agent |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
π [CI Build #7213ed4] Test results πTest resultsβ All tests passed on VSTS: test results. π All 203 tests passed π Tests countsβ
assembly-processing: All 1 tests passed. Html Report (VSDrops) Download macOS testsβ
Tests on macOS Monterey (12): All 5 tests passed. Html Report (VSDrops) Download Linux Build VerificationPipeline on Agent |
PreserveSmartEnumConversionsStepwas the only assembly-preparer step that modified user (hot reloadable) assemblies: when a[BindAs]smart enum's conversion methods (GetConstant/GetValue) live in a trimmed framework assembly, it injected[DynamicDependency]attributes into the referencing assembly. That breaks Hot Reload, which requires that user assemblies aren't modified.Fix
When the new
$(HotReloadCompatibleBuild)property is enabled and the step runs in the assembly-preparer, it no longer modifies the assembly. Instead it collects the framework-side conversion methods and emits an ILLink root-descriptor XML that preserves them unconditionally, and wires that descriptor into the trimmer viaTrimmerRootDescriptor. The existing attribute-injection behaviour is kept for all other cases (release builds and the trimmer pass), so there's no app-size impact on non-Hot-Reload builds.$(HotReloadCompatibleBuild)defaults to the debug state ($(_BundlerDebug)); since$(PrepareAssemblies)is opt-in, the new XML path only activates in the actual Hot Reload scenario.Because the dependency that introduces
$(HotReloadCompatibleBuild)(#26072) isn't merged yet, this PR includes a minimal, self-contained implementation of that property flow.Refactor
The XML root-descriptor generation was duplicated between this step and the existing
ApplyPreserveAttributeStep. Extracted it into a new, independently unit-testableXmlDescriptorclass (PreserveMethod/PreserveField/PreserveType*/CreateXml/Save) and used it from both steps (net ~174 lines removed across the two steps).XmlDescriptor.Saveonly rewrites the file when its contents change, so an unchanged descriptor doesn't needlessly invalidate incremental builds.Tests
HotReloadCompatibleBuildTestinPreserveSmartEnumConversionsTest.cs: asserts no[DynamicDependency]attributes are injected and that the emitted XML preserves the framework-sideGetConstant/GetValuemethods.XmlDescriptorTest.cs: 7 unit tests for the new class (unconditional/conditional methods, fields,preserve=all/fields, generic-method name fallback, save-only-when-changed).Fixes #26073
π€ Pull request created by Copilot