From 235a4732548e72edca4e53325bcd65b28d7dcf6e Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 6 Aug 2026 08:03:12 +0200 Subject: [PATCH 1/4] [Hot Reload] Add assembly modification safety net Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/building-apps/build-properties.md | 2 +- dotnet/targets/Xamarin.Shared.Sdk.targets | 3 ++- tools/dotnet-linker/AppBundleRewriter.cs | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/building-apps/build-properties.md b/docs/building-apps/build-properties.md index e6e264ecad6..7b6a6badd61 100644 --- a/docs/building-apps/build-properties.md +++ b/docs/building-apps/build-properties.md @@ -676,7 +676,7 @@ assemblies so they stay byte-for-byte unchanged (a requirement for Hot Reload). This will disable a few minor optimizations, but will otherwies not affect anything. -The default value is `true` for debug builds and `false` otherwise. +The default value is `true` for non-NativeAOT debug builds and `false` otherwise. ## IBToolPath diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index 78fce49117f..7cbc374dbb5 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -129,7 +129,8 @@ behaviour (and app size) for release builds, where the extra unconditional preservation of smart-enum conversion methods would otherwise cost a bit of app size. --> - $(_BundlerDebug) + $(_BundlerDebug) + false $(_BundlerDebug) diff --git a/tools/dotnet-linker/AppBundleRewriter.cs b/tools/dotnet-linker/AppBundleRewriter.cs index a6d545837ac..4e3c25ba4d2 100644 --- a/tools/dotnet-linker/AppBundleRewriter.cs +++ b/tools/dotnet-linker/AppBundleRewriter.cs @@ -1501,9 +1501,11 @@ void SaveAssembly (AssemblyDefinition assembly) { if (assembly != CurrentAssembly && assembly != PlatformAssembly) throw new InvalidOperationException ($"Can't save assembly {assembly.Name} because it's not the current assembly ({CurrentAssembly.Name}) or the platform assembly ({PlatformAssembly.Name})."); - AssemblySaved?.Invoke (assembly); var annotations = configuration.Context.Annotations; var action = annotations.GetAction (assembly); + if (configuration.HotReloadCompatibleBuild && action == AssemblyAction.Copy && assembly != PlatformAssembly) + throw new InvalidOperationException ($"The assembly '{assembly.Name.Name}' is reloadable, but was modified during a Hot Reload compatible build."); + AssemblySaved?.Invoke (assembly); if (action == AssemblyAction.Copy) { #if !ASSEMBLY_PREPARER // Preserve TypeForwardedTo which would the linker sweep otherwise From b9e7df2a3f6eff2bccb84a0fbd37c480762e86fc Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 6 Aug 2026 11:27:31 +0200 Subject: [PATCH 2/4] [Hot Reload] Report safety net failures as errors Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/dotnet-linker/AppBundleRewriter.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/dotnet-linker/AppBundleRewriter.cs b/tools/dotnet-linker/AppBundleRewriter.cs index 4e3c25ba4d2..c2ccd6fe06e 100644 --- a/tools/dotnet-linker/AppBundleRewriter.cs +++ b/tools/dotnet-linker/AppBundleRewriter.cs @@ -1503,8 +1503,10 @@ void SaveAssembly (AssemblyDefinition assembly) throw new InvalidOperationException ($"Can't save assembly {assembly.Name} because it's not the current assembly ({CurrentAssembly.Name}) or the platform assembly ({PlatformAssembly.Name})."); var annotations = configuration.Context.Annotations; var action = annotations.GetAction (assembly); - if (configuration.HotReloadCompatibleBuild && action == AssemblyAction.Copy && assembly != PlatformAssembly) - throw new InvalidOperationException ($"The assembly '{assembly.Name.Name}' is reloadable, but was modified during a Hot Reload compatible build."); + if (configuration.HotReloadCompatibleBuild && action == AssemblyAction.Copy && assembly != PlatformAssembly) { + configuration.Logger.LogError (ErrorHelper.CreateError (99, $"The assembly '{assembly.Name.Name}' is reloadable, but was modified during a Hot Reload compatible build.")); + return; + } AssemblySaved?.Invoke (assembly); if (action == AssemblyAction.Copy) { #if !ASSEMBLY_PREPARER From ea94722ac2db717352ec4279ab90dacccfd1f2fe Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 27 Aug 2026 19:30:25 +0200 Subject: [PATCH 3/4] [Hot Reload] Don't inject smart enum DynamicDependency attributes into reloadable assemblies PreserveSmartEnumConversionsStep injects [DynamicDependency] attributes into the assembly that references a smart enum. That assembly may be reloadable (i.e. not trimmed), in which case the injection breaks Hot Reload - and it now trips the newly added safety net (MT0099), which broke the 'link sdk' test build: ILLINK error MT0099: The assembly 'bindings-test' is reloadable, but was modified during a Hot Reload compatible build. Use the mark handler (PreserveSmartEnumConversionsHandler) instead when doing a Hot Reload compatible build without the assembly-preparer: it marks the conversion methods when the referencing method is marked, without modifying any assemblies (and without any app size cost). When the assembly-preparer is used, the step already emits a root descriptor xml file instead, so nothing changes there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/targets/Xamarin.Shared.Sdk.targets | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index 7cbc374dbb5..b816425b57b 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -666,6 +666,14 @@ <_UseDynamicDependenciesInsteadOfMarking Condition="'$(_UseDynamicDependenciesInsteadOfMarking)' == ''">true <_UseDynamicDependenciesForProtocolPreservation Condition="'$(_UseDynamicDependenciesForProtocolPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking) + + <_UseDynamicDependenciesForSmartEnumPreservation Condition="'$(_UseDynamicDependenciesForSmartEnumPreservation)' == '' And '$(HotReloadCompatibleBuild)' == 'true' And '$(PrepareAssemblies)' != 'true'">false <_UseDynamicDependenciesForSmartEnumPreservation Condition="'$(_UseDynamicDependenciesForSmartEnumPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking) <_UseDynamicDependenciesForBlockCodePreservation Condition="'$(_UseDynamicDependenciesForBlockCodePreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking) <_UseDynamicDependenciesForGeneratedCodeOptimizations Condition="'$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' == ''">$(_UseDynamicDependenciesInsteadOfMarking) From 2b61cafdb895e3d6d344ed913b3b197ce35df336 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 28 Aug 2026 10:15:32 +0200 Subject: [PATCH 4/4] [Hot Reload] Don't do a Hot Reload compatible build with the managed static registrar The managed static registrar emits registrar code into every user assembly, which breaks Hot Reload (and trips the recently added safety net, which broke the 'dont link' test build): MSBuild : error MT99: The assembly 'dont link' is reloadable, but was modified during a Hot Reload compatible build. Hot Reload can't work in that case at all (unlike with the trimmable static registrar, which emits this code into separate companion assemblies), so default $(HotReloadCompatibleBuild) to false when the managed static registrar is used (a user-specified value still wins, as before). This means the property can't be computed at evaluation time anymore, because the registrar isn't known until the SelectRegistrar target has run, so compute it in a new _ComputeHotReloadCompatibleBuild target instead, and make the consumers depend on that target: * _ComputeLinkerInputs in the SDK (it computes $(_UseDynamicDependenciesForSmartEnumPreservation) and the linker's HotReloadCompatibleBuild option from it). * _AddHotReloadCompatibleBuildDefine in the tests (it defines HOTRELOAD_COMPATIBLE_BUILD). The condition had to be moved from the target to the property group, because a target's condition is evaluated before its dependencies are built. Additionally, SelectRegistrar only runs for projects that produce an app bundle, so the referenced test library projects would keep the (potentially different) default, even though it's the app's value that decides whether their assemblies are modified. So pass the app's value on to every referenced project. Also update the documentation for $(HotReloadCompatibleBuild) accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/building-apps/build-properties.md | 4 +++- dotnet/targets/Xamarin.Shared.Sdk.targets | 25 ++++++++++++++--------- tests/common/shared-dotnet.csproj | 17 +++++++++++++-- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/docs/building-apps/build-properties.md b/docs/building-apps/build-properties.md index 7b6a6badd61..22f1b29f4fb 100644 --- a/docs/building-apps/build-properties.md +++ b/docs/building-apps/build-properties.md @@ -676,7 +676,9 @@ assemblies so they stay byte-for-byte unchanged (a requirement for Hot Reload). This will disable a few minor optimizations, but will otherwies not affect anything. -The default value is `true` for non-NativeAOT debug builds and `false` otherwise. +The default value is `true` for non-NativeAOT debug builds that don't use the +managed static registrar, and `false` otherwise (the managed static registrar +modifies user assemblies, which is incompatible with Hot Reload). ## IBToolPath diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets index b816425b57b..9a6452cf8ff 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.targets +++ b/dotnet/targets/Xamarin.Shared.Sdk.targets @@ -123,15 +123,6 @@ <_BundlerDebug Condition="'$(_BundlerDebug)' == ''">false - - $(_BundlerDebug) - false - $(_BundlerDebug) @@ -652,7 +643,21 @@ - + + + + false + $(_BundlerDebug) + false + + + + diff --git a/tests/common/shared-dotnet.csproj b/tests/common/shared-dotnet.csproj index b6da56214c9..ca1e6ada0a9 100644 --- a/tests/common/shared-dotnet.csproj +++ b/tests/common/shared-dotnet.csproj @@ -132,12 +132,25 @@ happen in a target (and not in a plain PropertyGroup), because $(HotReloadCompatibleBuild) is computed in the platform SDK targets, which are imported after this file. --> - - + + $(DefineConstants);HOTRELOAD_COMPATIBLE_BUILD + + + + + + +