From cee6656544ebdcb214b57d90a6a4c33aaff95ad0 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Fri, 13 Feb 2026 08:55:21 +0100 Subject: [PATCH 1/9] [targets] Improve/fix post-processing item collection for dylibs and frameworks Fix several issues in the _CollectItemsForPostProcessing target: * Fix broken MSBuild syntax: '$(AppBundleExtension/' was missing the closing ')' for the property expression. * Fix broken condition: '%(_ResolvedNativeReference.Kind' was missing the closing ')'. * Use unqualified %(Kind) metadata in conditions, because %(ItemType.Metadata) doesn't work in Condition attributes on items created with @(Item->'...') transforms. * Change dylib source from _ResolvedNativeReference (which only contains frameworks) to _FileNativeReference (which contains dylibs). * Add _AppContentsRelativePathForPostProcessing property with trailing '/' separator to fix path concatenation for macOS (where _AppContentsRelativePath has no trailing separator). * Set DSymName directly on framework and dylib items to avoid MSB4096 batching error when items without Kind metadata (the executable) are in the same item group. * Add tests for dylib and framework post-processing items on iOS and macOS. Improvements: * Make it easier to plug into this mechanism for other code to add items for post processing. * Fix an issue where dynamic native references were collected as if they were frameworks. * Simplify some defaults by computing them globally. * Add tests. --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 35 ++++++--- tests/dotnet/UnitTests/AppSizeTest.cs | 52 +++++++++++++ tests/dotnet/UnitTests/PostBuildTest.cs | 76 +++++++++++++++++++ 3 files changed, 154 insertions(+), 9 deletions(-) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 763083c329b0..04d2f9b0a3ba 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2882,9 +2882,18 @@ Copyright (C) 2018 Microsoft. All rights reserved. This target runs always, because container projects might want to create debug symbols or strip even if this project doesn't want to, and in that case the container project would still need to know what to do for contained projects. --> + + <_CollectItemsForPostProcessingDependsOn> + _CompileToNative; + _ParseBundlerArguments; + _ExpandNativeReferences; + _PrepareForPostProcessing; + $(_CollectItemsForPostProcessingDependsOn); + + @@ -2908,24 +2917,28 @@ Copyright (C) 2018 Microsoft. All rights reserved. + + <_AppContentsRelativePathForPostProcessing Condition="'$(_AppContentsRelativePath)' != ''">$(_AppContentsRelativePath)/ + + - <_PostProcessingItem Include="@(_ResolvedNativeReference->'$(_AppBundleName)$(AppBundleExtension/$(_AppFrameworksRelativePath)%(Filename).framework/%(Filename)')" Condition="'%(_ResolvedNativeReference.Kind' == 'Framework'"> + <_PostProcessingItem Include="@(_ResolvedNativeReference->'$(_AppBundleName)$(AppBundleExtension)/$(_AppFrameworksRelativePath)%(Filename).framework/%(Filename)')" Condition="'%(Kind)' == 'Framework'"> - %(_ResolvedNativeReference.Identity) + %(Identity) - $([System.IO.Path]::GetDirectoryName('%(_ResolvedNativeReference.Identity)')).dSYM + $([System.IO.Path]::GetDirectoryName('%(Identity)')).dSYM - %(_ResolvedNativeReference.Filename)%(_ResolvedNativeReference.Extension).dSYM + %(Filename).framework.dSYM - <_PostProcessingItem Include="@(_ResolvedNativeReference->'$(_AppBundleName)$(AppBundleExtension/$(_AppContentsRelativePath)%(Filename).framework/%(Filename)')" Condition="'%(_ResolvedNativeReference.Kind' == 'Dynamic'"> + <_PostProcessingItem Include="@(_FileNativeReference->'$(_AppBundleName)$(AppBundleExtension)/$(_AppContentsRelativePathForPostProcessing)%(Filename)%(Extension)')" Condition="'%(Kind)' == 'Dynamic'"> - %(_ResolvedNativeReference.Identity) + %(Identity) - $([System.IO.Path]::GetDirectoryName('%(_ResolvedNativeReference.Identity)')).dSYM + $([System.IO.Path]::GetDirectoryName('%(Identity)')).dSYM - %(_ResolvedNativeReference.Filename)%(_ResolvedNativeReference.Extension).dSYM + %(Filename).dSYM <_PostProcessingItem Include="$([System.IO.Path]::GetFileName('$(AppBundleDir)'))/$(_NativeExecutableRelativePath)" Condition="'$(IsWatchApp)' != 'true'"> $(_SymbolsListFullPath) @@ -2934,7 +2947,11 @@ Copyright (C) 2018 Microsoft. All rights reserved. $(IsAppExtension) + <_PostProcessingItem> + + %(Filename).dSYM + $(NoSymbolStrip) $(NoDSymUtil) diff --git a/tests/dotnet/UnitTests/AppSizeTest.cs b/tests/dotnet/UnitTests/AppSizeTest.cs index d39882216e49..952cde08d28e 100644 --- a/tests/dotnet/UnitTests/AppSizeTest.cs +++ b/tests/dotnet/UnitTests/AppSizeTest.cs @@ -66,6 +66,7 @@ void Run (ApplePlatform platform, string runtimeIdentifiers, string configuratio if (supportsAssemblyInspection) AssertAssemblyReport (platform, name, appPath, update, expectedDirectory); + AssertDSyms (platform, appPath); }); } @@ -182,6 +183,57 @@ static string FormatBytes (long bytes, bool alwaysShowSign = false) { return $"{(alwaysShowSign && bytes > 0 ? "+" : "")}{bytes:N0} bytes ({bytes / 1024.0:N1} KB = {bytes / (1024.0 * 1024.0):N1} MB)"; } + + // Assert that the expected dSYMs exist for all binaries in the app bundle, and that no unexpected dSYMs exist. + void AssertDSyms (ApplePlatform platform, string appPath) + { + var appContainerDir = Path.GetDirectoryName (appPath)!; + var appBundleName = Path.GetFileName (appPath); + + // Collect expected dSYM names based on the binaries in the app bundle + var expectedDSyms = new HashSet (); + + // The app bundle itself should have a dSYM + expectedDSyms.Add (appBundleName + ".dSYM"); + + // Find frameworks in the app bundle (Frameworks/ on iOS, Contents/Frameworks/ on macOS) + var frameworksRelativeDir = GetFrameworksRelativePath (platform); + var frameworksDir = Path.Combine (appPath, frameworksRelativeDir); + if (Directory.Exists (frameworksDir)) { + foreach (var frameworkDir in Directory.GetDirectories (frameworksDir, "*.framework")) { + var frameworkName = Path.GetFileNameWithoutExtension (frameworkDir); + var frameworkBinary = Path.Combine (frameworkDir, frameworkName); + if (File.Exists (frameworkBinary)) + expectedDSyms.Add (frameworkName + ".framework.dSYM"); + } + } + + // Find dylibs in the app bundle (root on iOS, Contents/MonoBundle/ on macOS) + var contentsRelativeDir = GetRelativeDylibDirectory (platform); + var contentsDir = string.IsNullOrEmpty (contentsRelativeDir) ? appPath : Path.Combine (appPath, contentsRelativeDir); + if (Directory.Exists (contentsDir)) { + foreach (var dylib in Directory.GetFiles (contentsDir, "*.dylib")) { + var fileName = Path.GetFileNameWithoutExtension (dylib); + expectedDSyms.Add (fileName + ".dSYM"); + } + } + + // Find actual dSYM directories + var actualDSyms = Directory.GetDirectories (appContainerDir, "*.dSYM") + .Select (d => Path.GetFileName (d)) + .ToHashSet (); + + var missingDSyms = expectedDSyms.Except (actualDSyms).OrderBy (v => v).ToList (); + var unexpectedDSyms = actualDSyms.Except (expectedDSyms).OrderBy (v => v).ToList (); + + if (missingDSyms.Count > 0) + Console.WriteLine ($" Missing dSYMs:\n {string.Join ("\n ", missingDSyms)}"); + if (unexpectedDSyms.Count > 0) + Console.WriteLine ($" Unexpected dSYMs:\n {string.Join ("\n ", unexpectedDSyms)}"); + + Assert.That (missingDSyms, Is.Empty, "Missing dSYMs"); + Assert.That (unexpectedDSyms, Is.Empty, "Unexpected dSYMs"); + } } static class StringExtensions { diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index 197f6bd3de0e..1edda4c4db71 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -233,5 +233,81 @@ public void PublishFailureTest (ApplePlatform platform, string runtimeIdentifier Assert.That (pkgPath, Does.Not.Exist, "ipa/pkg creation"); } + + [Test] + [TestCase (ApplePlatform.iOS, "iossimulator-arm64")] + [TestCase (ApplePlatform.MacOSX, "osx-arm64")] + public void DylibPostProcessingItems (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "NativeDynamicLibraryReferencesApp"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers, platform, out var appPath); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + + var result = DotNet.AssertBuild (project_path, properties); + var postProcessingItems = GetPostProcessingItems (result.BinLogPath); + + // Find the user's dylib item (not SDK runtime dylibs) + var dylibItems = postProcessingItems.Where (i => i.ItemSpec.Contains ("libframework.dylib")).ToList (); + Assert.That (dylibItems.Count, Is.EqualTo (1), $"Expected 1 libframework.dylib post-processing item, got {dylibItems.Count}. All items:\n\t{string.Join ("\n\t", postProcessingItems.Select (i => i.ItemSpec))}"); + var dylibItem = dylibItems [0]; + + // Verify the path does NOT contain ".framework/" (the bug was that dylibs were treated as frameworks) + Assert.That (dylibItem.ItemSpec, Does.Not.Contain (".framework/"), "Dylib path should not contain .framework/"); + + // Verify the path contains the full dylib filename + Assert.That (dylibItem.ItemSpec, Does.Contain ("libframework.dylib"), "Dylib path should contain the full dylib filename"); + + // Verify the DSymName is correct for a dylib (should be "libframework.dSYM", not "libframework.dylib.dSYM") + var dSymName = dylibItem.GetMetadata ("DSymName"); + Assert.That (dSymName, Is.EqualTo ("libframework.dSYM"), "DSymName for dylib"); + } + + [Test] + [TestCase (ApplePlatform.iOS, "iossimulator-x64")] + [TestCase (ApplePlatform.MacOSX, "osx-arm64")] + public void FrameworkPostProcessingItems (ApplePlatform platform, string runtimeIdentifiers) + { + var project = "NativeFrameworkReferencesApp"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + var project_path = GetProjectPath (project, runtimeIdentifiers, platform, out var appPath); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifiers); + + var result = DotNet.AssertBuild (project_path, properties); + var postProcessingItems = GetPostProcessingItems (result.BinLogPath); + + // Find the framework item (XTest.framework is the dynamic framework) + var frameworkItems = postProcessingItems.Where (i => i.ItemSpec.Contains ("XTest.framework/XTest")).ToList (); + Assert.That (frameworkItems.Count, Is.EqualTo (1), $"Expected 1 XTest framework post-processing item, got {frameworkItems.Count}. All items:\n\t{string.Join ("\n\t", postProcessingItems.Select (i => i.ItemSpec))}"); + var frameworkItem = frameworkItems [0]; + + // Verify the DSymName is correct for a framework (should be "XTest.framework.dSYM") + var dSymName = frameworkItem.GetMetadata ("DSymName"); + Assert.That (dSymName, Is.EqualTo ("XTest.framework.dSYM"), "DSymName for framework"); + } + + static List GetPostProcessingItems (string binLogPath) + { + var items = new Dictionary (); + foreach (var args in BinLog.ReadBuildEvents (binLogPath)) { + if (args is not TaskParameterEventArgs tpea) + continue; + if (tpea.Kind != TaskParameterMessageKind.AddItem) + continue; + if (tpea.ItemType != "_PostProcessingItem") + continue; + foreach (var item in tpea.Items) { + if (item is ITaskItem taskItem) + items [taskItem.ItemSpec] = taskItem; + } + } + return items.Values.ToList (); + } } } From ef05a4557a5227bf0cac096aaad7dca82aa0d14b Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Mon, 16 Feb 2026 17:27:57 +0100 Subject: [PATCH 2/9] [tests] Add dSYM on-disk validation to post-processing tests Move AssertExpectedDSyms to TestBaseClass for reuse across test classes. Add dSYM verification to DylibPostProcessingItems and FrameworkPostProcessingItems tests: since these are Debug builds, verify that no dSYM directories are generated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/dotnet/UnitTests/AppSizeTest.cs | 53 +------------------------ tests/dotnet/UnitTests/PostBuildTest.cs | 10 +++++ tests/dotnet/UnitTests/TestBaseClass.cs | 50 +++++++++++++++++++++++ 3 files changed, 61 insertions(+), 52 deletions(-) diff --git a/tests/dotnet/UnitTests/AppSizeTest.cs b/tests/dotnet/UnitTests/AppSizeTest.cs index 952cde08d28e..2a00bec887e7 100644 --- a/tests/dotnet/UnitTests/AppSizeTest.cs +++ b/tests/dotnet/UnitTests/AppSizeTest.cs @@ -66,7 +66,7 @@ void Run (ApplePlatform platform, string runtimeIdentifiers, string configuratio if (supportsAssemblyInspection) AssertAssemblyReport (platform, name, appPath, update, expectedDirectory); - AssertDSyms (platform, appPath); + AssertExpectedDSyms (platform, appPath); }); } @@ -183,57 +183,6 @@ static string FormatBytes (long bytes, bool alwaysShowSign = false) { return $"{(alwaysShowSign && bytes > 0 ? "+" : "")}{bytes:N0} bytes ({bytes / 1024.0:N1} KB = {bytes / (1024.0 * 1024.0):N1} MB)"; } - - // Assert that the expected dSYMs exist for all binaries in the app bundle, and that no unexpected dSYMs exist. - void AssertDSyms (ApplePlatform platform, string appPath) - { - var appContainerDir = Path.GetDirectoryName (appPath)!; - var appBundleName = Path.GetFileName (appPath); - - // Collect expected dSYM names based on the binaries in the app bundle - var expectedDSyms = new HashSet (); - - // The app bundle itself should have a dSYM - expectedDSyms.Add (appBundleName + ".dSYM"); - - // Find frameworks in the app bundle (Frameworks/ on iOS, Contents/Frameworks/ on macOS) - var frameworksRelativeDir = GetFrameworksRelativePath (platform); - var frameworksDir = Path.Combine (appPath, frameworksRelativeDir); - if (Directory.Exists (frameworksDir)) { - foreach (var frameworkDir in Directory.GetDirectories (frameworksDir, "*.framework")) { - var frameworkName = Path.GetFileNameWithoutExtension (frameworkDir); - var frameworkBinary = Path.Combine (frameworkDir, frameworkName); - if (File.Exists (frameworkBinary)) - expectedDSyms.Add (frameworkName + ".framework.dSYM"); - } - } - - // Find dylibs in the app bundle (root on iOS, Contents/MonoBundle/ on macOS) - var contentsRelativeDir = GetRelativeDylibDirectory (platform); - var contentsDir = string.IsNullOrEmpty (contentsRelativeDir) ? appPath : Path.Combine (appPath, contentsRelativeDir); - if (Directory.Exists (contentsDir)) { - foreach (var dylib in Directory.GetFiles (contentsDir, "*.dylib")) { - var fileName = Path.GetFileNameWithoutExtension (dylib); - expectedDSyms.Add (fileName + ".dSYM"); - } - } - - // Find actual dSYM directories - var actualDSyms = Directory.GetDirectories (appContainerDir, "*.dSYM") - .Select (d => Path.GetFileName (d)) - .ToHashSet (); - - var missingDSyms = expectedDSyms.Except (actualDSyms).OrderBy (v => v).ToList (); - var unexpectedDSyms = actualDSyms.Except (expectedDSyms).OrderBy (v => v).ToList (); - - if (missingDSyms.Count > 0) - Console.WriteLine ($" Missing dSYMs:\n {string.Join ("\n ", missingDSyms)}"); - if (unexpectedDSyms.Count > 0) - Console.WriteLine ($" Unexpected dSYMs:\n {string.Join ("\n ", unexpectedDSyms)}"); - - Assert.That (missingDSyms, Is.Empty, "Missing dSYMs"); - Assert.That (unexpectedDSyms, Is.Empty, "Unexpected dSYMs"); - } } static class StringExtensions { diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index 1edda4c4db71..d6fa9765b0e3 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -264,6 +264,11 @@ public void DylibPostProcessingItems (ApplePlatform platform, string runtimeIden // Verify the DSymName is correct for a dylib (should be "libframework.dSYM", not "libframework.dylib.dSYM") var dSymName = dylibItem.GetMetadata ("DSymName"); Assert.That (dSymName, Is.EqualTo ("libframework.dSYM"), "DSymName for dylib"); + + // Debug builds don't generate dSYMs, verify none exist + var appContainerDir = Path.GetDirectoryName (appPath)!; + var dSymDirs = Directory.GetDirectories (appContainerDir, "*.dSYM"); + Assert.That (dSymDirs, Is.Empty, "No dSYMs should exist for Debug builds"); } [Test] @@ -290,6 +295,11 @@ public void FrameworkPostProcessingItems (ApplePlatform platform, string runtime // Verify the DSymName is correct for a framework (should be "XTest.framework.dSYM") var dSymName = frameworkItem.GetMetadata ("DSymName"); Assert.That (dSymName, Is.EqualTo ("XTest.framework.dSYM"), "DSymName for framework"); + + // Debug builds don't generate dSYMs, verify none exist + var appContainerDir = Path.GetDirectoryName (appPath)!; + var dSymDirs = Directory.GetDirectories (appContainerDir, "*.dSYM"); + Assert.That (dSymDirs, Is.Empty, "No dSYMs should exist for Debug builds"); } static List GetPostProcessingItems (string binLogPath) diff --git a/tests/dotnet/UnitTests/TestBaseClass.cs b/tests/dotnet/UnitTests/TestBaseClass.cs index 6b35686da384..19e3511ada80 100644 --- a/tests/dotnet/UnitTests/TestBaseClass.cs +++ b/tests/dotnet/UnitTests/TestBaseClass.cs @@ -307,6 +307,56 @@ protected void AssertDSymDirectory (string appPath) Assert.That (dSYMDirectory, Does.Exist, "dsym directory"); } + // Assert that the expected dSYMs exist for all binaries in the app bundle, and that no unexpected dSYMs exist. + protected void AssertExpectedDSyms (ApplePlatform platform, string appPath) + { + var appContainerDir = Path.GetDirectoryName (appPath)!; + var appBundleName = Path.GetFileName (appPath); + + // Collect expected dSYM names based on the binaries in the app bundle + var expectedDSyms = new HashSet (); + + // The app bundle itself should have a dSYM + expectedDSyms.Add (appBundleName + ".dSYM"); + + // Find frameworks in the app bundle + var frameworksDir = Path.Combine (appPath, GetFrameworksRelativePath (platform)); + if (Directory.Exists (frameworksDir)) { + foreach (var frameworkDir in Directory.GetDirectories (frameworksDir, "*.framework")) { + var frameworkName = Path.GetFileNameWithoutExtension (frameworkDir); + var frameworkBinary = Path.Combine (frameworkDir, frameworkName); + if (File.Exists (frameworkBinary)) + expectedDSyms.Add (frameworkName + ".framework.dSYM"); + } + } + + // Find dylibs in the app bundle + var contentsRelativeDir = GetRelativeDylibDirectory (platform); + var contentsDir = string.IsNullOrEmpty (contentsRelativeDir) ? appPath : Path.Combine (appPath, contentsRelativeDir); + if (Directory.Exists (contentsDir)) { + foreach (var dylib in Directory.GetFiles (contentsDir, "*.dylib")) { + var fileName = Path.GetFileNameWithoutExtension (dylib); + expectedDSyms.Add (fileName + ".dSYM"); + } + } + + // Find actual dSYM directories + var actualDSyms = Directory.GetDirectories (appContainerDir, "*.dSYM") + .Select (d => Path.GetFileName (d)) + .ToHashSet (); + + var missingDSyms = expectedDSyms.Except (actualDSyms).OrderBy (v => v).ToList (); + var unexpectedDSyms = actualDSyms.Except (expectedDSyms).OrderBy (v => v).ToList (); + + if (missingDSyms.Count > 0) + Console.WriteLine ($" Missing dSYMs:\n {string.Join ("\n ", missingDSyms)}"); + if (unexpectedDSyms.Count > 0) + Console.WriteLine ($" Unexpected dSYMs:\n {string.Join ("\n ", unexpectedDSyms)}"); + + Assert.That (missingDSyms, Is.Empty, "Missing dSYMs"); + Assert.That (unexpectedDSyms, Is.Empty, "Unexpected dSYMs"); + } + protected static string GetNativeExecutable (ApplePlatform platform, string app_directory) { var executableName = Path.GetFileNameWithoutExtension (app_directory); From 3409dbb2fa8eaef3ab1c064d181d5348a9d455d8 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 17 Feb 2026 10:47:20 +0100 Subject: [PATCH 3/9] [tests] Add ignored test for Framework.With.Dots.framework dSYM issue The BundleStructure project includes Framework.With.Dots.framework, which causes dsymutil to fail because the framework binary name is truncated. This prevents dSYMs from being generated for all items in the app bundle. Add an ignored test to make it easy to reproduce the problem. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/dotnet/UnitTests/PostBuildTest.cs | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index d6fa9765b0e3..6639e853a570 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -302,6 +302,32 @@ public void FrameworkPostProcessingItems (ApplePlatform platform, string runtime Assert.That (dSymDirs, Is.Empty, "No dSYMs should exist for Debug builds"); } + // The BundleStructure project includes Framework.With.Dots.framework, which causes dsymutil + // to fail because the framework binary name ("Framework.With") doesn't match what dsymutil + // expects. This makes the _GenerateDSym target fail, preventing dSYMs from being generated + // for all items in the app bundle. + // This test is ignored until the Framework.With.Dots issue is resolved. + [Test] + [Ignore ("dsymutil fails on Framework.With.Dots.framework - the framework binary name is truncated")] + [TestCase (ApplePlatform.iOS, "ios-arm64", "Release")] + [TestCase (ApplePlatform.MacOSX, "osx-arm64", "Release")] + public void BundleStructureDSyms (ApplePlatform platform, string runtimeIdentifiers, string configuration) + { + var project = "BundleStructure"; + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers); + + 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 ["_IsAppSigned"] = "true"; + + DotNet.AssertBuild (project_path, properties); + + AssertExpectedDSyms (platform, appPath); + } + static List GetPostProcessingItems (string binLogPath) { var items = new Dictionary (); From 75b80537f79e9aa662e1858e84a43b78e287f285 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 17 Feb 2026 22:12:49 +0100 Subject: [PATCH 4/9] Update msbuild/Xamarin.Shared/Xamarin.Shared.targets Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 04d2f9b0a3ba..bb86c272c621 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2947,7 +2947,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. $(IsAppExtension) - + <_PostProcessingItem> %(Filename).dSYM From 7dbe9e61efffc259afc24571c3083fa380850371 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 17 Feb 2026 22:34:50 +0100 Subject: [PATCH 5/9] [msbuild] Fix dSYMSourcePath for dylibs in _CollectItemsForPostProcessing The dSYMSourcePath for dylibs was computed using GetDirectoryName, which returns the parent directory. For a dylib at /path/to/libfoo.dylib, this resulted in /path/to.dSYM instead of the correct /path/to/libfoo.dylib.dSYM. Fix by using %(Identity).dSYM directly for dylibs, and add dSYMSourcePath validation to both the DylibPostProcessingItems and FrameworkPostProcessingItems tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 2 +- tests/dotnet/UnitTests/PostBuildTest.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index bb86c272c621..6b3209f0a1ad 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2936,7 +2936,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. %(Identity) - $([System.IO.Path]::GetDirectoryName('%(Identity)')).dSYM + %(Identity).dSYM %(Filename).dSYM diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index 6639e853a570..4c9d9dfbdfa2 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -265,6 +265,12 @@ public void DylibPostProcessingItems (ApplePlatform platform, string runtimeIden var dSymName = dylibItem.GetMetadata ("DSymName"); Assert.That (dSymName, Is.EqualTo ("libframework.dSYM"), "DSymName for dylib"); + // Verify dSYMSourcePath points to where a pre-existing dSYM would be for the dylib. + // For a dylib at /path/to/libfoo.dylib, the dSYMSourcePath should be /path/to/libfoo.dylib.dSYM + var dSYMSourcePath = dylibItem.GetMetadata ("dSYMSourcePath"); + var itemSourcePath = dylibItem.GetMetadata ("ItemSourcePath"); + Assert.That (dSYMSourcePath, Is.EqualTo (itemSourcePath + ".dSYM"), "dSYMSourcePath for dylib"); + // Debug builds don't generate dSYMs, verify none exist var appContainerDir = Path.GetDirectoryName (appPath)!; var dSymDirs = Directory.GetDirectories (appContainerDir, "*.dSYM"); @@ -296,6 +302,12 @@ public void FrameworkPostProcessingItems (ApplePlatform platform, string runtime var dSymName = frameworkItem.GetMetadata ("DSymName"); Assert.That (dSymName, Is.EqualTo ("XTest.framework.dSYM"), "DSymName for framework"); + // Verify dSYMSourcePath points to where a pre-existing dSYM would be for the framework. + // For a framework at /path/to/XTest.framework/XTest, the dSYMSourcePath should be /path/to/XTest.framework.dSYM + var dSYMSourcePath = frameworkItem.GetMetadata ("dSYMSourcePath"); + var itemSourcePath = frameworkItem.GetMetadata ("ItemSourcePath"); + Assert.That (dSYMSourcePath, Is.EqualTo (Path.GetDirectoryName (itemSourcePath) + ".dSYM"), "dSYMSourcePath for framework"); + // Debug builds don't generate dSYMs, verify none exist var appContainerDir = Path.GetDirectoryName (appPath)!; var dSymDirs = Directory.GetDirectories (appContainerDir, "*.dSYM"); From aa741b193c915573fba231ab4fa81e0e15f89862 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 18 Feb 2026 12:04:21 +0100 Subject: [PATCH 6/9] [msbuild] Fix framework name computation for names with dots Use %(Filename)%(Extension) instead of %(Filename) when computing the framework binary path in _CollectItemsForPostProcessing. MSBuild's %(Filename) strips the last extension, so for Framework.With.Dots it returns Framework.With instead of Framework.With.Dots. This fixes the BundleStructureTest.Build test failure, and enables the BundleStructureDSyms test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 4 ++-- tests/dotnet/UnitTests/PostBuildTest.cs | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 6b3209f0a1ad..4bb3089fd9d9 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2923,13 +2923,13 @@ Copyright (C) 2018 Microsoft. All rights reserved. - <_PostProcessingItem Include="@(_ResolvedNativeReference->'$(_AppBundleName)$(AppBundleExtension)/$(_AppFrameworksRelativePath)%(Filename).framework/%(Filename)')" Condition="'%(Kind)' == 'Framework'"> + <_PostProcessingItem Include="@(_ResolvedNativeReference->'$(_AppBundleName)$(AppBundleExtension)/$(_AppFrameworksRelativePath)%(Filename)%(Extension).framework/%(Filename)%(Extension)')" Condition="'%(Kind)' == 'Framework'"> %(Identity) $([System.IO.Path]::GetDirectoryName('%(Identity)')).dSYM - %(Filename).framework.dSYM + %(Filename)%(Extension).framework.dSYM <_PostProcessingItem Include="@(_FileNativeReference->'$(_AppBundleName)$(AppBundleExtension)/$(_AppContentsRelativePathForPostProcessing)%(Filename)%(Extension)')" Condition="'%(Kind)' == 'Dynamic'"> diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index 4c9d9dfbdfa2..eed55358e45b 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -314,13 +314,7 @@ public void FrameworkPostProcessingItems (ApplePlatform platform, string runtime Assert.That (dSymDirs, Is.Empty, "No dSYMs should exist for Debug builds"); } - // The BundleStructure project includes Framework.With.Dots.framework, which causes dsymutil - // to fail because the framework binary name ("Framework.With") doesn't match what dsymutil - // expects. This makes the _GenerateDSym target fail, preventing dSYMs from being generated - // for all items in the app bundle. - // This test is ignored until the Framework.With.Dots issue is resolved. [Test] - [Ignore ("dsymutil fails on Framework.With.Dots.framework - the framework binary name is truncated")] [TestCase (ApplePlatform.iOS, "ios-arm64", "Release")] [TestCase (ApplePlatform.MacOSX, "osx-arm64", "Release")] public void BundleStructureDSyms (ApplePlatform platform, string runtimeIdentifiers, string configuration) @@ -334,6 +328,9 @@ public void BundleStructureDSyms (ApplePlatform platform, string runtimeIdentifi var properties = GetDefaultProperties (runtimeIdentifiers); properties ["Configuration"] = configuration; properties ["_IsAppSigned"] = "true"; + // macOS and Mac Catalyst default to NoDSymUtil=true (dSYMs only generated when archiving), + // so explicitly disable it to test dSYM generation. + properties ["NoDSymUtil"] = "false"; DotNet.AssertBuild (project_path, properties); From 7f4e270a3b4f2f5d359aa3086273dfa00b6f6915 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 18 Feb 2026 21:18:44 +0100 Subject: [PATCH 7/9] [msbuild] Don't strip dylibs from the .NET runtime The .NET runtime dylibs (from Microsoft.NETCore.App.Runtime.* NuGet packages) may contain symbols referenced by indirect symbol table entries that can't be stripped. Set NoSymbolStrip=true for these items in _CollectItemsForPostProcessing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 4bb3089fd9d9..25771ade96d2 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2962,6 +2962,11 @@ Copyright (C) 2018 Microsoft. All rights reserved. %(Filename)%(Extension).bcsymbolmap + + <_PostProcessingItem Condition="$([MSBuild]::ValueOrDefault('%(_PostProcessingItem.NuGetPackageId)', '').StartsWith('Microsoft.NETCore.App.Runtime'))"> + true + + <_PostProcessingAppExtensions Include="@(_AppExtensionPostProcessingItems)" Condition="'%(_AppExtensionPostProcessingItems.IsAppExtension)' == 'true' And '%(_AppExtensionPostProcessingItems.IsXPCService)' != 'true'" /> <_PostProcessingXpcServices Include="@(_AppExtensionPostProcessingItems)" Condition="'%(_AppExtensionPostProcessingItems.IsAppExtension)' == 'true' And '%(_AppExtensionPostProcessingItems.IsXPCService)' == 'true'" /> From ad898b6088515b818c135ae611df9cfe020be6ea Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Thu, 19 Feb 2026 11:44:17 +0100 Subject: [PATCH 8/9] [tests] Bump max relative path length from 110 to 118 Frameworks are now properly collected for post-processing (strip), which causes their binaries to be synced back to the Windows bin directory during remote builds. The FrameworksInRuntimesNativeDirectory test frameworks have paths of 118 characters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/dotnet/UnitTests/WindowsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dotnet/UnitTests/WindowsTest.cs b/tests/dotnet/UnitTests/WindowsTest.cs index 69a16f7e8af8..0599eae94308 100644 --- a/tests/dotnet/UnitTests/WindowsTest.cs +++ b/tests/dotnet/UnitTests/WindowsTest.cs @@ -60,7 +60,7 @@ class FileData { public required string RelativePath; } - void AssertMaxFileLengthInBinAndObjDirectories (ApplePlatform platform, string project_path, string runtimeIdentifiers, string configuration, int maxLength = 110) + void AssertMaxFileLengthInBinAndObjDirectories (ApplePlatform platform, string project_path, string runtimeIdentifiers, string configuration, int maxLength = 118) { var binDir = GetBinDir (project_path, platform, runtimeIdentifiers, configuration); var objDir = GetObjDir (project_path, platform, runtimeIdentifiers, configuration); From 14cd628b4a47121d26fe6fbbd48ac848cb7c3a8d Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Tue, 24 Feb 2026 20:15:29 +0100 Subject: [PATCH 9/9] Add link to runtime issue. --- msbuild/Xamarin.Shared/Xamarin.Shared.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 25771ade96d2..a0cd06a1fa02 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -2962,7 +2962,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. %(Filename)%(Extension).bcsymbolmap - + <_PostProcessingItem Condition="$([MSBuild]::ValueOrDefault('%(_PostProcessingItem.NuGetPackageId)', '').StartsWith('Microsoft.NETCore.App.Runtime'))"> true