Skip to content

Add NativeAOT publish slice for Android + Apple and CI integration - #1597

Draft
matouskozak wants to merge 17 commits into
dotnet:mainfrom
matouskozak:matouskozak/nativeaot-android-apple-poc
Draft

Add NativeAOT publish slice for Android + Apple and CI integration#1597
matouskozak wants to merge 17 commits into
dotnet:mainfrom
matouskozak:matouskozak/nativeaot-android-apple-poc

Conversation

@matouskozak

@matouskozak matouskozak commented May 21, 2026

Copy link
Copy Markdown
Member

What

Opt-in NativeAOT publish for Microsoft.DotNet.XHarness.CLI covering only the Android and Apple command surfaces, plus a new Helix-backed pipeline stage that exercises it end-to-end.

The change is additive and gated: nothing happens unless /p:XHarnessNativeAot=true is passed. The existing multi-TFM global-tool build, the nupkg, and every existing E2E stage are byte-identical to before.

Why

A per-RID single-file xharness lets us run the Android + Apple integration tests on Helix workers that don't have a .NET SDK installed, with faster startup and a simpler payload. WASM/WASI stay on the existing JIT path because their dependencies (Microsoft.AspNetCore.App, Selenium.WebDriver, non-generic Activator.CreateInstance) aren't AOT-compatible without significant rework.

How

