Skip to content

Commit 63f7cba

Browse files
[xabt] implement $(Device) and ComputeAvailableDevices MSBuild target (#10576)
Context: https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md This implements the first couple of steps to support new `dotnet run` behavior in .NET 11. * `$(Device)` MSBuild property to specify target device (passed from `dotnet run --device <id>`). This will simply set `$(AdbTarget)` for now. * `ComputeAvailableDevices` MSBuild target to get the list of connected Android devices/emulators using `adb devices` command: Target ComputeAvailableDevices 55 ms Using "GetAvailableAndroidDevices" task from assembly "D:\src\xamarin-android\bin\Debug\lib\packs\Microsoft.Android.Sdk.Windows\36.1.99\targets\..\tools\Xamarin.Android.Build.Tasks.dll". Task GetAvailableAndroidDevices 55 ms Assembly = D:\src\xamarin-android\bin\Debug\lib\packs\Microsoft.Android.Sdk.Windows\36.1.99\tools\Xamarin.Android.Build.Tasks.dll Parameters 16:47:08.2643378 C:\Program Files (x86)\Android\android-sdk\platform-tools\adb.exe devices -l 16:47:08.2867570 List of devices attached 16:47:08.2867734 0A041FDD400327 device product:redfin model:Pixel_5 device:redfin transport_id:2 16:47:08.2867765 emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1 16:47:08.2920363 Running process: C:\Program Files (x86)\Android\android-sdk\platform-tools\adb.exe -s emulator-5554 emu avd name 16:47:08.3172534 pixel_7_-_api_36 16:47:08.3172747 OK 16:47:08.3183905 Running process: exit code == 0 16:47:08.3185099 Found 2 Android device(s)/emulator(s) OutputItems TargetOutputs 0A041FDD400327 Status = Online Type = Device Device = redfin TransportId = 2 Description = Pixel 5 Product = redfin Model = Pixel_5 emulator-5554 Status = Online Type = Emulator Device = emu64xa TransportId = 1 Description = pixel 7 - api 36 Product = sdk_gphone64_x86_64 Model = sdk_gphone64_x86_64 Some of the extra MSBuild item metadata was completely optional, I just left anything that `adb devices -l` reports to be in here. Added unit tests for `GetAvailableAndroidDevices` task, that mostly test input/output parsing without actually running `adb`.
1 parent 9318103 commit 63f7cba

File tree

7 files changed

+725
-1
lines changed

7 files changed

+725
-1
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Instructions for AIs
22

3-
**.NET for Android** (formerly Xamarin.Android) - Open-source Android development bindings for .NET languages. `main` branch targets **.NET 10**.
3+
**.NET for Android** (formerly Xamarin.Android) - Open-source Android development bindings for .NET languages. `main` branch targets **.NET 11**.
44

55
## Architecture
66
- `src/Mono.Android/` - Android SDK bindings in C#

Documentation/docs-mobile/building-apps/build-properties.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,6 +1535,22 @@ If `DebugType` is not set or is the empty string, then the
15351535
`DebugSymbols` property controls whether or not the Application is
15361536
debuggable.
15371537

1538+
## Device
1539+
1540+
Specifies which Android device or emulator to target when using
1541+
`dotnet run --device <Device>` or MSBuild targets that interact with
1542+
devices (such as `Run`, `Install`, or `Uninstall`).
1543+
1544+
The value must be the full device serial number or identifier as
1545+
returned by `adb devices`. For example, if the device serial is
1546+
`emulator-5554`, you must use `-p:Device=emulator-5554`.
1547+
1548+
When set, this property is used to initialize the
1549+
[`AdbTarget`](#adbtarget) property with the value `-s "<Device>"`.
1550+
1551+
For more information about device selection, see the
1552+
[.NET SDK device selection specification](https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md).
1553+
15381554
## DiagnosticAddress
15391555

15401556
A value provided by `dotnet-dsrouter` such as `127.0.0.1`, the IP

Documentation/docs-mobile/building-apps/build-targets.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,36 @@ Added in Xamarin.Android 10.2.
3636

3737
Removes all files generated by the build process.
3838

39+
## ComputeAvailableDevices
40+
41+
Queries and returns a list of available Android devices and emulators that can be used with `dotnet run`.
42+
43+
This target is called automatically by the .NET SDK's `dotnet run` command to support device selection via the `--device` option. It returns an `@(Devices)` item group where each device has the following metadata:
44+
45+
- **Description**: A human-friendly name (e.g., "Pixel 7 - API 35" for emulators, "Pixel 6 Pro" for physical devices)
46+
- **Type**: Either "Device" or "Emulator"
47+
- **Status**: Device status - "Online", "Offline", "Unauthorized", or "NoPermissions"
48+
- **Model**: The device model identifier (optional)
49+
- **Product**: The product name (optional)
50+
- **Device**: The device name (optional)
51+
- **TransportId**: The adb transport ID (optional)
52+
53+
For example, to list all available devices:
54+
55+
```shell
56+
dotnet build -t:ComputeAvailableDevices
57+
```
58+
59+
This target is part of the [.NET SDK device selection specification](https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md) and enables commands like:
60+
61+
```shell
62+
dotnet run --device <device-serial>
63+
```
64+
65+
When a device is selected via the `$(Device)` property, the [`$(AdbTarget)`](build-properties.md#adbtarget) property is automatically set to target that specific device for all adb operations.
66+
67+
Added in .NET 11.
68+
3969
## FinishAotProfiling
4070

4171
Must be called *after* the [BuildAndStartAotProfiling](#buildandstartaotprofiling)

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.Application.targets

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This file contains targets specific for Android application projects.
99
<Project>
1010
<UsingTask TaskName="Xamarin.Android.Tasks.AndroidAdb" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
1111
<UsingTask TaskName="Xamarin.Android.Tasks.GetAndroidActivityName" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
12+
<UsingTask TaskName="Xamarin.Android.Tasks.GetAvailableAndroidDevices" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
1213
<UsingTask TaskName="Xamarin.Android.BuildTools.PrepTasks.XASleepInternal" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
1314

1415
<PropertyGroup>
@@ -26,6 +27,27 @@ This file contains targets specific for Android application projects.
2627
</_AndroidComputeRunArgumentsDependsOn>
2728
</PropertyGroup>
2829

30+
<!--
31+
***********************************************************************************************
32+
ComputeAvailableDevices
33+
34+
Target that queries available Android devices and emulators.
35+
This target is called by 'dotnet run' to support device selection.
36+
Returns @(Devices) items with metadata: Description, Type, Status
37+
38+
See: https://github.com/dotnet/sdk/blob/2b9fc02a265c735f2132e4e3626e94962e48bdf5/documentation/specs/dotnet-run-for-maui.md
39+
***********************************************************************************************
40+
-->
41+
<Target Name="ComputeAvailableDevices"
42+
DependsOnTargets="_ResolveMonoAndroidSdks"
43+
Returns="@(Devices)">
44+
<GetAvailableAndroidDevices
45+
ToolExe="$(AdbToolExe)"
46+
ToolPath="$(AdbToolPath)">
47+
<Output TaskParameter="Devices" ItemName="Devices" />
48+
</GetAvailableAndroidDevices>
49+
</Target>
50+
2951
<Target Name="_AndroidComputeRunArguments"
3052
BeforeTargets="ComputeRunArguments"
3153
DependsOnTargets="$(_AndroidComputeRunArgumentsDependsOn)">
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#nullable enable
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Text.RegularExpressions;
6+
using Microsoft.Android.Build.Tasks;
7+
using Microsoft.Build.Framework;
8+
using Microsoft.Build.Utilities;
9+
10+
namespace Xamarin.Android.Tasks;
11+
12+
/// <summary>
13+
/// MSBuild task that queries available Android devices and emulators using 'adb devices -l'.
14+
/// Returns a list of devices with metadata for device selection in dotnet run.
15+
/// </summary>
16+
public class GetAvailableAndroidDevices : AndroidAdb
17+
{
18+
enum DeviceType
19+
{
20+
Device,
21+
Emulator
22+
}
23+
24+
// Pattern to match device lines: <serial> <state> [key:value ...]
25+
// Example: emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64
26+
static readonly Regex AdbDevicesRegex = new(@"^([^\s]+)\s+(device|offline|unauthorized|no permissions)\s*(.*)$", RegexOptions.Compiled);
27+
28+
readonly List<string> output = [];
29+
30+
[Output]
31+
public ITaskItem [] Devices { get; set; } = [];
32+
33+
public GetAvailableAndroidDevices ()
34+
{
35+
Command = "devices";
36+
Arguments = "-l";
37+
}
38+
39+
protected override void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
40+
{
41+
base.LogEventsFromTextOutput (singleLine, messageImportance);
42+
output.Add (singleLine);
43+
}
44+
45+
protected override void LogToolCommand (string message) => Log.LogDebugMessage (message);
46+
47+
public override bool RunTask ()
48+
{
49+
if (!base.RunTask ())
50+
return false;
51+
52+
var devices = ParseAdbDevicesOutput (output);
53+
Devices = devices.ToArray ();
54+
55+
Log.LogDebugMessage ($"Found {Devices.Length} Android device(s)/emulator(s)");
56+
57+
return !Log.HasLoggedErrors;
58+
}
59+
60+
/// <summary>
61+
/// Parses the output of 'adb devices -l' command.
62+
/// Example output:
63+
/// List of devices attached
64+
/// emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:1
65+
/// 0A041FDD400327 device usb:1-1 product:raven model:Pixel_6_Pro device:raven transport_id:2
66+
/// </summary>
67+
List<ITaskItem> ParseAdbDevicesOutput (List<string> lines)
68+
{
69+
var devices = new List<ITaskItem> ();
70+
71+
foreach (var line in lines) {
72+
// Skip the header line "List of devices attached"
73+
if (line.Contains ("List of devices") || string.IsNullOrWhiteSpace (line))
74+
continue;
75+
76+
var match = AdbDevicesRegex.Match (line);
77+
if (!match.Success)
78+
continue;
79+
80+
var serial = match.Groups [1].Value.Trim ();
81+
var state = match.Groups [2].Value.Trim ();
82+
var properties = match.Groups [3].Value.Trim ();
83+
84+
// Parse key:value pairs from the properties string
85+
var propDict = new Dictionary<string, string> (StringComparer.OrdinalIgnoreCase);
86+
if (!string.IsNullOrWhiteSpace (properties)) {
87+
// Split by whitespace and parse key:value pairs
88+
var pairs = properties.Split ([' '], StringSplitOptions.RemoveEmptyEntries);
89+
foreach (var pair in pairs) {
90+
var colonIndex = pair.IndexOf (':');
91+
if (colonIndex > 0 && colonIndex < pair.Length - 1) {
92+
var key = pair.Substring (0, colonIndex);
93+
var value = pair.Substring (colonIndex + 1);
94+
propDict [key] = value;
95+
}
96+
}
97+
}
98+
99+
// Determine device type: Emulator or Device
100+
var deviceType = serial.StartsWith ("emulator-", StringComparison.OrdinalIgnoreCase) ? DeviceType.Emulator : DeviceType.Device;
101+
102+
// Build a friendly description
103+
var description = BuildDeviceDescription (serial, propDict, deviceType);
104+
105+
// Map adb state to device status
106+
var status = MapAdbStateToStatus (state);
107+
108+
// Create the MSBuild item
109+
var item = new TaskItem (serial);
110+
item.SetMetadata ("Description", description);
111+
item.SetMetadata ("Type", deviceType.ToString ());
112+
item.SetMetadata ("Status", status);
113+
114+
// Add optional metadata for additional information
115+
if (propDict.TryGetValue ("model", out var model))
116+
item.SetMetadata ("Model", model);
117+
if (propDict.TryGetValue ("product", out var product))
118+
item.SetMetadata ("Product", product);
119+
if (propDict.TryGetValue ("device", out var device))
120+
item.SetMetadata ("Device", device);
121+
if (propDict.TryGetValue ("transport_id", out var transportId))
122+
item.SetMetadata ("TransportId", transportId);
123+
124+
devices.Add (item);
125+
}
126+
127+
return devices;
128+
}
129+
130+
string BuildDeviceDescription (string serial, Dictionary<string, string> properties, DeviceType deviceType)
131+
{
132+
// Try to build a human-friendly description
133+
// Priority: AVD name (for emulators) > model > product > device > serial
134+
135+
// For emulators, try to get the AVD display name
136+
if (deviceType == DeviceType.Emulator) {
137+
var avdName = GetEmulatorAvdDisplayName (serial);
138+
if (!string.IsNullOrEmpty (avdName))
139+
return avdName!;
140+
}
141+
142+
if (properties.TryGetValue ("model", out var model) && !string.IsNullOrEmpty (model)) {
143+
// Clean up model name - replace underscores with spaces
144+
model = model.Replace ('_', ' ');
145+
return model;
146+
}
147+
148+
if (properties.TryGetValue ("product", out var product) && !string.IsNullOrEmpty (product)) {
149+
product = product.Replace ('_', ' ');
150+
return product;
151+
}
152+
153+
if (properties.TryGetValue ("device", out var device) && !string.IsNullOrEmpty (device)) {
154+
device = device.Replace ('_', ' ');
155+
return device;
156+
}
157+
158+
// Fallback to serial number
159+
return serial;
160+
}
161+
162+
static string MapAdbStateToStatus (string adbState)
163+
{
164+
// Map adb device states to the spec's status values
165+
return adbState.ToLowerInvariant () switch {
166+
"device" => "Online",
167+
"offline" => "Offline",
168+
"unauthorized" => "Unauthorized",
169+
"no permissions" => "NoPermissions",
170+
_ => "Unknown",
171+
};
172+
}
173+
174+
/// <summary>
175+
/// Queries the emulator for its AVD name using 'adb -s <serial> emu avd name'
176+
/// and formats it as a friendly display name.
177+
/// </summary>
178+
protected virtual string? GetEmulatorAvdDisplayName (string serial)
179+
{
180+
try {
181+
var adbPath = System.IO.Path.Combine (ToolPath, ToolExe);
182+
var outputLines = new List<string> ();
183+
184+
var exitCode = MonoAndroidHelper.RunProcess (
185+
adbPath,
186+
$"-s {serial} emu avd name",
187+
Log,
188+
onOutput: (sender, e) => {
189+
if (!string.IsNullOrEmpty (e.Data)) {
190+
outputLines.Add (e.Data);
191+
base.LogEventsFromTextOutput (e.Data, MessageImportance.Normal);
192+
}
193+
},
194+
logWarningOnFailure: false
195+
);
196+
197+
if (exitCode == 0 && outputLines.Count > 0) {
198+
var avdName = outputLines [0].Trim ();
199+
// Verify it's not the "OK" response
200+
if (!string.IsNullOrEmpty (avdName) && !avdName.Equals ("OK", StringComparison.OrdinalIgnoreCase)) {
201+
// Format the AVD name: replace underscores with spaces
202+
return avdName.Replace ('_', ' ');
203+
}
204+
}
205+
} catch (Exception ex) {
206+
Log.LogDebugMessage ($"Failed to get AVD display name for {serial}: {ex}");
207+
}
208+
209+
return null;
210+
}
211+
}

0 commit comments

Comments
 (0)