Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions dotnet/BreakingChanges.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,38 @@ for tvOS apps).

The version format has also changed, from "X.Y.Z.W (`branch`: `hash`)" to the
semantic versioning we use for .NET: "X.Y.Z-`branch`.Z+sha.`hash`".

## Debug iOS / tvOS / Mac Catalyst CoreCLR bundle composition

Debug iOS, tvOS, and Mac Catalyst apps built on CoreCLR now restrict the
composite ReadyToRun (R2R) image to root only on `System.Private.CoreLib.dll`.
Every other R2R-eligible framework assembly is re-headered to point at the
single per-app composite, so the upstream `Microsoft.NETCore.App.r2r.framework`
and per-module BCL `.r2r.framework` bundles are no longer carried into the app
bundle. The bundle now contains a single `<App>.r2r.framework` instead.

This change reduces Debug bundle size and improves cold startup and incremental
build times, but it changes the on-disk shape of the Debug bundle:

* `Microsoft.NETCore.App.r2r.framework` is no longer present.
* Per-module BCL R2R frameworks (e.g. `System.Linq.r2r.framework`) are no
longer present.
* A single `<App>.r2r.framework` aggregates the R2R code path.

Apps that introspect their own Debug bundle structure may need adjustment.
macOS, NativeAOT, Mono, and Release builds are unaffected.

To opt out and restore the previous behavior (every R2R-eligible assembly is a
root), declare the underlying SDK item group statically in the project:

```xml
<ItemGroup>
<PublishReadyToRunCompositeRoots Include="System.Private.CoreLib.dll" />
<PublishReadyToRunCompositeRoots Include="System.Linq.dll" />
<!-- ...enumerate the roots you want in the composite... -->
</ItemGroup>
```

Any user-provided `PublishReadyToRunCompositeRoots` takes precedence over the
macios default.

60 changes: 60 additions & 0 deletions dotnet/targets/Microsoft.Sdk.R2R.targets
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,66 @@
</ItemGroup>
</Target>

<!--
Restrict the R2R composite to a single root (System.Private.CoreLib.dll) for Debug
iOS/tvOS/MacCatalyst CoreCLR builds. All other BCL/framework assemblies remain in
the composite as UNROOTED inputs, so crossgen2 rewrites their component R2R header
owner pointer to our local composite (e.g. "MyApp.iOS.r2r.dylib") but skips code
generation for them.

Why this is needed: BCL assemblies in the runtime pack come pre-built with a
component R2R header pointing to upstream "Microsoft.NETCore.App.r2r.dylib".
Adding them to PublishReadyToRunExclude would leave that header in place, and at
runtime CoreCLR's NativeImage::Open -> HostInformation::GetNativeCodeData lookup
of "Microsoft.NETCore.App.r2r.dylib" fails (macios only registers the local
composite), triggering RaiseFailFastException -> SIGABRT at startup. Making them
unrooted composite inputs is the only mechanism that rewrites the per-component
header to a name macios actually registered.

User assemblies stay in PublishReadyToRunExclude via _SelectR2RAssemblies (they
have no upstream R2R headers to begin with).

Override per-project by declaring PublishReadyToRunCompositeRoots statically in
the csproj (or an imported props/targets file). Any user-provided set takes
precedence over this default and this target becomes a no-op:
<ItemGroup>
<PublishReadyToRunCompositeRoots Include="System.Private.CoreLib.dll" />
<PublishReadyToRunCompositeRoots Include="System.Linq.dll" />
... whatever roots you need ...
</ItemGroup>
-->
<Target Name="_ConfigureCoreLibOnlyCompositeRoots"
Condition="'$(UseMonoRuntime)' == 'false' And '$(_PlatformName)' != 'macOS' And '$(_UseNativeAot)' != 'true' And '$(Configuration)' == 'Debug' And '$(RuntimeIdentifiers)' == '' And '$(PublishReadyToRun)' == 'true' And '$(PublishReadyToRunComposite)' == 'true' And '$(PublishReadyToRunContainerFormat)' == 'macho' And '@(PublishReadyToRunCompositeRoots->Count())' == '0'"
AfterTargets="_SelectR2RAssemblies"
BeforeTargets="_PrepareForReadyToRunCompilation">
<ItemGroup>
<PublishReadyToRunCompositeRoots Include="System.Private.CoreLib.dll" KeepDuplicates="false" />
</ItemGroup>
<Message Importance="high" Text="Restricting R2R composite roots to @(PublishReadyToRunCompositeRoots) for Debug $(_PlatformName) CoreCLR" />
</Target>

<!--
Microsoft.NET.CrossGen.targets removes _ReadyToRunCompositeBuildInput (rooted
composite inputs) and _ReadyToRunCompileList from ResolvedFileToPublish so the
rewritten copies in obj/.../R2R/ replace the originals. It does NOT remove
_ReadyToRunCompositeUnrootedBuildInput, which causes NETSDK1152 "multiple publish
output files with the same relative path" once we have any unrooted composite
inputs. Mirror the same Remove for the unrooted set.

