Skip to content

[Apple mobile] Stage the Helix R2R app payload outside the crossgen2 input directory - #132489

Open
davidnguyen-tech wants to merge 18 commits into
dotnet:mainfrom
davidnguyen-tech:davidnguyen-tech-super-guide
Open

[Apple mobile] Stage the Helix R2R app payload outside the crossgen2 input directory#132489
davidnguyen-tech wants to merge 18 commits into
dotnet:mainfrom
davidnguyen-tech:davidnguyen-tech-super-guide

Conversation

@davidnguyen-tech

@davidnguyen-tech davidnguyen-tech commented Aug 18, 2026

Copy link
Copy Markdown
Member

Fixes #131922

Problem

The build agent sends Apple mobile test files to Helix in publish/. On the Helix Mac, ProxyProjectForAOTOnHelix.proj acts as an MSBuild stand-in for the original test project: it ReadyToRun-compiles the shipped assemblies and builds the Apple app. It then copied Crossgen2 output back into publish/, which is also the compiler input directory.

Helix retries reuse the work-item directory. A retry therefore fed attempt 1's already-compiled, IL-stripped assemblies back into Crossgen2. The resulting app crashed before test discovery:

Fatal error.
A method body required at runtime was stripped from the ReadyToRun image.

This is retry input corruption in the build orchestration, not a Crossgen2 compiler bug.

Fix

Keep publish/ as the stable Crossgen2 input for every attempt.

For CoreCLR Mach-O R2R, the proxy now:

  • recreates obj/R2R/ before compiling;
  • creates obj/r2r-app-bundle-source/ from publish/ plus the current R2R output;
  • points AppleBuildDir and the bundle item lists at that directory.

Crossgen2 and AppleAppBuilder output can no longer become input to a later retry, and partial output from a killed attempt is removed.

Regression coverage

The new Apple.Build.Tests host test runs the real proxy targets twice in the same temporary work-item directory. It verifies that:

  • publish/ keeps the same files and contents;
  • final bundle paths are declared before the bundle source is created;
  • the bundle source contains exactly the publish inputs plus the current attempt's R2R output;
  • files left by a killed or previous attempt are removed.

Apple MSBuild changes set the apple_msbuild path-filter variable. A dedicated macOS x64 job then runs the test project directly through the normal runtime build entry point; non-PR rolling builds run it as well. The test does not require an Apple SDK or device.

Validation

  • ./build.sh --projects "$(pwd)/src/mono/msbuild/apple/tests/Apple.Build.Tests/Apple.Build.Tests.csproj" -c Release -test: 1 passed, 0 failed.
  • An earlier form of the same staging fix ran System.Net.Security.Unit.Tests three times in one tvOS Helix work-item directory in build 1558829. All three attempts rebuilt R2R, launched the app, reached 128 tests, and had no stripped-IL or APP_CRASH failure. The work item remained red because of a separate certificate-context test failure.

Note

This description and change were prepared with GitHub Copilot assistance and reviewed by the submitting developer.

The Helix ReadyToRun step copied the crossgen2 output over the publish
directory, but that directory is also the crossgen2 input. When the AOT
build runs twice in the same work item - a Helix work item retry re-runs
the whole command in the same directory - the second run compiles the
already compiled assemblies again, and --strip-il-bodies drops the IL of
an image that no longer has the original method bodies. The app then dies
at startup with "A method body required at runtime was stripped from the
ReadyToRun image".

Stage the app payload in obj/r2r-publish instead: copy the pristine
publish tree there, overlay the crossgen2 output on top of it, and point
AppleBuildDir and the Apple*ToBundle items at the staging directory.
AppleAppBuilder bundles every top level entry of its AppDir, so
redirecting the item lists alone is not enough, the AppDir itself has to
move out of the pristine input directory.

Also fail with a clear error if the publish directory already contains
R2R output, and remove obj/R2R before compiling so that a partially
written attempt cannot look up to date to the next one.

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

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a Helix retry hazard in Apple mobile CoreCLR test app ReadyToRun (R2R) compilation by preventing crossgen2 outputs from being written back into the publish/ directory (which is also the crossgen2 input). Instead, it stages a pristine app payload into an intermediate directory and overlays R2R outputs there, ensuring retries recompile from original IL inputs.

Changes:

  • Introduces an obj/r2r-publish/ staging directory: copies the original publish/ payload into it, then overlays crossgen2 outputs, and builds the app bundle from the staged payload.
  • Ensures retry safety by recreating the staging directory on every run and deleting obj/R2R before recompilation.
  • Adds guardrails: warnings when R2R outputs are unexpectedly empty and a fail-fast error if stale *.r2r.dylib outputs are detected in publish/.