-p:XHarnessNativeAot=true flips a single MSBuild switch in Microsoft.DotNet.XHarness.CLI.csproj that:

  • Targets a single TFM ($(NetCurrent)), sets PublishAot=true, InvariantGlobalization=true.
  • Defines XHARNESS_NATIVEAOT so a handful of #if blocks compile WASM/WASI command-set registrations, help branches, and the Selenium-typed EnumPageLoadStrategyArgument out.
  • Drops Selenium.WebDriver and Microsoft.AspNetCore.App.
  • Excludes Commands/WASM, Commands/WASI, Commands/WebServer.cs, CommandArguments/WASM, CommandArguments/WASI, CommandArguments/Arguments/WebServer*.cs.
  • Renames the published binary to xharness so it sits on PATH unchanged.
  • Adds a post-Publish Copy target that stages adb and the mlaunch tree under publish/runtimes/any/native/... (the existing IncludeAdb / IncludeMlaunch targets hook DispatchToInnerBuilds / _GetPackageFiles which don't fire for a single-TFM publish).

AOT-compat fixes applied to both builds (back-compatible with the global-tool layout):

  • MacOSProcessManager.DetectMlaunchPath and AdbRunner.GetCliAdbExePath now use AppContext.BaseDirectory (valid in single-file) and probe the global-tool layout first, falling back to the flat publish layout.
  • XHarnessVersionCommand.GetAssemblyVersion falls back to Environment.ProcessPath when Assembly.Location is empty.
  • AndroidStateCommand, AppleStateCommand, CommandDiagnostics switch from reflection-based System.Text.Json to [JsonSerializable] source-generated contexts.

CI

Build_AOT_OSX stage publishes the AOT slice for osx-arm64 and uploads it as the XHarnessCli_AOT_osx-arm64 pipeline artifact.

E2E_Apple_Simulators_AOT stage runs the existing tests/integration-tests/Apple/Simulator.Tests.proj against that artifact through Helix. The integration-tests Directory.Build.props grows a $(UseXHarnessAotPayload) branch that disables IncludeXHarnessCli (so the Helix SDK skips its dotnet tool install), registers the AOT publish dir as a correlation payload, re-chmods adb/mlaunch, prepends the payload to PATH, and unsets XHARNESS_CLI_PATH so nothing tries dotnet exec on a native binary. The nupkg path is untouched.

eng/e2e-aot-test.yml is a sibling of eng/e2e-test.yml that downloads the AOT artifact and threads it through. Kept as a sibling rather than parameterising the existing template so the JIT stages stay byte-identical.

Verified locally

Check Result
dotnet publish -r osx-arm64 -p:XHarnessNativeAot=true 12 MB xharness binary; 76 MB total publish dir (binary + bundled adb + bundled mlaunch).
./xharness help, apple state --json, android adb -- version on the published binary All work end-to-end. apple state --json returns real simulator data via bundled mlaunch.
Default ./build.sh (4 TFMs) 0 warnings / 0 errors.
Unit tests (Common, CLI, Android, Apple, iOS.Shared) All pass.
YAML parse on pipeline + template files Pass.

Out of scope

  • Android / iOS-Devices / tvOS / Apple Commands / SimulatorInstaller AOT stages.
  • Linux + Windows AOT publish.

Both are intentional follow-ups so this PR keeps a focused signal — once E2E_Apple_Simulators_AOT is stable, the same template extends to the other queues.

Known AOT analyzer warnings (do not crash, tracked for separate cleanup)

  • Enum.GetValues(Type) in two spots in CommandArguments/Argument.cs (trivial to switch to Enum.GetValues<TEnum>()).
  • AddConsoleFormatter<T,TOptions> registration (an Microsoft.Extensions.Logging AOT gap).
  • TypeFromAssemblyArgument reflection (only used by test-filter scenarios).
  • xharness version prints empty in the AOT binary because no AssemblyInformationalVersion is baked in.

Opening as draft to let the new pipeline stages validate in CI before promoting.

matouskozak and others added 5 commits May 21, 2026 07:38
Introduces an opt-in '$(XHarnessAotNative)' build configuration that
publishes the xharness CLI as a per-RID NativeAOT single-file binary
covering only the Android and Apple command surfaces. The existing
multi-TFM global tool build is unchanged when the property is not set.

What the AOT slice does:
* Targets a single TFM ($(NetCurrent)), enables PublishAot, defines the
  XHARNESS_AOT_NATIVE compile constant and turns off PackAsTool / IsPackable.
* Drops the Selenium.WebDriver PackageReference and the
  Microsoft.AspNetCore.App FrameworkReference and excludes WASM/WASI
  source files (Commands/WASM, Commands/WASI, the matching
  CommandArguments folders and Commands/WebServer.cs).
* Conditionally compiles the WASM/WASI command-set registrations,
  help-command branches and the Selenium-typed
  EnumPageLoadStrategyArgument away from the AOT build.
* Adds a CopyAdbAndMlaunchForAotPublish target that runs after Publish
  to lay out adb and mlaunch under runtimes/any/native/... next to the
  native binary (the existing IncludeAdb / IncludeMlaunch hooks rely on
  DispatchToInnerBuilds / _GetPackageFiles which only fire for the
  multi-TFM / pack flow).

AOT-compat fixes that apply to both builds:
* Replace Assembly.Location with AppContext.BaseDirectory in
  MacOSProcessManager.DetectMlaunchPath and AdbRunner.GetCliAdbExePath,
  keeping the existing global-tool relative layout as a fallback so the
  global tool keeps working.
* Guard XHarnessVersionCommand.GetAssemblyVersion against an empty
  Assembly.Location under single-file/AOT.
* Replace the reflection-based System.Text.Json calls in
  AndroidStateCommand, AppleStateCommand and CommandDiagnostics with
  source-generated JsonSerializerContext partials so JSON output works
  without reflection metadata.

Verified end to end:
* dotnet publish -c Release -r osx-arm64 -p:XHarnessAotNative=true
  produces a 12 MB native binary; total publish dir (binary + bundled
  adb + bundled mlaunch) ~ 76 MB.
* Smoke tests against the published binary: 'help' shows the expected
  Android + Apple command tree, 'apple state --json' returns real
  simulator data via bundled mlaunch, 'android adb -- version' invokes
  the bundled adb.
* The default 4-TFM build (./build.sh) still succeeds with 0 warnings
  / 0 errors and all 429 unit tests (CLI, Common, Android, Apple,
  iOS.Shared) pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a Helix-backed E2E stage that runs the existing Apple simulator
integration tests against a NativeAOT-published xharness instead of
the global-tool install. This is the first concrete CI signal for the
AOT slice introduced in the previous commit; other platforms will be
added incrementally once this one is stable.

Pieces:

* azure-pipelines-public.yml: new 'Build_AOT_OSX' stage that runs
  ./build.sh --restore + 'dotnet publish ... -p:XHarnessAotNative=true
  -r osx-arm64' on a macOS-15 agent and uploads the publish dir as the
  'XHarnessCli_AOT_osx-arm64' pipeline artifact. New 'E2E_Apple_Simulators_AOT'
  stage placed right after the existing E2E_Apple_Simulators (same
  testProject) using the new e2e-aot-test.yml template.

* eng/e2e-aot-test.yml: clone of e2e-test.yml that downloads the AOT
  pipeline artifact and passes /p:UseXHarnessAotPayload=true
  /p:XHarnessAotPayloadPath=... when submitting to Helix. Depends on
  both Build_OSX (for parity with the existing E2E template's
  expectations) and Build_AOT_OSX.

* tests/integration-tests/Directory.Build.props: new MSBuild branch
  gated on $(UseXHarnessAotPayload). When set, IncludeXHarnessCli is
  disabled (so the Helix SDK skips its dotnet-tool install path) and
  the AOT publish dir is registered as a HelixCorrelationPayload at
  'xharness-aot'. HelixPreCommands then re-chmod +x the bundled adb
  and mlaunch (zip strips the executable bit), prepend the payload
  dir to PATH, set the usual XHARNESS_* env vars, and unset
  XHARNESS_CLI_PATH so nothing tries 'dotnet exec' on a native binary.
  The existing nupkg-based path is unchanged when the property is
  unset.

* src/Microsoft.DotNet.XHarness.CLI/Microsoft.DotNet.XHarness.CLI.csproj:
  when XHarnessAotNative=true, override AssemblyName to 'xharness' so
  the published native binary matches what the Helix work-item scripts
  invoke from PATH.

Local verification:
* dotnet publish -r osx-arm64 -p:XHarnessAotNative=true produces a
  12 MB binary named 'xharness' under publish/, with adb and mlaunch
  laid out at publish/runtimes/any/native/...; './xharness help'
  works from the publish dir.
* Default 'dotnet build src/Microsoft.DotNet.XHarness.CLI' (4 TFMs)
  still completes with 0 warnings / 0 errors.
* yaml.safe_load() round-trips all of azure-pipelines.yml,
  azure-pipelines-public.yml, eng/e2e-test.yml and eng/e2e-aot-test.yml
  cleanly.

Not yet covered (intentional): Android / iOS-Devices / tvOS / Apple
command / SimulatorInstaller AOT stages, Linux + Windows AOT publish,
non-Apple Helix queues. These follow once the Apple Simulators AOT
stage shows stable signal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ot-android-apple-poc

# Conflicts:
#	src/Microsoft.DotNet.XHarness.Common/CommandDiagnostics.cs
…E -> XHARNESS_NATIVEAOT)

