Skip to content

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

Open
jonathanpeppers wants to merge 5 commits intomainfrom
dev/peppers/backport-dotnet-watch
Open

Backport dotnet-watch changes to main/.NET 11#10960
jonathanpeppers wants to merge 5 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).

Comment on lines +78 to +79
<Exec Condition=" '$(_AndroidWebSocketPort)' != '' "
Command="&quot;$(_AdbToolPath)&quot; reverse tcp:$(_AndroidWebSocketPort) tcp:$(_AndroidWebSocketPort)" />
jonathanpeppers and others added 3 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>
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