A payload can legitimately arrive with ReadyToRun output in it: a test
project that sets PublishReadyToRun itself keeps that setting on the build
machine, where AppleBuild.props only suppresses the default, so the
composite image is published and then shipped inside the work item. Failing
the build on the mere presence of a *.r2r.dylib would break the first
attempt of such a work item, and the guard is not what keeps the crossgen2
inputs pristine, the staging directory is. Keep the diagnostic, drop the
hard failure, and say what the consequence is.

Also make the comment on the obj/R2R cleanup match what the SDK actually
tracks: the composite image is an output of _CreateR2RImages, the per
assembly files written next to it are not.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 21:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Measuring the actual payload showed the check has no signal. The build
machine does not strip IL when it publishes ReadyToRun for these projects,
because the in tree crossgen2 targets never pass the composite strip
argument: an agent published payload has 145956 method bodies and zero
stripped ones, and only the Helix compilation produces stripped bodies.
So the only thing that can still put a .r2r.dylib in the publish directory
is a build machine that pre compiled the app, where compiling again is
fine, and the message claimed the opposite. The staging directory is what
keeps the crossgen2 inputs pristine.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 23:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

The Apple Helix ReadyToRun proxy build had no test surface, so the retry
bug it just got fixed for could come back unnoticed. Cover it from
Wasm.Build.Tests, the only in-tree suite that already runs MSBuild driven
build tests on a CI build machine.

The test ships the real ProxyProjectForAOTOnHelix.proj into the test
payload and drives it with `dotnet msbuild`, so the production target
logic is what is under test rather than a copy of it. It lays out a Helix
shaped work item, stands in for crossgen2 with a stub props file, and runs
the R2R targets twice in the same directory with leftovers from a killed
attempt planted in between. Exact manifests assert that the publish
directory stays byte for byte identical across both runs, that the staging
directory is rebuilt from scratch, and that AppleBuildDir and the
Apple*ToBundle item lists resolve inside the staging directory.