Pure rename for consistency with the NativeAOT terminology used
elsewhere in the .NET ecosystem ('NativeAOT', not 'AOT Native'):

  MSBuild property:  $(XHarnessAotNative)  ->  $(XHarnessNativeAot)
  Compile constant:  XHARNESS_AOT_NATIVE    ->  XHARNESS_NATIVEAOT

Touches:
* src/Microsoft.DotNet.XHarness.CLI/Microsoft.DotNet.XHarness.CLI.csproj
* src/Microsoft.DotNet.XHarness.CLI/Program.cs
* src/Microsoft.DotNet.XHarness.CLI/Commands/XHarnessHelpCommand.cs
* src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Argument.cs
* azure-pipelines-public.yml (Build_AOT_OSX 'dotnet publish' invocation)

No behavioural change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… AOT-safe

Build_AOT_OSX failed in CI with NU1102 because the SDK pinned in
global.json (11.0.100-preview.4.26210.111) was published to the
'dotnet11' nightly feed but its matching runtime packs
(Microsoft.NETCore.App.Runtime.osx-arm64,
Microsoft.AspNetCore.App.Runtime.osx-arm64,
Microsoft.NETCore.App.Runtime.NativeAOT.osx-arm64,
runtime.osx-x64.Microsoft.DotNet.ILCompiler) at the same version
never made it to 'dotnet-public'. Only the .26230.115 build is on
'dotnet-public', and the SDK exact-version-matches its runtime packs
during a self-contained / AOT publish, so 'dotnet publish ...' could
not restore.

