From 8ad821da235944615add6590c98f9ded515d3d68 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Wed, 18 Mar 2026 18:18:10 +0000 Subject: [PATCH 1/6] Fix emulator boot and deploy flow for 'dotnet run' Address multiple issues preventing 'dotnet run --project' from successfully booting an Android emulator and deploying the app. Fixes: https://github.com/dotnet/android/issues/10965 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.Application.targets | 36 +++++++++++++-- .../Microsoft.Android.Sdk.BuildOrder.targets | 4 ++ .../BuildOrderTests.cs | 46 +++++++++++++++++++ .../Xamarin.Android.Common.targets | 15 ++++++ 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets index 6ce8c27ba5e..a94dc08289a 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets @@ -25,6 +25,7 @@ This file contains targets specific for Android application projects. <_AndroidComputeRunArgumentsDependsOn Condition=" '$(_AndroidComputeRunArgumentsDependsOn)' == '' "> _ResolveMonoAndroidSdks; + _EnsureDeviceBooted; _GetAndroidPackageName; _AndroidAdbToolPath; @@ -53,6 +54,28 @@ This file contains targets specific for Android application projects. + + + + + + + -s $(Device) + + + $(_MinimalSignAndroidPackageDependsOn); _GenerateEnvironmentFiles; + _RestoreDeviceFromCache; + _EnsureDeviceBooted; _Upload; _AndroidConfigureAdbReverse; $(_MinimalSignAndroidPackageDependsOn); _GenerateEnvironmentFiles; + _RestoreDeviceFromCache; + _EnsureDeviceBooted; _DeployApk; _DeployAppBundle; _AndroidConfigureAdbReverse; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs index 3945f526b9e..21e9984957d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs @@ -46,5 +46,51 @@ public void APFBDependsOn ([Values (false, true)] bool isAppProject) StringAssertEx.Contains ("Running target: 'MyPrepareTarget'", builder.LastBuildOutput); } + [Test] + public void DeployToDeviceDependsOn_ContainsEnsureDeviceBooted () + { + // _EnsureDeviceBooted must be in DeployToDeviceDependsOnTargets so that it fires + // when the .NET SDK calls ProjectInstance.Build(["DeployToDevice"]) in-process, + // where BeforeTargets hooks are not reliably triggered. + var checkTargets = new Import (() => "CheckDeployOrder.targets") { + TextContent = () => """ + + + + + +""" + }; + + var proj = new XamarinAndroidApplicationProject { + Imports = { checkTargets } + }; + + using var builder = CreateApkBuilder (); + builder.Verbosity = LoggerVerbosity.Detailed; + Assert.IsTrue (builder.RunTarget (proj, "_CheckDeployOrder"), + "Build should have succeeded."); + + string dependsOn = null; + foreach (var line in builder.LastBuildOutput) { + if (line.Contains ("DeployToDeviceDependsOnTargets=")) { + dependsOn = line; + break; + } + } + + Assert.IsNotNull (dependsOn, "DeployToDeviceDependsOnTargets property should be logged"); + StringAssert.Contains ("_EnsureDeviceBooted", dependsOn, + "DeployToDeviceDependsOnTargets must contain _EnsureDeviceBooted"); + + // _EnsureDeviceBooted must appear before _DeployApk to set AdbTarget first + int bootIndex = dependsOn.IndexOf ("_EnsureDeviceBooted", StringComparison.Ordinal); + int deployIndex = dependsOn.IndexOf ("_DeployApk", StringComparison.Ordinal); + if (deployIndex >= 0) { + Assert.Less (bootIndex, deployIndex, + "_EnsureDeviceBooted must appear before _DeployApk in DeployToDeviceDependsOnTargets"); + } + } + } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 84e1a46857c..93873f62dcc 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -995,9 +995,24 @@ because xbuild doesn't support framework reference assemblies. Overwrite="true" WriteOnlyWhenDifferent="true" /> + + + From f62b427e764f01b990236e2e7cda4ecd2078de48 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Wed, 18 Mar 2026 20:04:14 +0000 Subject: [PATCH 2/6] Remove device.props cache (no longer needed with SDK fix) The dotnet/sdk fix (dotnet/sdk#53018) now properly passes the Device global property to DeployToDevice via InvalidateGlobalProperties. Remove the device.props write/read cache that was working around this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.Application.targets | 22 ------------------- .../Microsoft.Android.Sdk.BuildOrder.targets | 2 -- .../Xamarin.Android.Common.targets | 15 ------------- 3 files changed, 39 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets index a94dc08289a..db06edf2e52 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets @@ -54,28 +54,6 @@ This file contains targets specific for Android application projects. - - - - - - - -s $(Device) - - - - - From 56042c7811e5cb3a5c99f11edc9ecda18b3d1ef7 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 18:29:22 +0000 Subject: [PATCH 3/6] Fix BuildOrderTests multi-line property parsing The DeployToDeviceDependsOnTargets property expands to multiple log lines because it includes $(_MinimalSignAndroidPackageDependsOn). The test was only checking the first line, which never contained _EnsureDeviceBooted. Switch to MSBuild property functions (Contains/IndexOf) to validate directly in MSBuild, avoiding log parsing entirely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BuildOrderTests.cs | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs index 21e9984957d..c5245511387 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs @@ -52,11 +52,19 @@ public void DeployToDeviceDependsOn_ContainsEnsureDeviceBooted () // _EnsureDeviceBooted must be in DeployToDeviceDependsOnTargets so that it fires // when the .NET SDK calls ProjectInstance.Build(["DeployToDevice"]) in-process, // where BeforeTargets hooks are not reliably triggered. + // Use MSBuild property functions for validation since the property value is multi-line. var checkTargets = new Import (() => "CheckDeployOrder.targets") { TextContent = () => """ - + + + <_BootIndex>$(DeployToDeviceDependsOnTargets.IndexOf('_EnsureDeviceBooted')) + <_DeployIndex>$(DeployToDeviceDependsOnTargets.IndexOf('_DeployApk')) + + """ @@ -67,29 +75,8 @@ public void DeployToDeviceDependsOn_ContainsEnsureDeviceBooted () }; using var builder = CreateApkBuilder (); - builder.Verbosity = LoggerVerbosity.Detailed; Assert.IsTrue (builder.RunTarget (proj, "_CheckDeployOrder"), - "Build should have succeeded."); - - string dependsOn = null; - foreach (var line in builder.LastBuildOutput) { - if (line.Contains ("DeployToDeviceDependsOnTargets=")) { - dependsOn = line; - break; - } - } - - Assert.IsNotNull (dependsOn, "DeployToDeviceDependsOnTargets property should be logged"); - StringAssert.Contains ("_EnsureDeviceBooted", dependsOn, - "DeployToDeviceDependsOnTargets must contain _EnsureDeviceBooted"); - - // _EnsureDeviceBooted must appear before _DeployApk to set AdbTarget first - int bootIndex = dependsOn.IndexOf ("_EnsureDeviceBooted", StringComparison.Ordinal); - int deployIndex = dependsOn.IndexOf ("_DeployApk", StringComparison.Ordinal); - if (deployIndex >= 0) { - Assert.Less (bootIndex, deployIndex, - "_EnsureDeviceBooted must appear before _DeployApk in DeployToDeviceDependsOnTargets"); - } + "Build should have succeeded — _EnsureDeviceBooted must be in DeployToDeviceDependsOnTargets before _DeployApk."); } } From 1133ab6ac7d2dc0d9af34c2ac6bb0c1f27e544ea Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Thu, 19 Mar 2026 23:43:06 +0000 Subject: [PATCH 4/6] Restore original log-based test, add MSBuild validation as second test Keep the readable log-parsing approach (fixed by joining all output lines for multi-line property matching) and add a separate MSBuild property function test as a safety net. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BuildOrderTests.cs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs index c5245511387..d80daab2fe3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs @@ -52,7 +52,44 @@ public void DeployToDeviceDependsOn_ContainsEnsureDeviceBooted () // _EnsureDeviceBooted must be in DeployToDeviceDependsOnTargets so that it fires // when the .NET SDK calls ProjectInstance.Build(["DeployToDevice"]) in-process, // where BeforeTargets hooks are not reliably triggered. - // Use MSBuild property functions for validation since the property value is multi-line. + var checkTargets = new Import (() => "CheckDeployOrder.targets") { + TextContent = () => """ + + + + + +""" + }; + + var proj = new XamarinAndroidApplicationProject { + Imports = { checkTargets } + }; + + using var builder = CreateApkBuilder (); + builder.Verbosity = LoggerVerbosity.Detailed; + Assert.IsTrue (builder.RunTarget (proj, "_CheckDeployOrder"), + "Build should have succeeded."); + + // The property is multi-line, so join all build output into a single string for matching + string allOutput = string.Join ("\n", builder.LastBuildOutput); + StringAssert.Contains ("_EnsureDeviceBooted", allOutput, + "DeployToDeviceDependsOnTargets must contain _EnsureDeviceBooted"); + + // _EnsureDeviceBooted must appear before _DeployApk to set AdbTarget first + int bootIndex = allOutput.IndexOf ("_EnsureDeviceBooted", StringComparison.Ordinal); + int deployIndex = allOutput.IndexOf ("_DeployApk", StringComparison.Ordinal); + if (deployIndex >= 0) { + Assert.Less (bootIndex, deployIndex, + "_EnsureDeviceBooted must appear before _DeployApk in DeployToDeviceDependsOnTargets"); + } + } + + [Test] + public void DeployToDeviceDependsOn_MSBuildValidation () + { + // Validates the same constraint using MSBuild property functions directly, + // as a safety net in case log parsing has edge cases. var checkTargets = new Import (() => "CheckDeployOrder.targets") { TextContent = () => """ From 17d3f5fc99be04b3dc51a349f34d60f0ce5fb2ad Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Fri, 20 Mar 2026 13:30:03 +0000 Subject: [PATCH 5/6] Fix log-based test: remove ordering check that scans full build output The IndexOf check for _EnsureDeviceBooted vs _DeployApk was scanning the entire build output, not just the property value. _DeployApk appears as a target reference earlier in the log, causing a false positive failure. Ordering is already validated reliably by DeployToDeviceDependsOn_MSBuildValidation which checks the property value directly via MSBuild functions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildOrderTests.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs index d80daab2fe3..1373ccd0b86 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs @@ -76,13 +76,10 @@ public void DeployToDeviceDependsOn_ContainsEnsureDeviceBooted () StringAssert.Contains ("_EnsureDeviceBooted", allOutput, "DeployToDeviceDependsOnTargets must contain _EnsureDeviceBooted"); - // _EnsureDeviceBooted must appear before _DeployApk to set AdbTarget first - int bootIndex = allOutput.IndexOf ("_EnsureDeviceBooted", StringComparison.Ordinal); - int deployIndex = allOutput.IndexOf ("_DeployApk", StringComparison.Ordinal); - if (deployIndex >= 0) { - Assert.Less (bootIndex, deployIndex, - "_EnsureDeviceBooted must appear before _DeployApk in DeployToDeviceDependsOnTargets"); - } + // Ordering (_EnsureDeviceBooted before _DeployApk) is validated by + // DeployToDeviceDependsOn_MSBuildValidation which checks the property + // value directly via MSBuild functions, avoiding false positives from + // target names appearing elsewhere in the full build log. } [Test] From 8b5f1fc3280c06473219546856f6d9d4eeba7725 Mon Sep 17 00:00:00 2001 From: Rui Marinho Date: Tue, 24 Mar 2026 12:12:34 +0000 Subject: [PATCH 6/6] Replace property-string tests with e2e device integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove DeployToDeviceDependsOn_ContainsEnsureDeviceBooted and DeployToDeviceDependsOn_MSBuildValidation from BuildOrderTests — they only checked that a property string contained a target name without testing actual behavior. Instead, extend the existing DeployToDevice device integration test to pass Device={serial} (the dotnet run code path) and assert _EnsureDeviceBooted was not skipped, verifying the target actually fires in the DeployToDevice dependency chain on a real device. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BuildOrderTests.cs | 67 ------------------- .../Tests/InstallAndRunTests.cs | 12 +++- 2 files changed, 11 insertions(+), 68 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs index 1373ccd0b86..3945f526b9e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildOrderTests.cs @@ -46,72 +46,5 @@ public void APFBDependsOn ([Values (false, true)] bool isAppProject) StringAssertEx.Contains ("Running target: 'MyPrepareTarget'", builder.LastBuildOutput); } - [Test] - public void DeployToDeviceDependsOn_ContainsEnsureDeviceBooted () - { - // _EnsureDeviceBooted must be in DeployToDeviceDependsOnTargets so that it fires - // when the .NET SDK calls ProjectInstance.Build(["DeployToDevice"]) in-process, - // where BeforeTargets hooks are not reliably triggered. - var checkTargets = new Import (() => "CheckDeployOrder.targets") { - TextContent = () => """ - - - - - -""" - }; - - var proj = new XamarinAndroidApplicationProject { - Imports = { checkTargets } - }; - - using var builder = CreateApkBuilder (); - builder.Verbosity = LoggerVerbosity.Detailed; - Assert.IsTrue (builder.RunTarget (proj, "_CheckDeployOrder"), - "Build should have succeeded."); - - // The property is multi-line, so join all build output into a single string for matching - string allOutput = string.Join ("\n", builder.LastBuildOutput); - StringAssert.Contains ("_EnsureDeviceBooted", allOutput, - "DeployToDeviceDependsOnTargets must contain _EnsureDeviceBooted"); - - // Ordering (_EnsureDeviceBooted before _DeployApk) is validated by - // DeployToDeviceDependsOn_MSBuildValidation which checks the property - // value directly via MSBuild functions, avoiding false positives from - // target names appearing elsewhere in the full build log. - } - - [Test] - public void DeployToDeviceDependsOn_MSBuildValidation () - { - // Validates the same constraint using MSBuild property functions directly, - // as a safety net in case log parsing has edge cases. - var checkTargets = new Import (() => "CheckDeployOrder.targets") { - TextContent = () => """ - - - - - <_BootIndex>$(DeployToDeviceDependsOnTargets.IndexOf('_EnsureDeviceBooted')) - <_DeployIndex>$(DeployToDeviceDependsOnTargets.IndexOf('_DeployApk')) - - - - -""" - }; - - var proj = new XamarinAndroidApplicationProject { - Imports = { checkTargets } - }; - - using var builder = CreateApkBuilder (); - Assert.IsTrue (builder.RunTarget (proj, "_CheckDeployOrder"), - "Build should have succeeded — _EnsureDeviceBooted must be in DeployToDeviceDependsOnTargets before _DeployApk."); - } - } } diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index fbea3acef3c..5341f509064 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -316,7 +316,17 @@ public void DeployToDevice (bool isRelease) var dotnet = new DotNetCLI (Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath)); Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed"); - Assert.IsTrue (dotnet.Build ("DeployToDevice"), "`dotnet build -t:DeployToDevice` should succeed"); + + // Pass the device serial as $(Device) — this is the `dotnet run` code path. + // _EnsureDeviceBooted must be in DeployToDeviceDependsOnTargets (not just BeforeTargets) + // because the .NET SDK invokes DeployToDevice via in-process ProjectInstance.Build() + // where BeforeTargets hooks don't reliably fire. + var serial = RunAdbCommand ("get-serialno").Trim (); + Assert.IsTrue (dotnet.Build ("DeployToDevice", parameters: new [] { $"Device={serial}" }), + "`dotnet build -t:DeployToDevice` should succeed"); + + // Verify _EnsureDeviceBooted actually ran in the chain + dotnet.AssertTargetIsNotSkipped ("_EnsureDeviceBooted"); // Verify correct targets ran based on FastDev support if (TestEnvironment.CommercialBuildAvailable) {