Decoupled from the policy target above so this fix also applies whenever a user
explicitly sets PublishReadyToRunCompositeRoots in their csproj (in any
configuration). Track upstream fix at dotnet/sdk; once the SDK does the
symmetric Remove, this target can be retired.
-->
<Target Name="_DedupUnrootedReadyToRunPublish"
Condition="@(PublishReadyToRunCompositeRoots->Count()) != 0"
AfterTargets="_PrepareForReadyToRunCompilation">
<ItemGroup>
<ResolvedFileToPublish Remove="@(_ReadyToRunCompositeUnrootedBuildInput)" />
</ItemGroup>
<Message Importance="high" Text="Removed @(_ReadyToRunCompositeUnrootedBuildInput->Count()) unrooted composite originals from ResolvedFileToPublish (NETSDK1152 workaround)" />
</Target>

<!-- Set per-module R2R header symbol names so each .o file exports a unique symbol directly,
avoiding the need for post-link symbol aliasing. -->
<Target Name="_SetR2RHeaderSymbolNames"
Expand Down
188 changes: 188 additions & 0 deletions tests/dotnet/UnitTests/ReadyToRunCompositeTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#nullable enable

namespace Xamarin.Tests {
[TestFixture]
public class ReadyToRunCompositeTest : TestBaseClass {
const string EmptyAppManifest =
@"<?xml version=""1.0"" encoding=""UTF-8""?>
<!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd"">
<plist version=""1.0"">
<dict>
<key>CFBundleIdentifier</key>
<string>ID</string>
</dict>
</plist>";

const string EmptyMainFile =
@"using System;
using Foundation;

class MainClass {
static void Main ()
{
Console.WriteLine (typeof (NSObject));
}
}
";

// Verify that Debug builds of CoreCLR iOS/tvOS/MacCatalyst apps default to a
// composite R2R image rooted only on System.Private.CoreLib.dll, so the app
// bundle contains exactly one per-app composite framework rather than the
// upstream Microsoft.NETCore.App.r2r.* set or per-module .r2r.framework files.
[Test]
[TestCase (ApplePlatform.iOS, "ios-arm64")]
[TestCase (ApplePlatform.TVOS, "tvos-arm64")]
[TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64")]
public void Debug_DefaultsToCoreLibOnlyCompositeRoot (ApplePlatform platform, string runtimeIdentifiers)
{
var project = "MySimpleApp";
Configuration.IgnoreIfIgnoredPlatform (platform);
Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers);

var configuration = "Debug";
var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: configuration);
Clean (project_path);

var properties = GetDefaultProperties (runtimeIdentifiers);
properties ["Configuration"] = configuration;
properties ["UseMonoRuntime"] = "false";

var rv = DotNet.AssertBuild (project_path, properties);

var allTargets = BinLog.GetAllTargets (rv.BinLogPath);
AssertTargetExecuted (allTargets, "_ConfigureCoreLibOnlyCompositeRoots", "default Debug CoreCLR build");
AssertTargetExecuted (allTargets, "_DedupUnrootedReadyToRunPublish", "default Debug CoreCLR build");

AssertCoreLibOnlyBundleComposition (platform, appPath, project);
}

// Verify the policy target skips itself when the user has already set
// PublishReadyToRunCompositeRoots via their csproj/Directory.Build.props.
// The dedup target still fires (it is decoupled from the policy decision
// and exists purely to work around the NETSDK1152 asymmetry in
// Microsoft.NET.CrossGen.targets).
[Test]
[TestCase (ApplePlatform.iOS, "ios-arm64")]
[TestCase (ApplePlatform.TVOS, "tvos-arm64")]
[TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64")]
public void Debug_UserSetCompositeRoots_OptsOutOfDefault (ApplePlatform platform, string runtimeIdentifiers)
{
Configuration.IgnoreIfIgnoredPlatform (platform);
Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers);

var tmpdir = Cache.CreateTemporaryDirectory ();
var csproj = $@"<Project Sdk=""Microsoft.NET.Sdk"">
<PropertyGroup>
<TargetFramework>{platform.ToFramework ()}</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PublishReadyToRunCompositeRoots Include=""System.Private.CoreLib.dll"" KeepDuplicates=""false"" />
<PublishReadyToRunCompositeRoots Include=""System.Linq.dll"" KeepDuplicates=""false"" />
</ItemGroup>
</Project>";

var project_path = Path.Combine (tmpdir, "TestProject.csproj");
File.WriteAllText (project_path, csproj);
File.WriteAllText (Path.Combine (tmpdir, "Info.plist"), EmptyAppManifest);
File.WriteAllText (Path.Combine (tmpdir, "Main.cs"), EmptyMainFile);

var properties = GetDefaultProperties (runtimeIdentifiers);
properties ["Configuration"] = "Debug";
properties ["UseMonoRuntime"] = "false";

var rv = DotNet.AssertBuild (project_path, properties);

var allTargets = BinLog.GetAllTargets (rv.BinLogPath);
AssertTargetNotExecuted (allTargets, "_ConfigureCoreLibOnlyCompositeRoots", "user-set composite roots opt out");
AssertTargetExecuted (allTargets, "_DedupUnrootedReadyToRunPublish", "dedup target stays active whenever composite roots are set");
}

// Verify Release CoreCLR builds are unchanged: the CoreLib-only policy target
// is gated to Debug and must not fire in Release, where the full per-module
// composite is still the intended behavior.
[Test]
[TestCase (ApplePlatform.iOS, "ios-arm64")]
[TestCase (ApplePlatform.TVOS, "tvos-arm64")]
[TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64")]
public void Release_DoesNotRestrictCompositeRoots (ApplePlatform platform, string runtimeIdentifiers)
{
var project = "MySimpleApp";
Configuration.IgnoreIfIgnoredPlatform (platform);
Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers);

var configuration = "Release";
var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out _, configuration: configuration);
Clean (project_path);

var properties = GetDefaultProperties (runtimeIdentifiers);
properties ["Configuration"] = configuration;
properties ["UseMonoRuntime"] = "false";

var rv = DotNet.AssertBuild (project_path, properties);

var allTargets = BinLog.GetAllTargets (rv.BinLogPath);
AssertTargetNotExecuted (allTargets, "_ConfigureCoreLibOnlyCompositeRoots", "Release CoreCLR build must keep full per-module composite");
}