Wire src/mono/msbuild/apple/* into the wasmbuildtests path set so a change
to the proxy project actually runs the test on PR CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 19, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/mono/wasm/Wasm.Build.Tests/AppleHelixR2RTests.cs:356

  • ReplaceDirectoryContent uses TryDeleteDirectory, which swallows IOException/UnauthorizedAccessException. Since this method is intended to ensure the crossgen output directory is clean between attempts, suppressing delete failures can leave stale files behind and make the test nondeterministic (or validate the wrong behavior). It’s better to delete the directory deterministically (or fail) and then recreate/populate it.
    private static void ReplaceDirectoryContent(string directory, (string RelativePath, string Content)[] files)
    {
        TryDeleteDirectory(directory);
        foreach ((string relativePath, string content) in files)
            WriteFile(Path.Combine(directory, ToNativePath(relativePath)), content);

The run script builds SDK_FOR_WORKLOAD_TESTING_PATH from $(dirname $0), so
it can be a relative path, and the child msbuild runs with a different
working directory. Resolve the host to a full path the way BuildEnvironment
already does, and point DOTNET_ROOT, DOTNET_INSTALL_DIR and PATH at it for
the same reason that file gives: the repo build environment sets them to its
own dotnet.

Narrow the path trigger to src/mono/msbuild/apple/data/*. The test only ever
loads the proxy project from data/, and the AppleBuild props and targets next
to it are shielded by the stub Directory.Build files, so triggering the whole
browser wasm test matrix on a change to those would buy no coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 19, 2026 01:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

The previous commit prepended the resolved host directory to PATH by
interpolating the parent value, which yields a trailing separator when the
parent has no PATH at all. An empty PATH entry means the working directory on
unix, and the child runs with the work item publish directory as its working
directory, so keep the separator out in that case.

Also say in the path trigger comment which files under apple/data the test
actually covers: it replaces the Directory.Build files with stubs, so a change
to those runs the test without exercising them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 19, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/mono/wasm/Wasm.Build.Tests/AppleHelixR2RTests.cs:378

  • ReplaceDirectoryContent deletes the directory via TryDeleteDirectory, which silently swallows IOException/UnauthorizedAccessException. In this method that behavior can hide real test setup failures and leave stale files behind (e.g., if deletion fails on Windows due to file locks), potentially making the test flaky or misleading. Prefer a deterministic delete that fails the test when the directory cannot be cleared; keep TryDeleteDirectory only for best-effort cleanup in the finally path.
    private static void ReplaceDirectoryContent(string directory, (string RelativePath, string Content)[] files)
    {
        TryDeleteDirectory(directory);
        foreach ((string relativePath, string content) in files)
            WriteFile(Path.Combine(directory, ToNativePath(relativePath)), content);

@davidnguyen-tech

Copy link
Copy Markdown
Member Author

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @akoeplinger, @matouskozak, @simonrozsival
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comment thread src/mono/msbuild/apple/tests/Directory.Build.props
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 780282fa-426e-4ebe-96ab-14cd314cce54
@davidnguyen-tech

Copy link
Copy Markdown
Member Author

Right now, the PR introduces a new subset - will change it not to.

Copilot AI review requested due to automatic review settings August 21, 2026 14:13
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 780282fa-426e-4ebe-96ab-14cd314cce54
@davidnguyen-tech
davidnguyen-tech marked this pull request as draft August 21, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Suppressed comments (1)

src/mono/msbuild/apple/tests/Directory.Build.props:14

  • The root props identifies this as a test project because it is under tests and ends in .Tests, but importing only the repository root bypasses src/libraries/Directory.Build.props. That is the import that enables eng/testing/tests.props and its implicit xUnit references, so the new Xunit usages cannot compile in this project. Import the libraries props here (it imports the root props itself).
  <Import Project="..\..\..\..\..\Directory.Build.props" />

Comment thread src/mono/msbuild/apple/tests/Directory.Build.targets
@davidnguyen-tech
davidnguyen-tech marked this pull request as ready for review August 21, 2026 14:38
Copilot AI review requested due to automatic review settings August 21, 2026 14:38
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
11 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/mono/msbuild/apple/tests/Directory.Build.props:14

  • This import bypasses src/libraries/Directory.Build.props, which is the layer that sets EnableTestSupport and imports eng/testing/tests.props (including the xUnit package setup). With only the root props imported, this project uses Fact/ITestOutputHelper without the implicit xUnit references and does not get the test-support properties used by the -test invocation. Import the libraries props (as Wasm.Build.Tests does) and configure this project for default source items/test support before relying on it as regression coverage.
  <Import Project="..\..\..\..\..\Directory.Build.props" />

src/mono/msbuild/apple/tests/Directory.Build.targets:4

  • The matching targets import also bypasses src/libraries/Directory.Build.targets, so eng/testing/tests.targets is never loaded. That file defines the Test target (and GenerateRunScript/RunTests) that the new pipeline invokes with -test; importing only the repository-root targets leaves that command unavailable. Route this import through the libraries targets after wiring the test project into the standard test-support chain.
  <Import Project="..\..\..\..\..\Directory.Build.targets" />

@davidnguyen-tech

Copy link
Copy Markdown
Member Author

/azp run runtime-extra-platforms

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@BrzVlad

BrzVlad commented Aug 24, 2026

Copy link
Copy Markdown
Member

Might fix #126458 as well.

Comment thread eng/pipelines/runtime.yml
#
# Apple MSBuild changes trigger this host test project directly on an ordinary macOS build host.
# It only drives MSBuild and file orchestration, so it needs no Apple mobile workload or device.
- template: /eng/pipelines/common/platform-matrix.yml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that familiar with the infrastructure bits or the logic of this new test but I'm wondering whether this is overkill. Do we really need this new job that will only ever have a single test which seems quite complex to me, trying to mimic helix packaging behavior. I'm wondering if it would be simpler and also provide better coverage to have a simple runtime test, that we make sure is being sent to helix as an independent work item. Maybe as part of a helix retry, the work item would receive some additional env var that the test could end up checking. If this env var is not set, the test would fail, making sure we hit the retry path. Then, on second run, the test would pass, effectively testing this logic regardless of configuration, r2r or not r2r, mobile or desktop, also testing exactly what happens as part of normal test runs.

@akoeplinger for second opinion in case I'm speaking nonsense

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it looks like an overkill at the moment.

I was thinking more in long-term. This could be the foundation for more tests - which might be easier to add than this initial one.

I'm not sure how much we want to test infra though.

@matouskozak matouskozak Aug 26, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not add a dedicated job as that requires provisioning azure runner and other stuff. We could add this to the tvOS CoreCLR job (either as another workitem) or a build stage.

I don't have a strong opinion on whether to test this or not :)

@matouskozak

Copy link
Copy Markdown
Member

Might fix #126458 as well.

If I'm reading this PR right, I think this is scoped purely for CoreCLR R2R and shouldn't affect Mono.

@matouskozak

matouskozak commented Aug 26, 2026

Copy link
Copy Markdown
Member

Overall looking good, except for the test inclusion/location.

Before merging this, can we test if this fixes also the other tests where we disabled the IL stripping (e.g. #130468 and others)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[tvOS CoreCLR] System.Net.Security.Unit.Tests crashes because required Dictionary IL was stripped from R2R image

4 participants