This only affects the AOT publish - the regular global-tool build does
not need RID-specific runtime packs and works fine on dotnet-public.

Fixes:

* NuGet.config: add the 'dotnet11' public nightly feed which has all
  four runtime packs at the exact pinned SDK version. Comment explains
  why it's needed for the AOT publish and not for the regular build.

* src/Microsoft.DotNet.XHarness.Common/RunSummaryEmitter.cs: replace
  the two reflection-based
  JsonSerializer.Serialize<Dictionary<string, object?>>(...) calls with
  JsonObject / JsonArray + ToJsonString(). The previous code triggered
  IL2026 and IL3050 under PublishAot because STJ cannot statically know
  how to serialize an 'object'-typed dictionary value, and the data
  would actually fail to serialize correctly at runtime inside an AOT
  binary. JsonObject/JsonArray are first-class STJ DOM types that work
  without reflection. The JSON shape and ordering produced are
  identical to before, so existing InstrumentationRunnerSummaryTests
  keep passing as-is.

Verified locally:

* dotnet publish -r osx-arm64 -p:XHarnessNativeAot=true now succeeds.
  The four IL2026/IL3050 warnings from RunSummaryEmitter are gone;
  remaining warnings are pre-existing (XHarnessVersionCommand version
  metadata, Enum.GetValues, AddConsoleFormatter) and tracked for
  separate cleanup.
* The published xharness binary runs end-to-end:
  'apple state --json' returns real simulator data via bundled mlaunch.
* Unit tests pass: Common (14), Android (33), Apple (82).
* Default 'dotnet build src/Microsoft.DotNet.XHarness.CLI' clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
matouskozak and others added 12 commits May 21, 2026 15:00
The AOT publish currently produces an osx-arm64 binary only; sending it
to the osx.15.amd64.open queue would fail at execution time because an
arm64 Mach-O cannot run on amd64 hardware.

Gate the amd64 queue on UseXHarnessAotPayload != 'true' so the existing
JIT global-tool flow (which is architecture-neutral) keeps exercising
both queues, while the new E2E_Apple_Simulators_AOT stage targets only
osx.26.arm64.open.

Observed in build 1429447: arm64 work items completed in ~62s; amd64
work items never picked up due to today's queue outage, and would have
failed anyway because of the arch mismatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The first end-to-end AOT Helix run (build 1429962) failed all 6 work
items with 'dotnet: command not found' (exit 127). Investigation showed
the Helix SDK's xharness-runner.apple.sh defines an unconditional bash
alias:

    function xharness() {
        dotnet exec "$XHARNESS_CLI_PATH" "$@"
    }

So no amount of prepending the native binary to PATH or unsetting
XHARNESS_CLI_PATH avoids the call to 'dotnet'. And because the AOT
flow sets IncludeXHarnessCli=false, the Helix SDK no longer also sets
IncludeDotNetCli=true, so there's no .NET runtime installed on the
worker either.