void AssertCoreLibOnlyBundleComposition (ApplePlatform platform, string appPath, string applicationName)
{
Assert.That (appPath, Does.Exist, "App bundle directory");

// iOS/tvOS package the per-app composite as a <App>.r2r.framework under
// Frameworks/; Mac Catalyst packages it as a <App>.r2r.dylib under
// Contents/MonoBundle/ (see _CreateR2RModuleFrameworks vs
// _CreateR2RModuleDylibs in Microsoft.Sdk.R2R.targets, and the
// <Platform>-CoreCLR-R2R-size.txt baselines).
switch (platform) {
case ApplePlatform.iOS:
case ApplePlatform.TVOS:
var frameworksDir = Path.Combine (appPath, GetFrameworksRelativePath (platform));
Assert.That (frameworksDir, Does.Exist, "Frameworks directory");

var r2rFrameworks = Directory.GetDirectories (frameworksDir, "*.r2r.framework");
Assert.That (r2rFrameworks.Length, Is.EqualTo (1),
$"Expected exactly one .r2r.framework (the per-app composite), found:\n {string.Join ("\n ", r2rFrameworks)}");

var perAppFramework = applicationName + ".r2r.framework";
Assert.That (Path.GetFileName (r2rFrameworks [0]), Is.EqualTo (perAppFramework),
$"The single .r2r.framework should be the per-app composite ({perAppFramework})");

// The upstream Microsoft.NETCore.App.r2r.framework must not be carried
// into the bundle: every non-CoreLib BCL assembly was re-headered to
// point at the per-app composite instead.
var leakedNetCoreFrameworks = Directory.GetDirectories (frameworksDir, "Microsoft.NETCore.App.r2r*");
Assert.That (leakedNetCoreFrameworks, Is.Empty,
$"Bundle must not contain upstream Microsoft.NETCore.App.r2r framework(s):\n {string.Join ("\n ", leakedNetCoreFrameworks)}");

// On iOS/tvOS the composite ships as a framework whose binary is
// <App>.r2r (no extension), so no *.r2r.dylib should be present.
var leakedR2RDylibs = Directory.GetFiles (appPath, "*.r2r.dylib", SearchOption.AllDirectories);
Assert.That (leakedR2RDylibs, Is.Empty,
$"Bundle must not contain any *.r2r.dylib files on iOS/tvOS:\n {string.Join ("\n ", leakedR2RDylibs)}");
break;
case ApplePlatform.MacCatalyst:
var monoBundleDir = Path.Combine (appPath, "Contents", "MonoBundle");
Assert.That (monoBundleDir, Does.Exist, "MonoBundle directory");

var r2rDylibs = Directory.GetFiles (monoBundleDir, "*.r2r.dylib");
Assert.That (r2rDylibs.Length, Is.EqualTo (1),
$"Expected exactly one .r2r.dylib (the per-app composite), found:\n {string.Join ("\n ", r2rDylibs)}");

var perAppDylib = applicationName + ".r2r.dylib";
Assert.That (Path.GetFileName (r2rDylibs [0]), Is.EqualTo (perAppDylib),
$"The single .r2r.dylib should be the per-app composite ({perAppDylib})");

// The upstream Microsoft.NETCore.App.r2r.dylib must not be carried into
// the bundle: every non-CoreLib BCL assembly was re-headered to point at
// the per-app composite instead.
var leakedNetCoreDylibs = Directory.GetFiles (monoBundleDir, "Microsoft.NETCore.App.r2r*");
Assert.That (leakedNetCoreDylibs, Is.Empty,
$"Bundle must not contain upstream Microsoft.NETCore.App.r2r dylib(s):\n {string.Join ("\n ", leakedNetCoreDylibs)}");
break;
default:
throw new ArgumentOutOfRangeException (nameof (platform), platform, "Unsupported platform for CoreLib-only R2R bundle composition");
}
}
}
}
Loading