diff --git a/docs/building-apps/build-properties.md b/docs/building-apps/build-properties.md index 6089dc2ed05c..0469fca47afe 100644 --- a/docs/building-apps/build-properties.md +++ b/docs/building-apps/build-properties.md @@ -1702,6 +1702,25 @@ The full path to the `strip` command-line tool. The default behavior is to use `xcrun strip`. +## StripMergeableLibraries + +A boolean property that specifies whether static linking metadata (`LC_ATOM_INFO`) +is removed from mergeable libraries embedded in the app bundle. + +Mergeable libraries are dynamic libraries that also contain metadata for static +linking. This metadata can roughly double the size of the library. When this +property is `true`, the metadata is stripped to reduce app size. + +The default value is the value of the `Optimize` property, which means `Release` +builds strip mergeable library metadata by default, while `Debug` builds preserve +it. + +```xml + + true + +``` + ## SupportedOSPlatformVersion Specifies the minimum OS version the app can run on. diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/StripMergeableLibraryMetadata.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/StripMergeableLibraryMetadata.cs new file mode 100644 index 000000000000..401768632a04 --- /dev/null +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/StripMergeableLibraryMetadata.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +using Microsoft.Build.Framework; +using Xamarin.Messaging.Build.Client; + +#nullable enable + +namespace Xamarin.MacDev.Tasks { + // Strips LC_ATOM_INFO (mergeable library metadata) from frameworks and dylibs in an app bundle. + public class StripMergeableLibraryMetadata : XamarinTask, ITaskCallback { + #region Inputs + + // The Frameworks directory inside the app bundle. + public string FrameworksDirectory { get; set; } = string.Empty; + + // Additional directories to scan for mergeable dylibs (e.g. MonoBundle). + public string [] DylibDirectories { get; set; } = []; + + public string StripPath { get; set; } = string.Empty; + + public string StampDirectory { get; set; } = string.Empty; + + [Output] + public ITaskItem [] FileWrites { get; set; } = []; + + #endregion + + public override bool Execute () + { + if (ShouldExecuteRemotely ()) + return ExecuteRemotely (); + + StripFrameworks (); + StripDylibs (); + + return !Log.HasLoggedErrors; + } + + void StripFrameworks () + { + if (string.IsNullOrEmpty (FrameworksDirectory) || !Directory.Exists (FrameworksDirectory)) + return; + + foreach (var framework in Directory.GetDirectories (FrameworksDirectory, "*.framework")) { + var name = Path.GetFileNameWithoutExtension (framework); + var executable = Path.Combine (framework, name); + StripIfMergeable (executable); + } + } + + void StripDylibs () + { + if (DylibDirectories is null) + return; + + foreach (var dir in DylibDirectories) { + if (string.IsNullOrEmpty (dir) || !Directory.Exists (dir)) + continue; + + foreach (var dylib in Directory.GetFiles (dir, "*.dylib")) { + StripIfMergeable (dylib); + } + } + } + + void StripIfMergeable (string path) + { + if (!File.Exists (path)) + return; + + var stamp = GetStampPath (path); + if (stamp is not null && File.Exists (stamp) && File.GetLastWriteTimeUtc (stamp) >= File.GetLastWriteTimeUtc (path)) { + Log.LogMessage (MessageImportance.Low, $"Skipping unchanged library: {path}"); + FileWrites = FileWrites.Append (new Microsoft.Build.Utilities.TaskItem (stamp)).ToArray (); + return; + } + + if (!MachO.IsMergeableLibrary (path)) { + Log.LogMessage (MessageImportance.Low, $"Not a mergeable library: {path}"); + WriteStamp (stamp); + return; + } + + Log.LogMessage (MessageImportance.Normal, $"Stripping mergeable library metadata from: {path}"); + + var args = new List (); + var stripExecutable = GetExecutable (args, "strip", StripPath); + args.Add ("-no_atom_info"); + args.Add ("-S"); + args.Add (Path.GetFullPath (path)); + ExecuteAsync (stripExecutable, args).Wait (); + WriteStamp (stamp); + } + + string? GetStampPath (string path) + { + if (string.IsNullOrEmpty (StampDirectory)) + return null; + + using var sha = SHA256.Create (); + var hash = sha.ComputeHash (Encoding.UTF8.GetBytes (Path.GetFullPath (path))); + var name = BitConverter.ToString (hash).Replace ("-", ""); + return Path.Combine (StampDirectory, name + ".stamp"); + } + + void WriteStamp (string? stamp) + { + if (stamp is null) + return; + + Directory.CreateDirectory (StampDirectory); + File.WriteAllText (stamp, string.Empty); + FileWrites = FileWrites.Append (new Microsoft.Build.Utilities.TaskItem (stamp)).ToArray (); + } + + public bool ShouldCopyToBuildServer (ITaskItem item) => false; + + public bool ShouldCreateOutputFile (ITaskItem item) => false; + + public IEnumerable GetAdditionalItemsToBeCopied () => Enumerable.Empty (); + } +} diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/SymbolStrip.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/SymbolStrip.cs index ee1fd5cebd80..7db29ac1387d 100644 --- a/msbuild/Xamarin.MacDev.Tasks/Tasks/SymbolStrip.cs +++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/SymbolStrip.cs @@ -29,6 +29,9 @@ public class SymbolStrip : XamarinParallelTask, ITaskCallback { // This can also be specified as metadata on the Executable item (as 'Kind') public string Kind { get; set; } = string.Empty; + + // Whether to strip mergeable library metadata (LC_ATOM_INFO) from frameworks and dylibs. + public bool StripMergeableLibraries { get; set; } #endregion bool GetIsFrameworkOrDynamicLibrary (ITaskItem item) @@ -66,6 +69,11 @@ void ExecuteStrip (ITaskItem item) // Only remove debug symbols from frameworks. args.Add ("-S"); args.Add ("-x"); + if (StripMergeableLibraries) { + // Remove atom info (LC_ATOM_INFO) from mergeable libraries to reduce size. + // This is a no-op for non-mergeable libraries. + args.Add ("-no_atom_info"); + } } args.Add (Path.GetFullPath (item.ItemSpec)); diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.props b/msbuild/Xamarin.Shared/Xamarin.Shared.props index 631834a6cca7..4898eb757dc4 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.props +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.props @@ -132,6 +132,10 @@ Copyright (C) 2020 Microsoft. All rights reserved. true false + + + $(Optimize) + $(MtouchNoDSymUtil) diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets index 1bfd17c95dd6..71159415e82e 100644 --- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets +++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets @@ -100,6 +100,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. + @@ -3247,6 +3248,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. _ComputeXCFrameworkDSyms; _CopyXCFrameworkDSyms; _NativeStripFiles; + _StripMergeableLibraryMetadata; _NotifySpotlight; @@ -3395,6 +3397,7 @@ Copyright (C) 2018 Microsoft. All rights reserved. Executable="$(_AppContainerDir)%(_NativeStripItems.Identity)" Kind="%(_NativeStripItems.Kind)" MaxDegreeOfParallelism="$(SymbolStripMaxDegreeOfParallelism)" + StripMergeableLibraries="$(StripMergeableLibraries)" StripPath="$(StripPath)" SymbolFile="%(_NativeStripItems.SymbolFile)" SymbolFileLocalPath="%(_NativeStripItems.SymbolFileLocalPath)" @@ -3423,6 +3426,29 @@ Copyright (C) 2018 Microsoft. All rights reserved. /> + + + + + + + + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst + + + diff --git a/tests/dotnet/NativeMergeableDylibReferencesApp/Makefile b/tests/dotnet/NativeMergeableDylibReferencesApp/Makefile new file mode 100644 index 000000000000..6affa45ff122 --- /dev/null +++ b/tests/dotnet/NativeMergeableDylibReferencesApp/Makefile @@ -0,0 +1,2 @@ +TOP=../../.. +include $(TOP)/tests/common/shared-dotnet-test.mk diff --git a/tests/dotnet/NativeMergeableDylibReferencesApp/iOS/NativeMergeableDylibReferencesApp.csproj b/tests/dotnet/NativeMergeableDylibReferencesApp/iOS/NativeMergeableDylibReferencesApp.csproj new file mode 100644 index 000000000000..86d408734aa8 --- /dev/null +++ b/tests/dotnet/NativeMergeableDylibReferencesApp/iOS/NativeMergeableDylibReferencesApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-ios + + + diff --git a/tests/dotnet/NativeMergeableDylibReferencesApp/macOS/NativeMergeableDylibReferencesApp.csproj b/tests/dotnet/NativeMergeableDylibReferencesApp/macOS/NativeMergeableDylibReferencesApp.csproj new file mode 100644 index 000000000000..a77287b9ba00 --- /dev/null +++ b/tests/dotnet/NativeMergeableDylibReferencesApp/macOS/NativeMergeableDylibReferencesApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-macos + + + diff --git a/tests/dotnet/NativeMergeableDylibReferencesApp/shared.csproj b/tests/dotnet/NativeMergeableDylibReferencesApp/shared.csproj new file mode 100644 index 000000000000..8a1305a7bd64 --- /dev/null +++ b/tests/dotnet/NativeMergeableDylibReferencesApp/shared.csproj @@ -0,0 +1,20 @@ + + + + Exe + + NativeMergeableDylibReferencesApp + com.xamarin.nativemergeabledylibreferences + 1.0 + + + + + + + + + + + + diff --git a/tests/dotnet/NativeMergeableDylibReferencesApp/tvOS/NativeMergeableDylibReferencesApp.csproj b/tests/dotnet/NativeMergeableDylibReferencesApp/tvOS/NativeMergeableDylibReferencesApp.csproj new file mode 100644 index 000000000000..bd487ddcd88d --- /dev/null +++ b/tests/dotnet/NativeMergeableDylibReferencesApp/tvOS/NativeMergeableDylibReferencesApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/NativeMergeableFrameworkReferencesApp/AppDelegate.cs b/tests/dotnet/NativeMergeableFrameworkReferencesApp/AppDelegate.cs new file mode 100644 index 000000000000..cf40be117502 --- /dev/null +++ b/tests/dotnet/NativeMergeableFrameworkReferencesApp/AppDelegate.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; + +using Foundation; + +namespace NativeMergeableFrameworkReferencesApp { + public class Program { + [DllImport ("XMergeableTest.framework/XMergeableTest")] + static extern int theUltimateAnswer (); + + static int Main (string [] args) + { + Console.WriteLine ($"Mergeable Framework: {theUltimateAnswer ()}"); + + GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly + + Console.WriteLine (Environment.GetEnvironmentVariable ("MAGIC_WORD")); + + return 0; + } + } +} diff --git a/tests/dotnet/NativeMergeableFrameworkReferencesApp/MacCatalyst/NativeMergeableFrameworkReferencesApp.csproj b/tests/dotnet/NativeMergeableFrameworkReferencesApp/MacCatalyst/NativeMergeableFrameworkReferencesApp.csproj new file mode 100644 index 000000000000..6b0e2c773180 --- /dev/null +++ b/tests/dotnet/NativeMergeableFrameworkReferencesApp/MacCatalyst/NativeMergeableFrameworkReferencesApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst + + + diff --git a/tests/dotnet/NativeMergeableFrameworkReferencesApp/Makefile b/tests/dotnet/NativeMergeableFrameworkReferencesApp/Makefile new file mode 100644 index 000000000000..6affa45ff122 --- /dev/null +++ b/tests/dotnet/NativeMergeableFrameworkReferencesApp/Makefile @@ -0,0 +1,2 @@ +TOP=../../.. +include $(TOP)/tests/common/shared-dotnet-test.mk diff --git a/tests/dotnet/NativeMergeableFrameworkReferencesApp/iOS/NativeMergeableFrameworkReferencesApp.csproj b/tests/dotnet/NativeMergeableFrameworkReferencesApp/iOS/NativeMergeableFrameworkReferencesApp.csproj new file mode 100644 index 000000000000..86d408734aa8 --- /dev/null +++ b/tests/dotnet/NativeMergeableFrameworkReferencesApp/iOS/NativeMergeableFrameworkReferencesApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-ios + + + diff --git a/tests/dotnet/NativeMergeableFrameworkReferencesApp/macOS/NativeMergeableFrameworkReferencesApp.csproj b/tests/dotnet/NativeMergeableFrameworkReferencesApp/macOS/NativeMergeableFrameworkReferencesApp.csproj new file mode 100644 index 000000000000..a77287b9ba00 --- /dev/null +++ b/tests/dotnet/NativeMergeableFrameworkReferencesApp/macOS/NativeMergeableFrameworkReferencesApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-macos + + + diff --git a/tests/dotnet/NativeMergeableFrameworkReferencesApp/shared.csproj b/tests/dotnet/NativeMergeableFrameworkReferencesApp/shared.csproj new file mode 100644 index 000000000000..b9fdc6a3c8c0 --- /dev/null +++ b/tests/dotnet/NativeMergeableFrameworkReferencesApp/shared.csproj @@ -0,0 +1,20 @@ + + + + Exe + + NativeMergeableFrameworkReferencesApp + com.xamarin.nativemergeableframeworkreferences + 1.0 + + + + + + + + + + + + diff --git a/tests/dotnet/NativeMergeableFrameworkReferencesApp/tvOS/NativeMergeableFrameworkReferencesApp.csproj b/tests/dotnet/NativeMergeableFrameworkReferencesApp/tvOS/NativeMergeableFrameworkReferencesApp.csproj new file mode 100644 index 000000000000..bd487ddcd88d --- /dev/null +++ b/tests/dotnet/NativeMergeableFrameworkReferencesApp/tvOS/NativeMergeableFrameworkReferencesApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/UnitTests/PackTest.cs b/tests/dotnet/UnitTests/PackTest.cs index c5992b43650a..e96bc95fec14 100644 --- a/tests/dotnet/UnitTests/PackTest.cs +++ b/tests/dotnet/UnitTests/PackTest.cs @@ -121,6 +121,47 @@ public void BindingFrameworksProject (ApplePlatform platform, bool noBindingEmbe } } + [Test] + [TestCase (ApplePlatform.iOS, true)] + [TestCase (ApplePlatform.iOS, false)] + [TestCase (ApplePlatform.TVOS, true)] + [TestCase (ApplePlatform.TVOS, false)] + [TestCase (ApplePlatform.MacOSX, true)] + [TestCase (ApplePlatform.MacOSX, false)] + [TestCase (ApplePlatform.MacCatalyst, true)] + [TestCase (ApplePlatform.MacCatalyst, false)] + public void BindingMergeableFrameworksProject (ApplePlatform platform, bool noBindingEmbedding) + { + var project = "bindings-framework-test-mergeable"; + Configuration.IgnoreIfIgnoredPlatform (platform); + + var project_path = Path.Combine (Configuration.RootPath, "tests", "mergeable-framework-test", platform.AsString (), $"{project}.csproj"); + Clean (project_path); + + var tmpdir = Cache.CreateTemporaryDirectory (); + var outputPath = Path.Combine (tmpdir, "OutputPath"); + var intermediateOutputPath = Path.Combine (tmpdir, "IntermediateOutputPath"); + var properties = GetDefaultProperties (); + properties ["OutputPath"] = outputPath + Path.DirectorySeparatorChar; + properties ["IntermediateOutputPath"] = intermediateOutputPath + Path.DirectorySeparatorChar; + properties ["NoBindingEmbedding"] = noBindingEmbedding ? "true" : "false"; + + DotNet.AssertPack (project_path, properties, msbuildParallelism: false); + + var nupkg = Path.Combine (outputPath, project + ".1.0.0.nupkg"); + Assert.That (nupkg, Does.Exist, "nupkg existence"); + + using var archive = ZipFile.OpenRead (nupkg); + var files = archive.Entries.Select (v => v.FullName).ToHashSet (); + var tfm = platform.ToFrameworkWithPlatformVersion (isExecutable: false); + Assert.That (files, Does.Contain (project + ".nuspec"), "nuspec"); + Assert.That (files, Does.Contain ($"lib/{tfm}/{project}.dll"), $"{project}.dll"); + if (noBindingEmbedding) { + // The XMergeableTest framework is always packaged as a .resources.zip + Assert.That (files, Does.Contain ($"lib/{tfm}/{project}.resources.zip"), $"{project}.resources.zip"); + } + } + [Test] [Category ("Multiplatform")] [TestCase (ApplePlatform.iOS, true, true, true)] diff --git a/tests/dotnet/UnitTests/ProjectTest.cs b/tests/dotnet/UnitTests/ProjectTest.cs index 7db2614ce7e3..4045ea855a4c 100644 --- a/tests/dotnet/UnitTests/ProjectTest.cs +++ b/tests/dotnet/UnitTests/ProjectTest.cs @@ -650,6 +650,14 @@ public void IsNotOverrideRuntimeIdentifier (ApplePlatform platform, string runti [TestCase ("NativeFrameworkReferencesApp", ApplePlatform.MacOSX, "osx-x64")] [TestCase ("NativeXCFrameworkReferencesApp", ApplePlatform.iOS, "iossimulator-x64")] [TestCase ("NativeXCFrameworkReferencesApp", ApplePlatform.MacOSX, "osx-x64")] + [TestCase ("NativeMergeableFrameworkReferencesApp", ApplePlatform.iOS, "iossimulator-arm64")] + [TestCase ("NativeMergeableFrameworkReferencesApp", ApplePlatform.TVOS, "tvossimulator-arm64")] + [TestCase ("NativeMergeableFrameworkReferencesApp", ApplePlatform.MacOSX, "osx-arm64")] + [TestCase ("NativeMergeableFrameworkReferencesApp", ApplePlatform.MacCatalyst, "maccatalyst-arm64")] + [TestCase ("NativeMergeableDylibReferencesApp", ApplePlatform.iOS, "iossimulator-arm64")] + [TestCase ("NativeMergeableDylibReferencesApp", ApplePlatform.TVOS, "tvossimulator-arm64")] + [TestCase ("NativeMergeableDylibReferencesApp", ApplePlatform.MacOSX, "osx-arm64")] + [TestCase ("NativeMergeableDylibReferencesApp", ApplePlatform.MacCatalyst, "maccatalyst-arm64")] public void BuildAndExecuteNativeReferencesTestApp (string project, ApplePlatform platform, string runtimeIdentifier) { Configuration.IgnoreIfIgnoredPlatform (platform); @@ -667,6 +675,60 @@ public void BuildAndExecuteNativeReferencesTestApp (string project, ApplePlatfor } } + [Test] + [TestCase (ApplePlatform.MacOSX, "osx-arm64", true)] // Optimize=true should strip atom info + [TestCase (ApplePlatform.MacOSX, "osx-arm64", false)] // Optimize=false should preserve atom info + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", true)] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", false)] + public void BuildNativeMergeableFrameworkReferencesApp_AtomInfoStripping (ApplePlatform platform, string runtimeIdentifier, bool optimize) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifier); + + var project = "NativeMergeableFrameworkReferencesApp"; + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifier, platform: platform, out var appPath); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifier); + properties ["Optimize"] = optimize.ToString ().ToLowerInvariant (); + DotNet.AssertBuild (project_path, properties); + + var frameworkPath = Path.Combine (appPath, GetFrameworksRelativePath (platform), "XMergeableTest.framework", "XMergeableTest"); + Assert.That (frameworkPath, Does.Exist, "Framework should exist in app bundle"); + + if (optimize) { + Assert.That (MachO.IsMergeableLibrary (frameworkPath), Is.False, "Framework should not be mergeable when Optimize=true"); + } else { + Assert.That (MachO.IsMergeableLibrary (frameworkPath), Is.True, "Framework should be mergeable when Optimize=false"); + } + } + + [Test] + [TestCase (ApplePlatform.MacOSX, "osx-arm64", true)] // Optimize=true should strip atom info + [TestCase (ApplePlatform.MacOSX, "osx-arm64", false)] // Optimize=false should preserve atom info + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", true)] + [TestCase (ApplePlatform.MacCatalyst, "maccatalyst-arm64", false)] + public void BuildNativeMergeableDylibReferencesApp_AtomInfoStripping (ApplePlatform platform, string runtimeIdentifier, bool optimize) + { + Configuration.IgnoreIfIgnoredPlatform (platform); + Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifier); + + var project = "NativeMergeableDylibReferencesApp"; + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifier, platform: platform, out var appPath); + Clean (project_path); + var properties = GetDefaultProperties (runtimeIdentifier); + properties ["Optimize"] = optimize.ToString ().ToLowerInvariant (); + DotNet.AssertBuild (project_path, properties); + + var dylibPath = Path.Combine (appPath, GetRelativeDylibDirectory (platform), "libMergeableFramework.dylib"); + Assert.That (dylibPath, Does.Exist, "Dylib should exist in app bundle"); + + if (optimize) { + Assert.That (MachO.IsMergeableLibrary (dylibPath), Is.False, "Dylib should not be mergeable when Optimize=true"); + } else { + Assert.That (MachO.IsMergeableLibrary (dylibPath), Is.True, "Dylib should be mergeable when Optimize=false"); + } + } + [Test] [TestCase (ApplePlatform.iOS, "ios-x64", false)] // valid RID in a previous preview (and common mistake) [TestCase (ApplePlatform.iOS, "iossimulator-x84", true)] // it's x86, not x84 diff --git a/tests/mergeable-framework-test/MacCatalyst/bindings-framework-test-mergeable.csproj b/tests/mergeable-framework-test/MacCatalyst/bindings-framework-test-mergeable.csproj new file mode 100644 index 000000000000..408137427dbc --- /dev/null +++ b/tests/mergeable-framework-test/MacCatalyst/bindings-framework-test-mergeable.csproj @@ -0,0 +1,12 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst + + + + + + + + diff --git a/tests/mergeable-framework-test/iOS/bindings-framework-test-mergeable.csproj b/tests/mergeable-framework-test/iOS/bindings-framework-test-mergeable.csproj new file mode 100644 index 000000000000..ea857c65891f --- /dev/null +++ b/tests/mergeable-framework-test/iOS/bindings-framework-test-mergeable.csproj @@ -0,0 +1,12 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-ios + + + + + + + + diff --git a/tests/mergeable-framework-test/macOS/bindings-framework-test-mergeable.csproj b/tests/mergeable-framework-test/macOS/bindings-framework-test-mergeable.csproj new file mode 100644 index 000000000000..687d45c00026 --- /dev/null +++ b/tests/mergeable-framework-test/macOS/bindings-framework-test-mergeable.csproj @@ -0,0 +1,12 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-macos + + + + + + + + diff --git a/tests/mergeable-framework-test/shared.csproj b/tests/mergeable-framework-test/shared.csproj new file mode 100644 index 000000000000..c01f9dc07b27 --- /dev/null +++ b/tests/mergeable-framework-test/shared.csproj @@ -0,0 +1,31 @@ + + + + Library + true + $(DefineConstants);NET + bindingstest + bindings-framework-test-mergeable + true + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\..')) + $(RootTestsDirectory)\test-libraries + $(RootTestsDirectory)\bindings-framework-test + + + + + + + + + + Framework + False + + + + + + + diff --git a/tests/mergeable-framework-test/tvOS/bindings-framework-test-mergeable.csproj b/tests/mergeable-framework-test/tvOS/bindings-framework-test-mergeable.csproj new file mode 100644 index 000000000000..092ec1d4c0a9 --- /dev/null +++ b/tests/mergeable-framework-test/tvOS/bindings-framework-test-mergeable.csproj @@ -0,0 +1,12 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + + + + + + diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeableLibraryTests.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeableLibraryTests.cs new file mode 100644 index 000000000000..043f60c5c892 --- /dev/null +++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeableLibraryTests.cs @@ -0,0 +1,276 @@ +#nullable enable + +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Build.Utilities; + +using NUnit.Framework; + +using Xamarin; +using Xamarin.Tests; + +namespace Xamarin.MacDev.Tasks.Tests { + + [TestFixture] + public class MergeableLibraryTests : TestBase { + + static byte [] CreateMinimalMachODylib () + { + var header = new byte [] { + // Mach-O magic number for 64-bit (MH_MAGIC_64) + 0xCF, 0xFA, 0xED, 0xFE, + // CPU type (CPU_TYPE_X86_64 = 0x01000007) + 0x07, 0x00, 0x00, 0x01, + // CPU subtype + 0x03, 0x00, 0x00, 0x00, + // File type (MH_DYLIB = 6) + 0x06, 0x00, 0x00, 0x00, + // Number of load commands + 0x00, 0x00, 0x00, 0x00, + // Size of load commands + 0x00, 0x00, 0x00, 0x00, + // Flags + 0x00, 0x00, 0x00, 0x00, + // Reserved (64-bit only) + 0x00, 0x00, 0x00, 0x00 + }; + return header; + } + + static byte [] CreateMinimalMergeableDylib () + { + // A Mach-O dylib with a single LC_ATOM_INFO load command (linkedit_data_command) + var header = new byte [] { + // Mach-O magic number for 64-bit (MH_MAGIC_64) + 0xCF, 0xFA, 0xED, 0xFE, + // CPU type (CPU_TYPE_X86_64 = 0x01000007) + 0x07, 0x00, 0x00, 0x01, + // CPU subtype + 0x03, 0x00, 0x00, 0x00, + // File type (MH_DYLIB = 6) + 0x06, 0x00, 0x00, 0x00, + // Number of load commands = 1 + 0x01, 0x00, 0x00, 0x00, + // Size of load commands = 16 (sizeof linkedit_data_command) + 0x10, 0x00, 0x00, 0x00, + // Flags + 0x00, 0x00, 0x00, 0x00, + // Reserved (64-bit only) + 0x00, 0x00, 0x00, 0x00, + // LC_ATOM_INFO load command (linkedit_data_command) + // cmd = 0x36 (LC_ATOM_INFO) + 0x36, 0x00, 0x00, 0x00, + // cmdsize = 16 + 0x10, 0x00, 0x00, 0x00, + // dataoff = 0 (offset to atom data) + 0x00, 0x00, 0x00, 0x00, + // datasize = 0 (size of atom data) + 0x00, 0x00, 0x00, 0x00, + }; + return header; + } + + static byte [] CreateMinimalStaticLib () + { + // A minimal MH_OBJECT file (not a dylib) + var header = new byte [] { + // Mach-O magic number for 64-bit (MH_MAGIC_64) + 0xCF, 0xFA, 0xED, 0xFE, + // CPU type (CPU_TYPE_X86_64 = 0x01000007) + 0x07, 0x00, 0x00, 0x01, + // CPU subtype + 0x03, 0x00, 0x00, 0x00, + // File type (MH_OBJECT = 1) + 0x01, 0x00, 0x00, 0x00, + // Number of load commands + 0x00, 0x00, 0x00, 0x00, + // Size of load commands + 0x00, 0x00, 0x00, 0x00, + // Flags + 0x00, 0x00, 0x00, 0x00, + // Reserved (64-bit only) + 0x00, 0x00, 0x00, 0x00 + }; + return header; + } + + [Test] + public void NonMergeableDylib_IsNotMergeable () + { + var tempDir = Cache.CreateTemporaryDirectory (); + var dylibPath = Path.Combine (tempDir, "test.dylib"); + File.WriteAllBytes (dylibPath, CreateMinimalMachODylib ()); + Assert.That (MachO.IsMergeableLibrary (dylibPath), Is.False, "Non-mergeable dylib should not be detected as mergeable"); + } + + [Test] + public void MergeableDylib_IsMergeable () + { + var tempDir = Cache.CreateTemporaryDirectory (); + var dylibPath = Path.Combine (tempDir, "test_mergeable.dylib"); + File.WriteAllBytes (dylibPath, CreateMinimalMergeableDylib ()); + Assert.That (MachO.IsMergeableLibrary (dylibPath), Is.True, "Mergeable dylib should be detected as mergeable"); + } + + [Test] + public void MergeableDylib_IsDynamicFramework () + { + // A mergeable dylib is still a dynamic library + var tempDir = Cache.CreateTemporaryDirectory (); + var dylibPath = Path.Combine (tempDir, "test_mergeable.dylib"); + File.WriteAllBytes (dylibPath, CreateMinimalMergeableDylib ()); + Assert.That (MachO.IsDynamicFramework (dylibPath), Is.True, "Mergeable dylib should still be detected as a dynamic framework"); + } + + [Test] + public void ObjectFile_IsNotMergeable () + { + var tempDir = Cache.CreateTemporaryDirectory (); + var objPath = Path.Combine (tempDir, "test.o"); + File.WriteAllBytes (objPath, CreateMinimalStaticLib ()); + Assert.That (MachO.IsMergeableLibrary (objPath), Is.False, "Object file should not be detected as mergeable"); + } + + [Test] + public void MergeableFramework_IsMergeable () + { + var tempDir = Cache.CreateTemporaryDirectory (); + var frameworkDir = Path.Combine (tempDir, "TestMergeable.framework"); + Directory.CreateDirectory (frameworkDir); + var executablePath = Path.Combine (frameworkDir, "TestMergeable"); + File.WriteAllBytes (executablePath, CreateMinimalMergeableDylib ()); + Assert.That (MachO.IsMergeableLibrary (executablePath), Is.True, "Mergeable framework executable should be detected as mergeable"); + } + + [Test] + public void NonMergeableFramework_IsNotMergeable () + { + var tempDir = Cache.CreateTemporaryDirectory (); + var frameworkDir = Path.Combine (tempDir, "TestNonMergeable.framework"); + Directory.CreateDirectory (frameworkDir); + var executablePath = Path.Combine (frameworkDir, "TestNonMergeable"); + File.WriteAllBytes (executablePath, CreateMinimalMachODylib ()); + Assert.That (MachO.IsMergeableLibrary (executablePath), Is.False, "Non-mergeable framework executable should not be detected as mergeable"); + } + + [Test] + public void HasAtomInfo_Property () + { + var tempDir = Cache.CreateTemporaryDirectory (); + + var mergeablePath = Path.Combine (tempDir, "mergeable.dylib"); + File.WriteAllBytes (mergeablePath, CreateMinimalMergeableDylib ()); + var mergeableFiles = MachO.Read (mergeablePath); + foreach (var mf in mergeableFiles) + Assert.That (mf.HasAtomInfo, Is.True, "Mergeable MachOFile should have atom info"); + + var normalPath = Path.Combine (tempDir, "normal.dylib"); + File.WriteAllBytes (normalPath, CreateMinimalMachODylib ()); + var normalFiles = MachO.Read (normalPath); + foreach (var mf in normalFiles) + Assert.That (mf.HasAtomInfo, Is.False, "Normal MachOFile should not have atom info"); + } + + static void RunProcess (string filename, IList arguments) + { + var rv = ExecutionHelper.Execute (filename, arguments, out var output, null, TimeSpan.FromSeconds (30)); + if (rv != 0) + Assert.Fail ($"Process '{filename} {string.Join (" ", arguments)}' failed with exit code {rv}.\nOutput: {output}"); + } + + [Test] + public void RealMergeableDylib_DetectedAndStrippable () + { + var tempDir = Cache.CreateTemporaryDirectory (); + var sourcePath = Path.Combine (tempDir, "test.c"); + File.WriteAllText (sourcePath, "int mergeable_test_func (int a, int b) { return a + b; }"); + + // Build a normal dylib and a mergeable dylib + var normalDylib = Path.Combine (tempDir, "normal.dylib"); + var mergeableDylib = Path.Combine (tempDir, "mergeable.dylib"); + + RunProcess ("xcrun", new [] { "clang", "-dynamiclib", "-o", normalDylib, sourcePath, "-arch", "arm64" }); + RunProcess ("xcrun", new [] { "clang", "-dynamiclib", "-o", mergeableDylib, sourcePath, "-arch", "arm64", "-Wl,-make_mergeable" }); + + // Verify detection + Assert.That (MachO.IsMergeableLibrary (normalDylib), Is.False, "Normal dylib should not be mergeable"); + Assert.That (MachO.IsMergeableLibrary (mergeableDylib), Is.True, "Mergeable dylib should be mergeable"); + + // Both should be detected as dynamic + Assert.That (MachO.IsDynamicFramework (normalDylib), Is.True, "Normal dylib should be dynamic"); + Assert.That (MachO.IsDynamicFramework (mergeableDylib), Is.True, "Mergeable dylib should be dynamic"); + + // Strip atom info and verify + var strippedDylib = Path.Combine (tempDir, "stripped.dylib"); + File.Copy (mergeableDylib, strippedDylib); + RunProcess ("xcrun", new [] { "strip", "-no_atom_info", "-S", strippedDylib }); + + Assert.That (MachO.IsMergeableLibrary (strippedDylib), Is.False, "Stripped dylib should not be mergeable"); + Assert.That (MachO.IsDynamicFramework (strippedDylib), Is.True, "Stripped dylib should still be dynamic"); + + // Verify size reduction + var mergeableSize = new FileInfo (mergeableDylib).Length; + var strippedSize = new FileInfo (strippedDylib).Length; + Assert.That (strippedSize, Is.LessThan (mergeableSize), "Stripped dylib should be smaller than mergeable dylib"); + } + + [Test] + public void SymbolStrip_StripsAtomInfoFromFramework () + { + var tempDir = Cache.CreateTemporaryDirectory (); + var sourcePath = Path.Combine (tempDir, "test.c"); + File.WriteAllText (sourcePath, "int mergeable_test_func (int a, int b) { return a + b; }"); + + // Create a mergeable framework + var frameworkDir = Path.Combine (tempDir, "TestMergeable.framework"); + Directory.CreateDirectory (frameworkDir); + var executablePath = Path.Combine (frameworkDir, "TestMergeable"); + RunProcess ("xcrun", new [] { "clang", "-dynamiclib", "-o", executablePath, sourcePath, "-arch", "arm64", "-Wl,-make_mergeable", "-install_name", "@rpath/TestMergeable.framework/TestMergeable" }); + + Assert.That (MachO.IsMergeableLibrary (executablePath), Is.True, "Framework should be mergeable before stripping"); + + // Run the SymbolStrip task with StripMergeableLibraries=true + var task = CreateTask (); + var item = new TaskItem (executablePath); + item.SetMetadata ("Kind", "Framework"); + task.Executable = new Microsoft.Build.Framework.ITaskItem [] { item }; + task.StripMergeableLibraries = true; + ExecuteTask (task); + + // Verify atom info was removed + Assert.That (MachO.IsMergeableLibrary (executablePath), Is.False, "Framework should not be mergeable after SymbolStrip with StripMergeableLibraries=true"); + Assert.That (MachO.IsDynamicFramework (executablePath), Is.True, "Framework should still be dynamic after SymbolStrip"); + } + + [Test] + public void SymbolStrip_PreservesAtomInfoWhenNotStripping () + { + var tempDir = Cache.CreateTemporaryDirectory (); + var sourcePath = Path.Combine (tempDir, "test.c"); + File.WriteAllText (sourcePath, "int mergeable_test_func (int a, int b) { return a + b; }"); + + // Create a mergeable framework + var frameworkDir = Path.Combine (tempDir, "TestMergeable.framework"); + Directory.CreateDirectory (frameworkDir); + var executablePath = Path.Combine (frameworkDir, "TestMergeable"); + RunProcess ("xcrun", new [] { "clang", "-dynamiclib", "-o", executablePath, sourcePath, "-arch", "arm64", "-Wl,-make_mergeable", "-install_name", "@rpath/TestMergeable.framework/TestMergeable" }); + + Assert.That (MachO.IsMergeableLibrary (executablePath), Is.True, "Framework should be mergeable before stripping"); + + // Run the SymbolStrip task with StripMergeableLibraries=false (debug mode) + var task = CreateTask (); + var item = new TaskItem (executablePath); + item.SetMetadata ("Kind", "Framework"); + task.Executable = new Microsoft.Build.Framework.ITaskItem [] { item }; + task.StripMergeableLibraries = false; + ExecuteTask (task); + + // Verify atom info was preserved + Assert.That (MachO.IsMergeableLibrary (executablePath), Is.True, "Framework should still be mergeable after SymbolStrip with StripMergeableLibraries=false"); + Assert.That (MachO.IsDynamicFramework (executablePath), Is.True, "Framework should still be dynamic after SymbolStrip"); + } + } +} diff --git a/tests/test-libraries/Makefile b/tests/test-libraries/Makefile index c405c68f772f..2caaf2806627 100644 --- a/tests/test-libraries/Makefile +++ b/tests/test-libraries/Makefile @@ -14,12 +14,14 @@ ZIP=zip --symlinks endif TEST_FRAMEWORKS+=XTest +TEST_FRAMEWORKS+=XMergeableTest TEST_FRAMEWORKS+=XStaticArTest TEST_FRAMEWORKS+=XStaticObjectTest TEST_FRAMEWORKS+=SwiftTest TEST_FRAMEWORKS+=SwiftTest2 DYNAMIC_TEST_FRAMEWORKS+=XTest +DYNAMIC_TEST_FRAMEWORKS+=XMergeableTest STATIC_AR_TEST_FRAMEWORKS+=XStaticArTest STATIC_OBJECT_TEST_FRAMEWORKS+=XStaticObjectTest DYNAMIC_TEST_FRAMEWORKS+=SwiftTest @@ -240,6 +242,7 @@ define PlatformDynamicTemplate all-local:: .libs/$(1)/$(2).dylib endef $(foreach testFramework,libframework,$(foreach xcframeworkPlatform,$(XCFRAMEWORK_PLATFORMS),$(eval $(call PlatformDynamicTemplate,$(xcframeworkPlatform),$(testFramework),$(XCFRAMEWORK_$(xcframeworkPlatform)_RUNTIME_IDENTIFIERS))))) +$(foreach testFramework,libMergeableFramework,$(foreach xcframeworkPlatform,$(XCFRAMEWORK_PLATFORMS),$(eval $(call PlatformDynamicTemplate,$(xcframeworkPlatform),$(testFramework),$(XCFRAMEWORK_$(xcframeworkPlatform)_RUNTIME_IDENTIFIERS))))) # Create the symlinks for the per-platform frameworks (for desktop platforms) # Here 'platform' is defined as a platform for xcframework (see above) @@ -298,6 +301,14 @@ EXTRA_DEPENDENCIES = libtest.h $(GENERATED_FILES) rename.h $$(Q) $(CP) $$^ $$@ $$(Q) $(XCODE_DEVELOPER_ROOT)/Toolchains/XcodeDefault.xctoolchain/usr/bin/install_name_tool -id @rpath/XTest.framework/XTest $$@ +# XMergeableTest is a framework where the binary code is a (fat) mergeable dynamic library (built with -make_mergeable) +.libs/$(1)/libXMergeableTest.dylib: libframework.m +.libs/$(1)/libXMergeableTest.dylib: EXTRA_FLAGS += -Wl,-make_mergeable + +# libMergeableFramework.dylib is a standalone mergeable dynamic library (not in a framework) +.libs/$(1)/libMergeableFramework.dylib: libframework.m +.libs/$(1)/libMergeableFramework.dylib: EXTRA_FLAGS += -Wl,-make_mergeable + # XStaticObjectTest is a framework where the binary code is a (fat) object file .libs/$(1)/libXStaticObjectTest.o: .libs/$(1)/libtest-object.o $$(Q) rm -f $$@ diff --git a/tests/test-libraries/plists/XMergeableTest-Info-ios-arm64.plist b/tests/test-libraries/plists/XMergeableTest-Info-ios-arm64.plist new file mode 100644 index 000000000000..172d355721c9 --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-ios-arm64.plist @@ -0,0 +1,57 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + CFBundleSupportedPlatforms + + iPhoneOS + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + iphoneos + DTPlatformVersion + 8.2 + DTSDKBuild + 12D508 + DTSDKName + iphoneos8.2 + DTXcode + 0620 + DTXcodeBuild + 6C131e + MinimumOSVersion + 12.2 + UIDeviceFamily + + 1 + 2 + + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-iossimulator-arm64.plist b/tests/test-libraries/plists/XMergeableTest-Info-iossimulator-arm64.plist new file mode 100644 index 000000000000..172d355721c9 --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-iossimulator-arm64.plist @@ -0,0 +1,57 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + CFBundleSupportedPlatforms + + iPhoneOS + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + iphoneos + DTPlatformVersion + 8.2 + DTSDKBuild + 12D508 + DTSDKName + iphoneos8.2 + DTXcode + 0620 + DTXcodeBuild + 6C131e + MinimumOSVersion + 12.2 + UIDeviceFamily + + 1 + 2 + + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-iossimulator-x64.plist b/tests/test-libraries/plists/XMergeableTest-Info-iossimulator-x64.plist new file mode 100644 index 000000000000..172d355721c9 --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-iossimulator-x64.plist @@ -0,0 +1,57 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + CFBundleSupportedPlatforms + + iPhoneOS + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + iphoneos + DTPlatformVersion + 8.2 + DTSDKBuild + 12D508 + DTSDKName + iphoneos8.2 + DTXcode + 0620 + DTXcodeBuild + 6C131e + MinimumOSVersion + 12.2 + UIDeviceFamily + + 1 + 2 + + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-maccatalyst-arm64.plist b/tests/test-libraries/plists/XMergeableTest-Info-maccatalyst-arm64.plist new file mode 100644 index 000000000000..f48a067f721a --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-maccatalyst-arm64.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + macosx + DTPlatformVersion + 10.15 + DTSDKBuild + 12D508 + DTSDKName + macosx10.15 + DTXcode + 0620 + DTXcodeBuild + 6C131e + LSMinimumSystemVersion + 12.0 + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-maccatalyst-x64.plist b/tests/test-libraries/plists/XMergeableTest-Info-maccatalyst-x64.plist new file mode 100644 index 000000000000..f48a067f721a --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-maccatalyst-x64.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + macosx + DTPlatformVersion + 10.15 + DTSDKBuild + 12D508 + DTSDKName + macosx10.15 + DTXcode + 0620 + DTXcodeBuild + 6C131e + LSMinimumSystemVersion + 12.0 + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-osx-arm64.plist b/tests/test-libraries/plists/XMergeableTest-Info-osx-arm64.plist new file mode 100644 index 000000000000..1797108fd845 --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-osx-arm64.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + macosx + DTPlatformVersion + 10.9 + DTSDKBuild + 12D508 + DTSDKName + macosx10.9 + DTXcode + 0620 + DTXcodeBuild + 6C131e + LSMinimumSystemVersion + 12.0 + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-osx-x64.plist b/tests/test-libraries/plists/XMergeableTest-Info-osx-x64.plist new file mode 100644 index 000000000000..1797108fd845 --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-osx-x64.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + macosx + DTPlatformVersion + 10.9 + DTSDKBuild + 12D508 + DTSDKName + macosx10.9 + DTXcode + 0620 + DTXcodeBuild + 6C131e + LSMinimumSystemVersion + 12.0 + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-tvos-arm64.plist b/tests/test-libraries/plists/XMergeableTest-Info-tvos-arm64.plist new file mode 100644 index 000000000000..33ccc237efab --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-tvos-arm64.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + CFBundleSupportedPlatforms + + AppleTVOS + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + appletvos + DTPlatformVersion + 9.0 + DTSDKBuild + 12D508 + DTSDKName + appletvos9.0 + DTXcode + 0620 + DTXcodeBuild + 6C131e + MinimumOSVersion + 12.2 + UIDeviceFamily + + 3 + + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-tvossimulator-arm64.plist b/tests/test-libraries/plists/XMergeableTest-Info-tvossimulator-arm64.plist new file mode 100644 index 000000000000..33ccc237efab --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-tvossimulator-arm64.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + CFBundleSupportedPlatforms + + AppleTVOS + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + appletvos + DTPlatformVersion + 9.0 + DTSDKBuild + 12D508 + DTSDKName + appletvos9.0 + DTXcode + 0620 + DTXcodeBuild + 6C131e + MinimumOSVersion + 12.2 + UIDeviceFamily + + 3 + + + diff --git a/tests/test-libraries/plists/XMergeableTest-Info-tvossimulator-x64.plist b/tests/test-libraries/plists/XMergeableTest-Info-tvossimulator-x64.plist new file mode 100644 index 000000000000..33ccc237efab --- /dev/null +++ b/tests/test-libraries/plists/XMergeableTest-Info-tvossimulator-x64.plist @@ -0,0 +1,56 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleIdentifier + xamarin.ios.xmergeabletest + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + XMergeableTest + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 3.12 + NSPrincipalClass + + CFBundleExecutable + XMergeableTest + BuildMachineOSBuild + 13F34 + CFBundleDevelopmentRegion + en + CFBundleSupportedPlatforms + + AppleTVOS + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 12D508 + DTPlatformName + appletvos + DTPlatformVersion + 9.0 + DTSDKBuild + 12D508 + DTSDKName + appletvos9.0 + DTXcode + 0620 + DTXcodeBuild + 6C131e + MinimumOSVersion + 12.2 + UIDeviceFamily + + 3 + + + diff --git a/tools/common/MachO.cs b/tools/common/MachO.cs index 8f5426f4cf99..7235ba5eabc8 100644 --- a/tools/common/MachO.cs +++ b/tools/common/MachO.cs @@ -156,6 +156,9 @@ public enum LoadCommands : uint { MinwatchOS = 0x30,//#define LC_VERSION_MIN_WATCHOS 0x30 /* build for Watch min OS version */ //#define LC_NOTE 0x31 /* arbitrary data included within a Mach-O file */ BuildVersion = 0x32,//#define LC_BUILD_VERSION 0x32 /* build for platform min OS version */ + //#define LC_DYLD_CHAINED_FIXUPS (0x34 | LC_REQ_DYLD) /* used with linkedit_data_command */ + //#define LC_FILESET_ENTRY (0x35 | LC_REQ_DYLD) /* used with fileset_entry_command */ + AtomInfo = 0x36,//#define LC_ATOM_INFO 0x36 /* used with linkedit_data_command, used by mergeable libraries */ } public enum Platform : uint { @@ -410,6 +413,29 @@ public static bool IsDynamicFramework (string filename) return true; } + // A mergeable library is a dynamic library that also contains atom info (LC_ATOM_INFO), + // which allows it to be linked statically as well. The atom info can be stripped to + // reduce the size of the library when it's used as a dynamic library. + // Ref: https://developer.apple.com/videos/play/wwdc2023/10268/ + public static bool IsMergeableLibrary (string filename) + { + var f = ReadFile (filename); + if (f is MachOFile mf) + return mf.IsDynamicLibrary && mf.HasAtomInfo; + + var fat = f as FatFile; + if (fat is null) + return false; + if (fat.entries is null) + return false; + + foreach (var entry in fat.entries) + if (entry.entry is not null && entry.entry.IsDynamicLibrary && entry.entry.HasAtomInfo) + return true; + + return false; + } + public static bool IsMachOFile (string filename) { using (var fs = File.OpenRead (filename)) { @@ -848,6 +874,18 @@ public bool IsObjectFile { get => filetype == MachO.MH_OBJECT; } + // Whether the Mach-O file contains atom info (LC_ATOM_INFO), which is the metadata + // added to mergeable libraries that enables static linking. + public bool HasAtomInfo { + get { + foreach (var lc in load_commands) { + if ((MachO.LoadCommands) lc.cmd == MachO.LoadCommands.AtomInfo) + return true; + } + return false; + } + } + const byte N_EXT = 0x01; // external symbol const byte N_TYPE = 0x0e; // mask for type bits const byte N_UNDF = 0x0; // undefined symbol