Skip to content

Backport dotnet-watch changes to main/.NET 11#10960

Merged
jonathanpeppers merged 6 commits intomainfrom
dev/peppers/backport-dotnet-watch
Mar 19, 2026
Merged

Backport dotnet-watch changes to main/.NET 11#10960
jonathanpeppers merged 6 commits intomainfrom
dev/peppers/backport-dotnet-watch

Conversation

@jonathanpeppers
Copy link
Member

Context: dotnet/sdk@bd5d3af

`dotnet run` now passes in `dotnet run -e FOO=BAR` as
`@(RuntimeEnvironmentVariable)` MSBuild items.

To opt in to this new feature, we need to add:

    <ProjectCapability Include="RuntimeEnvironmentVariableSupport" />

As well as update the `_GenerateEnvironmentFiles` MSBuild target:

    <!-- RuntimeEnvironmentVariable items come from 'dotnet run -e NAME=VALUE' -->
    <_GeneratedAndroidEnvironment Include="@(RuntimeEnvironmentVariable->'%(Identity)=%(Value)')" />

I added a new test to verify we have the env vars on-device at runtime.

Note that I tested this in combination with a local .NET SDK build:

* #10769

We won't be able to merge this until we have a .NET SDK here that
includes the above commit. Merging with nightly .NET 10.0.3xx SDK.
Context: dotnet/sdk#52492
Context: dotnet/sdk#52581

`dotnet-watch` now runs Android applications via:

    dotnet watch 🚀 [helloandroid (net10.0-android)] Launched 'D:\src\xamarin-android\bin\Debug\dotnet\dotnet.exe' with arguments 'run --no-build -e DOTNET_WATCH=1 -e DOTNET_WATCH_ITERATION=1 -e DOTNET_MODIFIABLE_ASSEMBLIES=debug -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://localhost:9000 -e DOTNET_STARTUP_HOOKS=D:\src\xamarin-android\bin\Debug\dotnet\sdk\10.0.300-dev\DotnetTools\dotnet-watch\10.0.300-dev\tools\net10.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll -bl': process id 3356

And so the pieces on Android for this to work are:

~~ Startup Hook Assembly ~~

Parse out the value:

    <_AndroidHotReloadAgentAssemblyPath>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_STARTUP_HOOKS')->'%(Value)'->Exists())</_AndroidHotReloadAgentAssemblyPath>

And verify this assembly is included in the app:

    <ResolvedFileToPublish Include="$(_AndroidHotReloadAgentAssemblyPath)" />

Then, for Android, we need to patch up `$DOTNET_STARTUP_HOOKS` to be
just the assembly name, not the full path:

    <_AndroidHotReloadAgentAssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension('$(_AndroidHotReloadAgentAssemblyPath)'))</_AndroidHotReloadAgentAssemblyName>
    ...
    <RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" />

~~ Port Forwarding ~~

A new `_AndroidConfigureAdbReverse` target runs after deploying apps,
that does:

    adb reverse tcp:9000 tcp:9000

I parsed the value out of:

    <_AndroidWebSocketEndpoint>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT')->'%(Value)')</_AndroidWebSocketEndpoint>
    <_AndroidWebSocketPort>$([System.Text.RegularExpressions.Regex]::Match('$(_AndroidWebSocketEndpoint)', ':(\d+)').Groups[1].Value)</_AndroidWebSocketPort>

~~ Prevent Startup Hooks in Microsoft.Android.Run ~~

When I was implementing this, I keep seeing *two* clients connect to
`dotnet-watch` and I was pulling my hair to figure out why!

Then I realized that `Microsoft.Android.Run` was also getting
`$DOTNET_STARTUP_HOOKS`, and so we had a desktop process + mobile
process both trying to connect!

Easiest fix, is to disable startup hook support in
`Microsoft.Android.Run`. I reviewed the code in `dotnet run`, and it
doesn't seem correct to try to clear the env vars.

~~ Conclusion ~~

With these changes, everything is working!

    dotnet watch 🔥 C# and Razor changes applied in 23ms.
Copilot AI review requested due to automatic review settings March 17, 2026 13:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Backport of .NET SDK dotnet run -e / dotnet watch behaviors into .NET for Android (.NET 11) by flowing @(RuntimeEnvironmentVariable) into the Android environment file, adding Hot Reload wiring (startup hook deployment + adb reverse), and extending device tests to validate the scenarios end-to-end.

