Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
13a839f
[msbuild] Add support for stripping mergeable library metadata
rolfbjarne Feb 12, 2026
fcd6b2b
[msbuild] Add StripMergeableLibraries property and test infrastructure
rolfbjarne Feb 13, 2026
ace8553
[tests] Add mergeable framework binding project and pack test
rolfbjarne Feb 13, 2026
9ebf2af
Auto-format source code
Feb 13, 2026
ac502d3
[msbuild] Fix mergeable library stripping for all build configurations
rolfbjarne Feb 16, 2026
140eb62
[tests] Add mergeable dylib test and remove hardcoded RuntimeIdentifiers
rolfbjarne Feb 16, 2026
2c4267d
[tests] Use arm64 runtime identifiers in mergeable library tests
rolfbjarne Feb 17, 2026
c5264a1
Merge remote-tracking branch 'origin/main' into dev/rolf/mergeable-li…
rolfbjarne Feb 17, 2026
6fa9a8d
[tests] Fix BindingMergeableFrameworksProject nupkg content assertions
rolfbjarne Feb 18, 2026
1a5150c
[review] Address PR review comments
rolfbjarne Feb 19, 2026
57b4643
Merge origin/main into dev/rolf/mergeable-libraries
rolfbjarne Sep 2, 2026
892215e
Restore BindingXcFrameworksProject iOS test case
rolfbjarne Sep 2, 2026
b300d98
Address mergeable library review feedback
rolfbjarne Sep 3, 2026
c8cd8fe
Merge remote-tracking branch 'origin/main' into dev/rolf/mergeable-li…
rolfbjarne Sep 3, 2026
d2bb947
[msbuild] Fix stripping universal mergeable dylibs
rolfbjarne Sep 4, 2026
c312fa8
Merge remote-tracking branch 'origin/main' into dev/rolf/mergeable-li…
rolfbjarne Sep 4, 2026
14f99e6
Address mergeable library review comments
rolfbjarne Sep 4, 2026
ce1c8a1
Simplify mergeable library stamps
rolfbjarne Sep 4, 2026
729ba3d
Merge remote-tracking branch 'origin/main' into dev/rolf/mergeable-li…
rolfbjarne Sep 4, 2026
8b80584
Merge branch 'main' into dev/rolf/mergeable-libraries
rolfbjarne Sep 7, 2026
57b3041
[msbuild] Keep mergeable library stamp names short
rolfbjarne Sep 7, 2026
ab786e1
Merge remote-tracking branch 'origin/main' into dev/rolf/mergeable-li…
rolfbjarne Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<PropertyGroup>
<StripMergeableLibraries>true</StripMergeableLibraries>
</PropertyGroup>
```

## SupportedOSPlatformVersion

Specifies the minimum OS version the app can run on.
Expand Down
127 changes: 127 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/StripMergeableLibraryMetadata.cs
Original file line number Diff line number Diff line change
@@ -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<string> ();
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<ITaskItem> GetAdditionalItemsToBeCopied () => Enumerable.Empty<ITaskItem> ();
}
}
8 changes: 8 additions & 0 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/SymbolStrip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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));
Expand Down
4 changes: 4 additions & 0 deletions msbuild/Xamarin.Shared/Xamarin.Shared.props
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ Copyright (C) 2020 Microsoft. All rights reserved.
<NoSymbolStrip Condition="'$(NoSymbolStrip)' == '' And '$(SdkIsSimulator)' == 'true'">true</NoSymbolStrip>
<NoSymbolStrip Condition="'$(NoSymbolStrip)' == ''">false</NoSymbolStrip>

<!-- StripMergeableLibraries: remove static linking metadata (LC_ATOM_INFO) from mergeable libraries to reduce app size -->
<!-- Default to true when optimizing (typically Release builds) -->
<StripMergeableLibraries Condition="'$(StripMergeableLibraries)' == ''">$(Optimize)</StripMergeableLibraries>
Comment thread
rolfbjarne marked this conversation as resolved.

<!-- NoDSymUtil -->
<!-- Xamarin.Mac never had an equivalent for MtouchNoDSymUtil and never produced them -> now, produce them by default when archiving -->
<NoDSymUtil Condition="'$(NoDSymUtil)' == '' And '$(_PlatformName)' != 'macOS'">$(MtouchNoDSymUtil)</NoDSymUtil>
Expand Down
26 changes: 26 additions & 0 deletions msbuild/Xamarin.Shared/Xamarin.Shared.targets
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.SmartCopy" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.SpotlightIndexer" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.StripFrameworkHeaders" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.StripMergeableLibraryMetadata" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.SymbolStrip" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.TextureAtlas" AssemblyFile="$(_TaskAssemblyName)" />
<UsingTask Runtime="$(_TaskRuntime)" TaskName="Xamarin.MacDev.Tasks.UnpackLibraryResources" AssemblyFile="$(_TaskAssemblyName)" />
Expand Down Expand Up @@ -3247,6 +3248,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
_ComputeXCFrameworkDSyms;
_CopyXCFrameworkDSyms;
_NativeStripFiles;
_StripMergeableLibraryMetadata;
_NotifySpotlight;
</_PostProcessAppBundleDependsOn>
</PropertyGroup>
Expand Down Expand Up @@ -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)"
Expand Down Expand Up @@ -3423,6 +3426,29 @@ Copyright (C) 2018 Microsoft. All rights reserved.
/>
</Target>

<!--
Strip mergeable library metadata (LC_ATOM_INFO) from frameworks and dylibs in the app bundle.
This runs independently of the regular symbol stripping, because symbol stripping
may be disabled (e.g. simulator builds, debug builds, macOS/MacCatalyst).
-->
<Target
Name="_StripMergeableLibraryMetadata"
Condition="'$(_PostProcess)' == 'true' And '$(StripMergeableLibraries)' == 'true'"
DependsOnTargets="_PreparePostProcessing"
>

<StripMergeableLibraryMetadata
SessionId="$(BuildSessionId)"
Condition="'$(IsMacEnabled)' == 'true'"
FrameworksDirectory="$(_AppFrameworksPath)"
DylibDirectories="$(_AppBundlePath);$(_AppContentsPath)"
StampDirectory="$(DeviceSpecificIntermediateOutputPath)mergeable-library-metadata"
StripPath="$(StripPath)"
>
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
</StripMergeableLibraryMetadata>
</Target>
Comment thread
rolfbjarne marked this conversation as resolved.

<!-- make sure spotlight indexes everything we've built -->
<Target
Name="_NotifySpotlight"
Expand Down
22 changes: 22 additions & 0 deletions tests/dotnet/NativeMergeableDylibReferencesApp/AppDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Runtime.InteropServices;

using Foundation;

namespace NativeMergeableDylibReferencesApp {
public class Program {
[DllImport ("libMergeableFramework.dylib")]
static extern int theUltimateAnswer ();

static int Main (string [] args)
{
Console.WriteLine ($"Mergeable Dynamic library: {theUltimateAnswer ()}");

GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly

Console.WriteLine (Environment.GetEnvironmentVariable ("MAGIC_WORD"));

return 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
2 changes: 2 additions & 0 deletions tests/dotnet/NativeMergeableDylibReferencesApp/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TOP=../../..
include $(TOP)/tests/common/shared-dotnet-test.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-ios</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-macos</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
20 changes: 20 additions & 0 deletions tests/dotnet/NativeMergeableDylibReferencesApp/shared.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<OutputType>Exe</OutputType>

<ApplicationTitle>NativeMergeableDylibReferencesApp</ApplicationTitle>
<ApplicationId>com.xamarin.nativemergeabledylibreferences</ApplicationId>
<ApplicationVersion>1.0</ApplicationVersion>
</PropertyGroup>

<Import Project="../../common/shared-dotnet.csproj" />

<ItemGroup>
<NativeReference Include="..\..\..\test-libraries\.libs\$(NativeLibName)\libMergeableFramework.dylib" Kind="Dynamic" />
</ItemGroup>

<ItemGroup>
<Compile Include="../*.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-tvos</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
2 changes: 2 additions & 0 deletions tests/dotnet/NativeMergeableFrameworkReferencesApp/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TOP=../../..
include $(TOP)/tests/common/shared-dotnet-test.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-ios</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-macos</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<OutputType>Exe</OutputType>

<ApplicationTitle>NativeMergeableFrameworkReferencesApp</ApplicationTitle>
<ApplicationId>com.xamarin.nativemergeableframeworkreferences</ApplicationId>
<ApplicationVersion>1.0</ApplicationVersion>
</PropertyGroup>

<Import Project="../../common/shared-dotnet.csproj" />

<ItemGroup>
<NativeReference Include="..\..\..\test-libraries\.libs\$(NativeLibName)\XMergeableTest.framework" Kind="Framework" />
</ItemGroup>

<ItemGroup>
<Compile Include="../*.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net$(BundledNETCoreAppTargetFrameworkVersion)-tvos</TargetFramework>
</PropertyGroup>
<Import Project="..\shared.csproj" />
</Project>
Loading