Surgical fix that doesn't require touching the Helix SDK: ship a tiny
'dotnet' bash shim alongside the native binary that translates
'dotnet exec <managed-dll> <args>' into a direct invocation of the
native 'xharness' binary with the same args. The shim is placed first
on PATH so it shadows any real dotnet (there isn't one anyway). Any
other dotnet invocation fails fast with a clear message — the shim is
intentionally not a general-purpose dotnet replacement.

Pieces:

* eng/aot/dotnet-shim.sh: the new shim script (committed; the publish
  target copies it into the publish dir as 'dotnet').

* src/Microsoft.DotNet.XHarness.CLI/Microsoft.DotNet.XHarness.CLI.csproj:
  CopyAdbAndMlaunchForAotPublish now also copies the shim into the
  publish dir as 'dotnet' and chmod +x's it.

* tests/integration-tests/Directory.Build.props (AOT branch):
  - Re-chmod the shim under HelixPreCommands (zip strips +x).
  - Stop unsetting XHARNESS_CLI_PATH; instead point it at the native
    binary. The Helix SDK xharness() alias will pass it to the shim,
    which ignores the value and invokes the native binary directly.

Local verification:
* dotnet publish -r osx-arm64 -p:XHarnessNativeAot=true now produces
  publish/{xharness, dotnet, runtimes/...}.
* Both 'xharness help' and 'dotnet exec /fake.dll help' through the
  shim print the same Android+Apple command tree end-to-end.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hing else through

The previous shim intercepted any 'dotnet ...' invocation and either
translated 'dotnet exec <dll> <args>' into a native xharness call or
errored out. That was safe but brittle: anything else on a Helix
worker that ever needed a real 'dotnet' (e.g. dotnet-trace,
dotnet-counters, dotnet test, dotnet exec of some other dll) would
hit our error message instead.

New behaviour:

  1. 'dotnet exec <...Microsoft.DotNet.XHarness.CLI.dll> <args>'
     -> exec the native xharness binary with <args>. Tightened from
     the previous 'any dotnet exec' match so unrelated dotnet exec
     calls fall through.

  2. Any other invocation -> forward to the next real 'dotnet' on
     PATH, with the shim's own directory removed first so it cannot
     recursively re-enter itself. If no real dotnet is present (which
     is the case in today's AOT Helix flow), fail fast with a clear
     message.

Verified locally against the published AOT artifact in five scenarios:
* Helix pattern with no real dotnet -> runs native xharness.
* Other 'dotnet exec' with no real dotnet -> clear error.
* 'dotnet --info' with no real dotnet -> clear error.
* 'dotnet --info' with a real dotnet on PATH -> prints SDK info
  (forwarded to the real dotnet).
* Helix pattern with a real dotnet on PATH -> still routes to native
  xharness (interception takes precedence over pass-through).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After tightening the shim in bfdfa85 to only intercept calls of the
form 'dotnet exec *Microsoft.DotNet.XHarness.CLI.dll <args>', the AOT
Helix flow regressed (build 1435574 'E2E Apple - Simulators (NativeAOT)'
failed). The reason: in the AOT flow XHARNESS_CLI_PATH is set to the
native binary path (.../xharness-aot/xharness) - there is no managed
CLI dll. The Helix SDK xharness() bash alias unconditionally runs
'dotnet exec "$XHARNESS_CLI_PATH" "$@"', so the call becomes
'dotnet exec .../xharness-aot/xharness <args>', which no longer matched
the case pattern and fell through to the 'no real dotnet on PATH'
error path.

Broaden the case to also match '*/xharness' and bare 'xharness'.
Keeps the explicit JIT shape ('*Microsoft.DotNet.XHarness.CLI.dll')
because future flows might point XHARNESS_CLI_PATH at the managed
assembly even when the shim is on PATH.

Caught by code review (all three review models flagged it). The
right long-term fix is for the Helix SDK to learn about NativeAOT
xharness payloads directly, but until then this keeps the shim
honest about its contract: it intercepts calls that target our
payload (managed or native), and forwards everything else to a real
dotnet on PATH.

Local verification (sanitised PATH, no real dotnet):
* Helix shape 'dotnet exec $XHARNESS_CLI_PATH help' -> native xharness.
* JIT shape  'dotnet exec /any/...XHarness.CLI.dll help' -> native xharness.
* Unrelated 'dotnet exec /some/other(.dll)' -> clear error (no real dotnet).
* 'dotnet --info' with a real dotnet on PATH -> forwarded.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lVersion)

FileVersionInfo.ProductVersion on a Mach-O / ELF binary is empty
because there is no Win32 VS_VERSION_INFO resource embedded. The
previous AOT fallback through Environment.ProcessPath produced a
non-null FileVersionInfo but an empty ProductVersion, so
'xharness version' printed a blank line under the NativeAOT slice.

Switch to reading AssemblyInformationalVersionAttribute (set by
Arcade at build time and present in the managed metadata bundled
inside both JIT assemblies and AOT images), with AssemblyFileVersion
and FileVersionInfo as fall-backs for unusual deployment shapes.

The native binary now prints:
  $ xharness version
  11.0.0-dev
  $ xharness version -v
  XHarness version 11.0.0-dev
  InstalledDir: /.../publish

Drive-by:
* Delete the now-unused GetAssemblyVersion() helper. The two
  remaining callers (Program.cs, InstallCommand.cs) only ever read
  .ProductVersion off the returned FileVersionInfo, so they switch
  to the new GetProductVersion() string accessor cleanly.
* Suppress IL3000 on the legitimate Assembly.Location single-file
  fall-back (the empty-string case is explicitly handled by
  reaching Environment.ProcessPath).

All unit tests still pass (CLI 37, Common 11+3 skipped, Apple 82,
Android 33). Found by GPT-5.4 code review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…atorInstaller

Extends the NativeAOT pilot from one Apple stage to three, exercising
two additional Apple-simulator integration test surfaces against the
same osx-arm64 NativeAOT xharness artifact produced by Build_AOT_OSX.
No new publish jobs, no new shim work, no changes to existing JIT
stages.

* tests/integration-tests/Apple/Simulator.Commands.Tests.proj:
  - Add osx.26.arm64.open queue gated on UseXHarnessAotPayload=true.
  - Mirror Simulator.Tests.proj's architecture-detection pattern so
    TestArch (x64 vs arm64) and the iOS simulator app bundle URL
    follow the queue.
  - Leave iOSSimulatorVersionUnderTest=18.1 pinned only for the amd64
    queue (its Xcode set is known). On the arm64 queue, let xharness
    pick the simulator that ships with the worker's Xcode by
    dropping the version suffix from TestTarget. This avoids chasing
    image-version bumps in this proj.

* tests/integration-tests/Apple/SimulatorInstaller.Tests.proj:
  - Add osx.26.arm64.open queue gated on UseXHarnessAotPayload=true.
  - The helper script uses 'dotnet "$XHARNESS_CLI_PATH" apple
    simulators ...' which the dotnet shim already routes to the
    native binary - no script changes needed.

* azure-pipelines-public.yml:
  - New stages E2E_Apple_Simulator_Commands_AOT and
    E2E_Apple_Simulator_Mgmt_AOT, both referencing the existing
    eng/e2e-aot-test.yml template and the existing
    XHarnessCli_AOT_osx-arm64 pipeline artifact.

Local verification:
* Default 'dotnet build src/Microsoft.DotNet.XHarness.CLI' clean
  (0 warn / 0 err).
* Both proj files msbuild-restore cleanly with UseXHarnessAotPayload=true
  and HelixTargetQueue=osx.26.arm64.open set.
* yaml.safe_load(azure-pipelines-public.yml) confirms three AOT
  stages now reference eng/e2e-aot-test.yml.

This is Phase 1 of the documented AOT-CI expansion plan (Apple arm64
sim breadth, zero new AOT publishes). Phase 2 (Linux x64 AOT for
Android scenarios) and Phase 3 (Windows AOT for Android devices) will
follow in separate PRs once Phase 1 stays stable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous run failed OSX Release on
Microsoft.DotNet.XHarness.iOS.Shared.Tests.Hardware.TCCDatabaseTests.AgreeToPromptsAsyncSuccessTest.
That suite is pure mock-based, passes locally (17/17), passed on
OSX Debug in the same build (1439029), and is also flaky on main
(see build 1404973). Retriggering with an empty commit.

[skip ci - empty-retrigger]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… shape

Build 1439245 revealed two bugs in the Phase 1 AOT expansion (the JIT
Simulator Commands stage also regressed because the proj fix targets a
shared code path).

1) tests/integration-tests/Apple/Simulator.Commands.Tests.proj:
   The previous edit copied the IsArm64Queue/IsAmd64Queue detection
   pattern from Simulator.Tests.proj, but that pattern works there
   because TestArch is consumed inside TestAppBundle.proj which the
   Helix SDK re-invokes per-queue with HelixTargetQueue set as a
   property. Simulator.Commands.Tests.proj builds the download URL
   directly in the outer project, where $(HelixTargetQueue) is the
   *property* (empty - only the @(HelixTargetQueue) ItemGroup is
   populated) at evaluation time. Result: TestArch was empty and the
   download URL had a double slash, producing 404s in both JIT and
   AOT runs.

   Fix: derive TestArch and the iOS sim version pin from
   UseXHarnessAotPayload directly. Since each queue maps 1:1 to a
   code path (AOT -> arm64-only; JIT -> the legacy amd64 setup) this
   is correct and avoids the property-vs-item evaluation pitfall.