Changes:

  • Add @(RuntimeEnvironmentVariable) support so dotnet run -e NAME=VALUE is written into the app’s generated environment file.
  • Add dotnet watch Hot Reload support by deploying the startup hook assembly, rewriting DOTNET_STARTUP_HOOKS for Android, and configuring adb reverse based on the websocket endpoint env var.
  • Add/extend device integration tests and test infrastructure (DotNetCLI, ProjectBuilder) to exercise dotnet run -e and dotnet watch hot reload.

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
tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs Adds device tests for dotnet watch hot reload and dotnet run -e environment variable propagation; adjusts run property passing.
src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets Writes @(RuntimeEnvironmentVariable) into __environment__.txt and ensures Hot Reload env adjustments happen first.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs Exposes BuiltBefore to support external build flows like dotnet watch while still using ProjectTools file update infrastructure.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs Adds StartWatch() and changes Run/StartRun parameter handling to allow passing non-MSBuild args (e.g. -e).
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ProjectCapabilities.targets Advertises new project capabilities needed by the .NET SDK (RuntimeEnvironmentVariableSupport, HotReloadWebSockets).
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets New targets to rewrite startup hook env var, deploy the delta applier assembly, and set up adb reverse.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets Hooks Hot Reload port forwarding and environment file generation into install/deploy target ordering.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets Ensures Hot Reload startup hook assembly is included in files to publish/deploy.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets Refactors _AdbToolPath computation into _AndroidAdbToolPath target for reuse.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.After.targets Imports the new Hot Reload targets for Android application projects.
src/Microsoft.Android.Run/Microsoft.Android.Run.csproj Disables startup hook support to avoid host-side startup hooks interfering with device Hot Reload.
Documentation/docs-mobile/building-apps/build-items.md Documents @(RuntimeEnvironmentVariable) usage and the dotnet run -e integration.
.github/copilot-instructions.md Documents test best-practice for modifying project files via ProjectTools (Touch + Save).

jonathanpeppers and others added 4 commits March 17, 2026 13:23
When multiple devices/emulators are connected, the adb reverse
command needs  (e.g. -s emulator-5554) to target
the correct device. Without it, the command fails with
'more than one device' or forwards on the wrong device.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment on lines +210 to +211
var proj = new XamarinAndroidApplicationProject ();
proj.SetRuntime (AndroidRuntime.MonoVM); // MonoVM only for now, until we get: https://github.com/dotnet/sdk/pull/53501
Copy link
Member Author

Choose a reason for hiding this comment

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

This works under Mono:

Image

Rerunning some failed lanes, but we can probably merge and parameterize for CoreCLR later.

@jonathanpeppers jonathanpeppers merged commit 24d6a92 into main Mar 19, 2026
6 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/peppers/backport-dotnet-watch branch March 19, 2026 12:26
jonathanpeppers added a commit that referenced this pull request Mar 19, 2026
Context: dotnet/sdk#52492
Context: dotnet/sdk#52581

`dotnet-watch` now runs Android applications via:

    dotnet watch 🚀 [helloandroid (net10.0-android)] Launched 'D:\src\xamarin-android\bin\Debug\dotnet\dotnet.exe' with arguments 'run --no-build -e DOTNET_WATCH=1 -e DOTNET_WATCH_ITERATION=1 -e DOTNET_MODIFIABLE_ASSEMBLIES=debug -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://localhost:9000 -e DOTNET_STARTUP_HOOKS=D:\src\xamarin-android\bin\Debug\dotnet\sdk\10.0.300-dev\DotnetTools\dotnet-watch\10.0.300-dev\tools\net10.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll -bl': process id 3356

And so the pieces on Android for this to work are:

~~ Startup Hook Assembly ~~

Parse out the value:

    <_AndroidHotReloadAgentAssemblyPath>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_STARTUP_HOOKS')->'%(Value)'->Exists())</_AndroidHotReloadAgentAssemblyPath>

And verify this assembly is included in the app:

    <ResolvedFileToPublish Include="$(_AndroidHotReloadAgentAssemblyPath)" />

Then, for Android, we need to patch up `$DOTNET_STARTUP_HOOKS` to be
just the assembly name, not the full path:

    <_AndroidHotReloadAgentAssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension('$(_AndroidHotReloadAgentAssemblyPath)'))</_AndroidHotReloadAgentAssemblyName>
    ...
    <RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" />

~~ Port Forwarding ~~

A new `_AndroidConfigureAdbReverse` target runs after deploying apps,
that does:

    adb reverse tcp:9000 tcp:9000

I parsed the value out of:

    <_AndroidWebSocketEndpoint>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT')->'%(Value)')</_AndroidWebSocketEndpoint>
    <_AndroidWebSocketPort>$([System.Text.RegularExpressions.Regex]::Match('$(_AndroidWebSocketEndpoint)', ':(\d+)').Groups[1].Value)</_AndroidWebSocketPort>

~~ Prevent Startup Hooks in Microsoft.Android.Run ~~

When I was implementing this, I keep seeing *two* clients connect to
`dotnet-watch` and I was pulling my hair to figure out why!

Then I realized that `Microsoft.Android.Run` was also getting
`$DOTNET_STARTUP_HOOKS`, and so we had a desktop process + mobile
process both trying to connect!

Easiest fix, is to disable startup hook support in
`Microsoft.Android.Run`. I reviewed the code in `dotnet run`, and it
doesn't seem correct to try to clear the env vars.

~~ Conclusion ~~

With these changes, everything is working!

    dotnet watch 🔥 C# and Razor changes applied in 23ms.

* Include $(AdbTarget) in adb reverse command

When multiple devices/emulators are connected, the adb reverse
command needs  (e.g. -s emulator-5554) to target
the correct device. Without it, the command fails with
'more than one device' or forwards on the wrong device.

NOTE: we run the test on MonoVM-only for now, until we get:
* dotnet/sdk#53501

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants