diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 763083c329b0..a0cd06a1fa02 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)%(Extension).framework/%(Filename)%(Extension)')" 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)%(Extension).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 + %(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) @@ -2945,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'" /> diff --git a/tests/dotnet/UnitTests/AppSizeTest.cs b/tests/dotnet/UnitTests/AppSizeTest.cs index d39882216e49..2a00bec887e7 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); + AssertExpectedDSyms (platform, appPath); }); } diff --git a/tests/dotnet/UnitTests/PostBuildTest.cs b/tests/dotnet/UnitTests/PostBuildTest.cs index 197f6bd3de0e..eed55358e45b 100644 --- a/tests/dotnet/UnitTests/PostBuildTest.cs +++ b/tests/dotnet/UnitTests/PostBuildTest.cs @@ -233,5 +233,126 @@ 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"); + + // 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"); + Assert.That (dSymDirs, Is.Empty, "No dSYMs should exist for Debug builds"); + } + + [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"); + + // 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"); + Assert.That (dSymDirs, Is.Empty, "No dSYMs should exist for Debug builds"); + } + + [Test] + [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"; + // 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); + + AssertExpectedDSyms (platform, appPath); + } + + 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 (); + } } } 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); 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);