2) eng/aot/dotnet-shim.sh:
   The Helix-payloads helper script (simulatorinstaller-integration-tests.sh)
   invokes xharness as 'dotnet "$XHARNESS_CLI_PATH" apple simulators
   list' - i.e. 'dotnet <path>' with no 'exec' keyword. The shim only
   intercepted 'dotnet exec <path>', so the call fell through to the
   'no real dotnet on PATH' error and Sim Mgmt AOT failed.

   Broaden the shim to also intercept 'dotnet <…/xharness>' and
   'dotnet <…/Microsoft.DotNet.XHarness.CLI.dll>' (without exec).
   Pass-through behaviour for unrelated calls is unchanged.

Local verification:
* msbuild -getProperty on Simulator.Commands.Tests.proj prints the
  correct TestArch + URL in both JIT and AOT shapes; both URLs
  HTTP 200.
* Shim exercised against the published artifact:
  - 'dotnet exec /.../xharness …'  -> native (Helix xharness() alias)
  - 'dotnet /.../xharness …'      -> native (SimulatorInstaller script)
  - 'dotnet exec /.../XHarness.CLI.dll …' -> native (JIT shape)
  - 'dotnet --info' with real dotnet -> forwarded
  - 'dotnet --info' / 'dotnet other.dll' without real dotnet -> clear error

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…queue arch detection properly

