Skip to content

[tools] PreserveSmartEnumConversionsStep: emit root-descriptor XML instead of modifying (hot) reloadable assemblies. Fixes #26073 - #26087

Merged
rolfbjarne merged 14 commits into
mainfrom
dev/rolf/issue-26073-hot-reload-preservesmartenumconversions-e38dd9
Jul 22, 2026
Merged

rolfbjarne merged 14 commits into
mainfrom
dev/rolf/issue-26073-hot-reload-preservesmartenumconversions-e38dd9

Conversation

@rolfbjarne

Copy link
Copy Markdown
Member

PreserveSmartEnumConversionsStep was 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 via TrimmerRootDescriptor. 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-testable XmlDescriptor class (PreserveMethod/PreserveField/PreserveType*/CreateXml/Save) and used it from both steps (net ~174 lines removed across the two steps). XmlDescriptor.Save only rewrites the file when its contents change, so an unchanged descriptor doesn't needlessly invalidate incremental builds.

Tests

  • HotReloadCompatibleBuildTest in PreserveSmartEnumConversionsTest.cs: asserts no [DynamicDependency] attributes are injected and that the emitted XML preserves the framework-side GetConstant/GetValue methods.
  • 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

rolfbjarne and others added 2 commits July 14, 2026 16:17
…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>
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, PreserveSmartEnumConversionsStep emits a root-descriptor XML instead of injecting [DynamicDependency] into user assemblies, and MSBuild wires it via TrimmerRootDescriptor.
  • Refactor descriptor XML generation into XmlDescriptor and 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.

Comment thread tools/dotnet-linker/XmlDescriptor.cs Resolved
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>
@vs-mobiletools-engineering-service2

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>
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne
rolfbjarne marked this pull request as ready for review July 16, 2026 13:53
dalexsoto
dalexsoto previously approved these changes Jul 16, 2026
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne
rolfbjarne marked this pull request as draft July 20, 2026 11:06
auto-merge was automatically disabled July 20, 2026 11:06

Pull request was converted to draft

rolfbjarne and others added 2 commits July 20, 2026 14:27
* 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
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

rolfbjarne and others added 5 commits July 21, 2026 12:12
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
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

βœ… API diff for current PR / commit

NET (empty diffs)

βœ… API diff vs stable

NET (empty diffs)

ℹ️ Generator diff

Generator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes)

Pipeline on Agent
Hash: 7213ed42c7b53e51cf031251eebddbfdd7ef2ef8 [PR build]

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

πŸš€ [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
βœ… cecil: All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (iOS): All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (MacCatalyst): All 1 tests passed. [attempt 3] Html Report (VSDrops) Download
βœ… dotnettests (macOS): All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (Multiple platforms): All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (tvOS): All 1 tests passed. Html Report (VSDrops) Download
βœ… framework: All 2 tests passed. Html Report (VSDrops) Download
βœ… fsharp: All 4 tests passed. Html Report (VSDrops) Download
βœ… generator: All 5 tests passed. Html Report (VSDrops) Download
βœ… interdependent-binding-projects: All 4 tests passed. Html Report (VSDrops) Download
βœ… introspection: All 4 tests passed. Html Report (VSDrops) Download
βœ… linker (iOS): All 15 tests passed. [attempt 2] Html Report (VSDrops) Download
βœ… linker (MacCatalyst): All 15 tests passed. Html Report (VSDrops) Download
βœ… linker (macOS): All 21 tests passed. [attempt 4] Html Report (VSDrops) Download
βœ… linker (tvOS): All 15 tests passed. [attempt 2] Html Report (VSDrops) Download
βœ… monotouch (iOS): All 19 tests passed. Html Report (VSDrops) Download
βœ… monotouch (MacCatalyst): All 18 tests passed. Html Report (VSDrops) Download
βœ… monotouch (macOS): All 19 tests passed. Html Report (VSDrops) Download
βœ… monotouch (tvOS): All 19 tests passed. Html Report (VSDrops) Download
βœ… msbuild: All 2 tests passed. Html Report (VSDrops) Download
βœ… sharpie: All 1 tests passed. Html Report (VSDrops) Download
βœ… windows: All 3 tests passed. Html Report (VSDrops) Download
βœ… xcframework: All 4 tests passed. Html Report (VSDrops) Download
βœ… xtro: All 1 tests passed. Html Report (VSDrops) Download

macOS tests

βœ… Tests on macOS Monterey (12): All 5 tests passed. Html Report (VSDrops) Download
βœ… Tests on macOS Ventura (13): All 5 tests passed. Html Report (VSDrops) Download
βœ… Tests on macOS Sonoma (14): All 5 tests passed. Html Report (VSDrops) Download
βœ… Tests on macOS Sequoia (15): All 5 tests passed. Html Report (VSDrops) Download
βœ… Tests on macOS Tahoe (26): All 5 tests passed. Html Report (VSDrops) Download

Linux Build Verification

βœ… Linux build succeeded

Pipeline on Agent
Hash: 7213ed42c7b53e51cf031251eebddbfdd7ef2ef8 [PR build]

@rolfbjarne
rolfbjarne merged commit b3af478 into main Jul 22, 2026
56 checks passed
@rolfbjarne
rolfbjarne deleted the dev/rolf/issue-26073-hot-reload-preservesmartenumconversions-e38dd9 branch July 22, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Hot Reload] PreserveSmartEnumConversionsStep: emit root-descriptor XML instead of modifying user assemblies

4 participants