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