Build 1439245 fix (commit dd10fc6) used UseXHarnessAotPayload to pick
TestArch. That made the JIT path lose the arm64 queue coverage it had
just gained - JIT only ran on amd64, AOT only on arm64, with no
overlap. The right shape is what Simulator.Tests.proj already does:
JIT runs on both queues, AOT runs on arm64 only.

Root cause of the original 1439245 404 was a misunderstanding of when
$(HelixTargetQueue) is populated:

* The Helix SDK MultiQueue.targets re-invokes the project per
  HelixTargetQueue item via 'MSBuild Projects="$(MSBuildProjectFile)"
  Targets="Test" Properties="HelixTargetQueue=%(Identity);..."' from
  inside the CoreTest target.
* Anything that runs BEFORE CoreTest in the outer invocation (the
  TestApple target was wired with BeforeTargets="CoreTest") fires
  with HelixTargetQueue empty.
* Anything that runs INSIDE the per-queue re-invocation (Test target,
  or anything chained to it) sees HelixTargetQueue populated.

Fix: keep the original $(HelixTargetQueue).Contains('arm64') pattern
(matches Simulator.Tests.proj), but move TestApple from
BeforeTargets="CoreTest" to BeforeTargets="Test" so the download
happens per-queue with HelixTargetQueue in scope. Also add an Error
guard that catches the empty-TestArch case loudly instead of producing
a silently-broken double-slash URL.

Matrix verified via 'dotnet msbuild -getProperty':
  queue                 UseXHarnessAotPayload   TestArch  URL has /arch/  iOS pin
  osx.15.amd64.open     (unset)                 x64       x64             18.1
  osx.15.amd64.open     true                    x64       x64             18.1 (n/a, queue filtered out)
  osx.26.arm64.open     (unset)                 arm64     arm64           none
  osx.26.arm64.open     true                    arm64     arm64           none

Net behaviour:
  E2E_Apple_Simulator_Commands       (JIT) -> osx.15.amd64.open + osx.26.arm64.open
  E2E_Apple_Simulator_Commands_AOT   (AOT) -> osx.26.arm64.open

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build 1440410 surfaced two more issues:

