diff --git a/src/Microsoft.Android.Run/Program.cs b/src/Microsoft.Android.Run/Program.cs index b61ce341b2a..cc61504f00d 100644 --- a/src/Microsoft.Android.Run/Program.cs +++ b/src/Microsoft.Android.Run/Program.cs @@ -6,6 +6,7 @@ const string VersionsFileName = "Microsoft.Android.versions.txt"; string? adbPath = null; +string? adbTarget = null; string? package = null; string? activity = null; bool verbose = false; @@ -38,6 +39,9 @@ int Run (string[] args) "Path to the {ADB} executable. If not specified, will attempt to locate " + "the Android SDK automatically.", v => adbPath = v }, + { "adb-target=", + "The {TARGET} device/emulator for adb commands (e.g., '-s emulator-5554').", + v => adbTarget = v }, { "p|package=", "The Android application {PACKAGE} name (e.g., com.example.myapp). Required.", v => package = v }, @@ -123,6 +127,8 @@ int Run (string[] args) if (verbose) { Console.WriteLine ($"Using adb: {adbPath}"); + if (!string.IsNullOrEmpty (adbTarget)) + Console.WriteLine ($"Target: {adbTarget}"); Console.WriteLine ($"Package: {package}"); if (!string.IsNullOrEmpty (activity)) Console.WriteLine ($"Activity: {activity}"); @@ -224,13 +230,15 @@ void StartLogcat () if (!string.IsNullOrEmpty (logcatArgs)) logcatArguments += $" {logcatArgs}"; + var fullArguments = string.IsNullOrEmpty (adbTarget) ? logcatArguments : $"{adbTarget} {logcatArguments}"; + if (verbose) - Console.WriteLine ($"Running: adb {logcatArguments}"); + Console.WriteLine ($"Running: adb {fullArguments}"); var locker = new Lock(); var psi = new ProcessStartInfo { FileName = adbPath, - Arguments = logcatArguments, + Arguments = fullArguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -320,12 +328,14 @@ void StopApp () (int ExitCode, string Output, string Error) RunAdb (string arguments) { + var fullArguments = string.IsNullOrEmpty (adbTarget) ? arguments : $"{adbTarget} {arguments}"; + if (verbose) - Console.WriteLine ($"Running: adb {arguments}"); + Console.WriteLine ($"Running: adb {fullArguments}"); var psi = new ProcessStartInfo { FileName = adbPath, - Arguments = arguments, + Arguments = fullArguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, 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 3eddd2ac6c1..d6efbf7b5ee 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 @@ -68,8 +68,9 @@ This file contains targets specific for Android application projects. <_AndroidRunPath Condition=" '$(_AndroidRunPath)' == '' ">$(MSBuildThisFileDirectory)..\tools\Microsoft.Android.Run.dll <_AndroidRunLogcatArgs Condition=" '$(_AndroidRunLogcatArgs)' == '' ">monodroid-assembly:S + <_AndroidRunAdbTargetArg Condition=" '$(AdbTarget)' != '' ">--adb-target "$(AdbTarget)" dotnet - exec "$(_AndroidRunPath)" --adb "$(_AdbToolPath)" --package "$(_AndroidPackage)" --activity "$(AndroidLaunchActivity)" --logcat-args "$(_AndroidRunLogcatArgs)" $(_AndroidRunExtraArgs) + exec "$(_AndroidRunPath)" --adb "$(_AdbToolPath)" $(_AndroidRunAdbTargetArg) --package "$(_AndroidPackage)" --activity "$(AndroidLaunchActivity)" --logcat-args "$(_AndroidRunLogcatArgs)" $(_AndroidRunExtraArgs) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs index 6961acbba1d..32135f406c8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs @@ -132,7 +132,7 @@ public bool Publish (string target = null, string runtimeIdentifier = null, stri return Execute (arguments.ToArray ()); } - public bool Run (bool waitForExit = false) + public bool Run (bool waitForExit = false, string [] parameters = null) { string binlog = Path.Combine (Path.GetDirectoryName (projectOrSolution), "run.binlog"); var arguments = new List { @@ -142,6 +142,11 @@ public bool Run (bool waitForExit = false) $"/bl:\"{binlog}\"", $"/p:WaitForExit={waitForExit.ToString (CultureInfo.InvariantCulture)}" }; + if (parameters != null) { + foreach (var parameter in parameters) { + arguments.Add ($"/p:{parameter}"); + } + } return Execute (arguments.ToArray ()); } @@ -149,8 +154,9 @@ public bool Run (bool waitForExit = false) /// Starts `dotnet run` and returns a running Process that can be monitored and killed. /// /// Whether to use Microsoft.Android.Run tool which waits for app exit and streams logcat. + /// Optional MSBuild properties to pass (e.g., "Device=emulator-5554"). /// A running Process instance. Caller is responsible for disposing. - public Process StartRun (bool waitForExit = true) + public Process StartRun (bool waitForExit = true, string [] parameters = null) { string binlog = Path.Combine (Path.GetDirectoryName (projectOrSolution), "run.binlog"); var arguments = new List { @@ -160,6 +166,11 @@ public Process StartRun (bool waitForExit = true) $"/bl:\"{binlog}\"", $"/p:WaitForExit={waitForExit.ToString (CultureInfo.InvariantCulture)}" }; + if (parameters != null) { + foreach (var parameter in parameters) { + arguments.Add ($"/p:{parameter}"); + } + } return ExecuteProcess (arguments.ToArray ()); } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 45a98f688d6..ee75d41fb97 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -298,6 +298,11 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidStripNativeLibraries Condition=" '$(_AndroidStripNativeLibraries)' != 'true' ">false + + + -s $(Device) + + @@ -819,11 +824,6 @@ because xbuild doesn't support framework reference assemblies. android.app.Application - - - - -s $(Device) - diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 5e4c4a5e1b8..3c1e2691dd1 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -121,6 +121,82 @@ public void DotNetRunWaitForExit () Assert.IsTrue (foundMessage, $"Expected message '{logcatMessage}' was not found in output. See {logPath} for details."); } + [Test] + public void DotNetRunWithDeviceParameter () + { + const string logcatMessage = "DOTNET_RUN_DEVICE_TEST_67890"; + var proj = new XamarinAndroidApplicationProject (); + + // Enable verbose output from Microsoft.Android.Run for debugging + proj.SetProperty ("_AndroidRunExtraArgs", "--verbose"); + + // Add a Console.WriteLine that will appear in logcat + proj.MainActivity = proj.DefaultMainActivity.Replace ( + "//${AFTER_ONCREATE}", + $"Console.WriteLine (\"{logcatMessage}\");"); + + using var builder = CreateApkBuilder (); + builder.Save (proj); + + var dotnet = new DotNetCLI (Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath)); + Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed"); + + // Get the attached device serial + var serial = GetAttachedDeviceSerial (); + + // Start dotnet run with Device parameter, which should set $(AdbTarget) + using var process = dotnet.StartRun (waitForExit: true, parameters: [$"Device={serial}"]); + + var locker = new Lock (); + var output = new StringBuilder (); + var outputReceived = new ManualResetEventSlim (false); + bool foundMessage = false; + bool foundAdbTarget = false; + + process.OutputDataReceived += (sender, e) => { + if (e.Data != null) { + lock (locker) { + output.AppendLine (e.Data); + // Check for the --adb-target argument in verbose output + if (e.Data.Contains ($"Target: -s {serial}")) { + foundAdbTarget = true; + } + if (e.Data.Contains (logcatMessage)) { + foundMessage = true; + outputReceived.Set (); + } + } + } + }; + process.ErrorDataReceived += (sender, e) => { + if (e.Data != null) { + lock (locker) { + output.AppendLine ($"STDERR: {e.Data}"); + } + } + }; + + process.BeginOutputReadLine (); + process.BeginErrorReadLine (); + + // Wait for the expected message or timeout + bool messageFound = outputReceived.Wait (TimeSpan.FromSeconds (60)); + + // Kill the process (simulating Ctrl+C) + if (!process.HasExited) { + process.Kill (entireProcessTree: true); + process.WaitForExit (); + } + + // Write the output to a log file for debugging + string logPath = Path.Combine (Root, builder.ProjectDirectory, "dotnet-run-device-output.log"); + File.WriteAllText (logPath, output.ToString ()); + TestContext.AddTestAttachment (logPath); + + Assert.IsTrue (foundAdbTarget, $"Expected --adb-target argument with serial '{serial}' was not found in verbose output. See {logPath} for details."); + Assert.IsTrue (foundMessage, $"Expected message '{logcatMessage}' was not found in output. See {logPath} for details."); + } + [Test] [TestCase (true)] [TestCase (false)]