* The previous attempt (commit a0de383) wired TestApple to
  BeforeTargets="Test" so per-queue $(HelixTargetQueue) would be
  populated. But `eng/common/build.sh --test` only invokes the Test
  target - it does NOT call CoreBuild beforehand. Without
  CoreBuild, the MultiQueue per-queue dispatch never fires, so
  the project is evaluated only once with $(HelixTargetQueue)
  empty - same failure mode as the original 06637b1.

* Even if CoreBuild had run, the Helix SDK's CreateAppleWorkItems
  fans every XHarnessAppBundleToTest item out to every queue (no
  per-queue affinity). So the previous approach of producing one
  item per queue would have ended up sending the arm64 bundle to
  the amd64 worker and vice versa.

Replace the property-based arch detection with HelixTargetQueue
metadata + a batched target. Each HelixTargetQueue item carries
its own TestArch and (optional) iOSSimulatorVersionUnderTest
metadata. The TestApple target is batched on
HelixTargetQueue.Identity so it runs once per queue, downloads
the matching app bundle, and registers a XHarnessAppBundleToTest
item that the Helix SDK pairs with the right queue.

Behaviour verified via 'dotnet msbuild -getItem':
  JIT (UseXHarnessAotPayload unset):
    2 queues, 2 bundles - x64 with TestTarget=ios-simulator-64_18.1,
                          arm64 with TestTarget=ios-simulator-64
  AOT (UseXHarnessAotPayload=true):
    1 queue, 1 bundle - arm64 with TestTarget=ios-simulator-64

This restores arm64 JIT coverage in the Sim Commands proj and
preserves the AOT pilot's arm64-only target. JIT continues to
exercise both queues; AOT exercises only the arm64 queue (the
published binary is osx-arm64 only).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Build 1440699 surfaced two regressions introduced by the previous
per-queue rework of Simulator.Commands.Tests.proj:

1. Work item duplication on the JIT amd64 job. The batched 'TestApple'
   target with Outputs="%(HelixTargetQueue.Identity)" iterated over the
   full HelixTargetQueue collection in both the outer pass AND the
   inner per-queue MSBuild re-invocation. In the inner pass the proj
   was still freshly evaluated with both queue items declared, so two
   XHarnessAppBundleToTest items were registered and both ended up
   dispatched to whichever queue's CoreTest was running - one with a
   mismatched arch app bundle. Main's job sends 1 work item; this PR
   was sending 2.

2. arm64 queue had no iOSSimulatorVersionUnderTest pinned, which made
   the CustomCommands chain call 'xharness apple simulators install
   ios-simulator-64' (no version suffix). SimulatorsCommand requires
   the '_X.Y' suffix and throws ArgumentException: 'Failed to parse
   simulator ios-simulator-64'. Reproduced locally against the AOT
   binary - the truncated Helix stack trace looked AOT-shaped but was
   actually this validation error.

Switch to the property-derivation pattern that Simulator.Tests.proj
already uses successfully:
- TestArch is derived from $(HelixTargetQueue).Contains('amd64'|'arm64')
  at PropertyGroup evaluation. In the inner per-queue re-invocation
  the property holds the single queue identity, so the selector
  resolves to the correct arch with no batching needed.
- iOSSimulatorVersionUnderTest is pinned to 18.1 for both queues.
- The TestApple target is gated on Condition="$(HelixTargetQueue) != ''"
  so it only runs in the inner pass. In the outer pass no work items
  are registered; CreateAppleWorkItems is a no-op (it guards on
  '@(XHarnessAppBundleToTest)' != '').

Verified locally with msbuild + a diagnostic AfterTargets:
- Outer: HelixTargetQueue='', XHarnessAppBundleToTest count=0
- Inner amd64: count=1, URL .../x64/..., target=ios-simulator-64_18.1
- Inner arm64 (UseXHarnessAotPayload=true): count=1, URL .../arm64/...,
  target=ios-simulator-64_18.1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ot-android-apple-poc

# Conflicts:
#	src/Microsoft.DotNet.XHarness.Common/CommandDiagnostics.cs
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.

1 participant