Skip to content

Commit

Permalink
[msbuild] Add ILStrip'ing for net6 applications. Fixes #11445. (#12563)
Browse files Browse the repository at this point in the history
- Controlled by EnableAssemblyILStripping which defaults to true
- Integration test included

Before - https://gist.github.com/chamons/c7886f7bacbc2e5ac5966e4251d13e71
After - https://gist.github.com/chamons/148e1bef22fa336f953f3d02dcf20667

859,136 -> 527,872 managed

Fixes #11445.
  • Loading branch information
chamons authored Oct 5, 2021
1 parent 7d1cd52 commit a300dfc
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 18 deletions.
38 changes: 38 additions & 0 deletions dotnet/targets/Xamarin.Shared.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<UsingTask TaskName="Xamarin.MacDev.Tasks.LinkNativeCode" AssemblyFile="$(_XamarinTaskAssembly)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.SymbolStrip" AssemblyFile="$(_XamarinTaskAssembly)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.MergeAppBundles" AssemblyFile="$(_XamarinTaskAssembly)" />
<UsingTask TaskName="Xamarin.MacDev.Tasks.ILStrip" AssemblyFile="$(_XamarinTaskAssembly)" />

<!-- Project types and how do we distinguish between them
Expand Down Expand Up @@ -209,6 +210,7 @@
<!-- We re-use ComputeFilesToPublish & CopyFilesToPublishDirectory to copy files to the .app -->
<!-- ComputeFilesToPublish will run ILLink -->
<!-- single-rid build (either plain single, or inner build for multi-rid build) -->
<!-- Note - _ComputeStripAssemblyIL must be before _StripAssemblyIL as msbuild DependsOn do not execute before Conditions are evaluated -->
<CreateAppBundleDependsOn Condition="'$(RuntimeIdentifiers)' == ''">
$(CreateAppBundleDependsOn);
_CopyResourcesToBundle;
Expand All @@ -226,6 +228,8 @@
_ComputeFrameworkFilesToPublish;
_ComputeDynamicLibrariesToPublish;
ComputeFilesToPublish;
_ComputeStripAssemblyIL;
_StripAssemblyIL;
_LoadLinkerOutput;
_CompileNativeExecutable;
_LinkNativeExecutable;
Expand Down Expand Up @@ -628,6 +632,40 @@
</ItemGroup>
</Target>

<Target Name="_ComputeStripAssemblyIL" Condition=" '$(EnableAssemblyILStripping)' == '' " DependsOnTargets="_ComputeVariables;ComputeFilesToPublish">
<PropertyGroup>
<!-- Don't strip IL by default -->
<EnableAssemblyILStripping>false</EnableAssemblyILStripping>

<!-- Strip if we are AOT and Release -->
<EnableAssemblyILStripping Condition="'$(_RunAotCompiler)' == 'true' And '$(Configuration)' == 'Release'">true</EnableAssemblyILStripping>

<!-- Don't strip if we are running the interpreter -->
<EnableAssemblyILStripping Condition="'$(MtouchInterpreter)' != ''">false</EnableAssemblyILStripping>
</PropertyGroup>
</Target>

<!-- The DependsOnTargets here will not force EnableAssemblyILStripping to be calculated before the condition is evaulated. The order in CreateAppBundleDependsOn matters. -->
<Target Name="_StripAssemblyIL" Condition="'$(EnableAssemblyILStripping)' == 'true'" DependsOnTargets="_ComputeStripAssemblyIL">
<PropertyGroup>
<_StrippedAssemblyDirectory>$(DeviceSpecificIntermediateOutputPath)\stripped</_StrippedAssemblyDirectory>
</PropertyGroup>
<MakeDir SessionId="$(BuildSessionId)" Condition="'$(IsMacEnabled)' == 'true'" Directories="$(_StrippedAssemblyDirectory)" />
<ItemGroup>
<_AssembliesToBeStripped Include="@(ResolvedFileToPublish)" Condition="'%(Extension)' == '.dll'">
<OutputPath>$(_StrippedAssemblyDirectory)\%(Filename)%(Extension)</OutputPath>
</_AssembliesToBeStripped>
</ItemGroup>
<Xamarin.MacDev.Tasks.ILStrip Assemblies="@(_AssembliesToBeStripped)" SessionId="$(BuildSessionId)">
<Output TaskParameter="StrippedAssemblies" PropertyName="_StrippedAssemblies" />
</Xamarin.MacDev.Tasks.ILStrip>
<ItemGroup>
<ResolvedFileToPublish Remove="@(_AssembliesToBeStripped)" />
<ResolvedFileToPublish Include="@(_StrippedAssemblies)" />
</ItemGroup>
</Target>


<Target Name="_LoadLinkerOutput" DependsOnTargets="ComputeFilesToPublish">
<!-- Load _MainFile -->
<ReadItemsFromFile SessionId="$(BuildSessionId)" File="$(_LinkerItemsDirectory)/_MainFile.items" Condition="Exists('$(_LinkerItemsDirectory)/_MainFile.items')">
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
<MicrosoftDotnetSdkInternalPackageVersion>6.0.100-rtm.21480.21</MicrosoftDotnetSdkInternalPackageVersion>
<MicrosoftNETILLinkTasksPackageVersion>6.0.100-1.21473.1</MicrosoftNETILLinkTasksPackageVersion>
<MicrosoftDotNetBuildTasksFeedPackageVersion>6.0.0-beta.21212.6</MicrosoftDotNetBuildTasksFeedPackageVersion>
<MicrosoftNETILStripTasksPackageVersion>6.0.0-rc.2.21468.3</MicrosoftNETILStripTasksPackageVersion>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions msbuild/ILMerge.targets
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<MergedAssemblies Include="@(ReferencePath)" Condition="'%(FileName)' == 'Xamarin.MacDev'" />
<MergedAssemblies Include="@(ReferencePath)" Condition="'%(FileName)' == 'DotNetZip'" />
<MergedAssemblies Include="@(ReferencePath)" Condition="'%(FileName)' == 'ILLink.Tasks'" />
<MergedAssemblies Include="@(ReferencePath)" Condition="'%(FileName)' == 'ILStrip'" />
<MergedAssemblies Include="@(ReferencePath)" Condition="'%(FileName)' == 'Newtonsoft.Json'" />
<MergedAssemblies Include="@(ReferencePath)" Condition="'%(FileName)' == 'Renci.SshNet'" />
<MergedAssemblies Include="@(ReferencePath)" Condition="'%(FileName)' == 'SshNet.Security.Cryptography'" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public abstract class ParseBundlerArgumentsTaskBase : XamarinTask {
public string Registrar { get; set; }

// This is input too
[Output]
public string NoStrip { get; set; }

[Output]
public int Verbosity { get; set; }

Expand Down Expand Up @@ -142,6 +145,10 @@ public override bool Execute ()
value = hasValue ? value : nextValue; // requires a value, which might be the next option
xml.Add (value);
break;
case "nostrip":
// Output is EnableAssemblyILStripping so we enable if --nostrip=false and disable if true
NoStrip = ParseBool (value) ? "false" : "true";
break;
default:
Log.LogMessage (MessageImportance.Low, "Skipping unknown argument '{0}' with value '{1}'", name, value);
break;
Expand Down
1 change: 1 addition & 0 deletions msbuild/Xamarin.Shared/Xamarin.Shared.targets
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
<Output TaskParameter="Registrar" PropertyName="_BundlerRegistrar" />
<Output TaskParameter="Verbosity" PropertyName="_BundlerVerbosity" />
<Output TaskParameter="XmlDefinitions" ItemName="_BundlerXmlDefinitions" />
<Output TaskParameter="NoStrip" PropertyName="EnableAssemblyILStripping" />
</ParseBundlerArguments>

<PropertyGroup>
Expand Down
35 changes: 35 additions & 0 deletions msbuild/Xamarin.iOS.Tasks.Core/Tasks/ILStripBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

// Normally this would be ILStrip.Tasks but ILStrip is in the global namespace
// And having a type and the parent namespace have the same name really confuses the compiler
namespace ILStripTasks {
public class ILStripBase : ILStrip
{
public string SessionId { get; set; }

[Output]
public ITaskItem [] StrippedAssemblies { get; set; }

public override bool Execute ()
{
var result = base.Execute ();

var stripedItems = new List<ITaskItem> ();

if (result)
{
foreach (var item in Assemblies)
{
stripedItems.Add (new TaskItem (item.GetMetadata("OutputPath")));
}
}

StrippedAssemblies = stripedItems.ToArray();

return result;
}
}
}
5 changes: 5 additions & 0 deletions msbuild/Xamarin.iOS.Tasks.Core/Xamarin.iOS.Tasks.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="16.8.0" IncludeAssets="compile" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.8.0" IncludeAssets="compile" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="$(MicrosoftNETILLinkTasksPackageVersion)" />
<PackageReference Include="Microsoft.NET.Runtime.MonoTargets.Sdk" Version="$(MicrosoftNETILStripTasksPackageVersion)" GeneratePathProperty="true"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\external\Xamarin.MacDev\Xamarin.MacDev\Xamarin.MacDev.csproj" />
Expand All @@ -24,6 +25,10 @@
<!-- We need the net472 impl, otherwise the Build agent needs to be a net5.0 app -->
<HintPath>$(PkgMicrosoft_NET_ILLink_Tasks)\tools\net472\ILLink.Tasks.dll</HintPath>
</Reference>
<Reference Include="ILStrip">
<!-- We need the net472 impl, otherwise the Build agent needs to be a net5.0 app -->
<HintPath>$(PkgMicrosoft_NET_Runtime_MonoTargets_Sdk)\tasks\net472\ILStrip.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="NoCode.cs">
Expand Down
25 changes: 25 additions & 0 deletions msbuild/Xamarin.iOS.Tasks/Tasks/ILStrip.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.Tasks;
using Microsoft.Build.Framework;
using Xamarin.Messaging.Build.Client;

namespace Xamarin.MacDev.Tasks
{
public class ILStrip : ILStripTasks.ILStripBase, ITaskCallback
{
public override bool Execute ()
{
if (this.ShouldExecuteRemotely (SessionId))
return new TaskRunner (SessionId, BuildEngine4).RunAsync (this).Result;

return base.Execute ();
}

public bool ShouldCopyToBuildServer (ITaskItem item) => false;

public bool ShouldCreateOutputFile (ITaskItem item) => true;

public IEnumerable<ITaskItem> GetAdditionalItemsToBeCopied () => Enumerable.Empty<ITaskItem> ();
}
}
5 changes: 5 additions & 0 deletions msbuild/Xamarin.iOS.Tasks/Xamarin.iOS.Tasks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="16.8.0" IncludeAssets="$(IncludeMSBuildAssets)" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.8.0" IncludeAssets="$(IncludeMSBuildAssets)" />
<PackageReference Include="Microsoft.NET.ILLink.Tasks" Version="$(MicrosoftNETILLinkTasksPackageVersion)" />
<PackageReference Include="Microsoft.NET.Runtime.MonoTargets.Sdk" Version="$(MicrosoftNETILStripTasksPackageVersion)" GeneratePathProperty="true"/>
<PackageReference Include="Xamarin.Messaging.Build.Client" Version="$(MessagingVersion)" />
</ItemGroup>

Expand All @@ -32,6 +33,10 @@
<Reference Include="ILLink.Tasks">
<HintPath>$(PkgMicrosoft_NET_ILLink_Tasks)\tools\net472\ILLink.Tasks.dll</HintPath>
</Reference>
<Reference Include="ILStrip">
<!-- We need the net472 impl, otherwise the Build agent needs to be a net5.0 app -->
<HintPath>$(PkgMicrosoft_NET_Runtime_MonoTargets_Sdk)\tasks\net472\ILStrip.dll</HintPath>
</Reference>
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 3 additions & 5 deletions tests/dotnet/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,10 @@ build-dotnet: $(TARGETS)
$(DOTNET6) build size-comparison/MySingleView/dotnet/MySingleView.csproj --runtime ios-arm64 $(COMMON_ARGS) /bl:$@.binlog $(MSBUILD_VERBOSITY)

run-dotnet: $(TARGETS)
$(DOTNET6) build -t:Run size-comparison/MySingleView/dotnet/MySingleView.csproj --runtime ios-arm64 $(COMMON_ARGS)
$(DOTNET6) build -t:Run size-comparison/MySingleView/dotnet/MySingleView.csproj --runtime ios-arm64 $(COMMON_ARGS) /bl:$@.binlog $(MSBUILD_VERBOSITY)

# this will break the signature, so app won't run anymore. Use it only to compare final size w/legacy
# https://github.com/xamarin/xamarin-macios/issues/11445
strip-dotnet:
$(foreach file, $(wildcard size-comparison/MySingleView/dotnet/bin/iPhone/Release/net6.0-ios/ios-arm64/MySingleView.app/*.dll), mono-cil-strip $(file) $(file);)
run-dotnet-sim: $(TARGETS)
$(DOTNET6) build -t:Run size-comparison/MySingleView/dotnet/MySingleView.csproj /p:Configuration=Release --runtime iossimulator-x64 /p:Platform=iPhoneSimulator /bl:$@.binlog

# this target will copy NuGet.config and global.json to the directories that need it for their .NET build to work correctly.
copy-dotnet-config: $(TARGETS)
Expand Down
9 changes: 1 addition & 8 deletions tests/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,4 @@ Add this option inside the `Release|iPhone` configuration of `size-comparison/My

* net6

**IL stripping is not yet available** so there's nothing to disable right now.

If you want to compare (trimmed) size you can manually call `mono-cil-strip`
on each assembly inside the app bundle.

`make strip-dotnet` will remove the IL from the dotnet app version.
However this is done after the code signature so it will not be possible
to deploy and execute the app afterward. Use for binary analysis only!
Build with `/p:EnableAssemblyILStripping=false` set. The `MtouchExtraArgs` legacy option is also honored.
26 changes: 25 additions & 1 deletion tests/dotnet/UnitTests/PostBuildTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

using Microsoft.Build.Framework;
using Microsoft.Build.Logging.StructuredLogger;
using Mono.Cecil;

namespace Xamarin.Tests {
[TestFixture]
Expand Down Expand Up @@ -50,15 +51,38 @@ public void BuildIpaTest (ApplePlatform platform, string runtimeIdentifiers)
var project = "MySimpleApp";
Configuration.IgnoreIfIgnoredPlatform (platform);

var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath);
var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath, configuration: "Release");
Clean (project_path);
var properties = GetDefaultProperties (runtimeIdentifiers);
properties ["BuildIpa"] = "true";
properties ["Configuration"] = "Release";

DotNet.AssertBuild (project_path, properties);

var pkgPath = Path.Combine (appPath, "..", $"{project}.ipa");
Assert.That (pkgPath, Does.Exist, "pkg creation");

AssertBundleAssembliesStripStatus (appPath, true);
}

[Test]
[TestCase (ApplePlatform.iOS, "ios-arm64", true)]
[TestCase (ApplePlatform.iOS, "ios-arm64", false)]
public void AssemblyStripping (ApplePlatform platform, string runtimeIdentifiers, bool shouldStrip)
{
var project = "MySimpleApp";
Configuration.IgnoreIfIgnoredPlatform (platform);

var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath);
Clean (project_path);
var properties = GetDefaultProperties (runtimeIdentifiers);

// Force EnableAssemblyILStripping since we are building debug which never will by default
properties ["EnableAssemblyILStripping"] = shouldStrip ? "true" : "false";

DotNet.AssertBuild (project_path, properties);

AssertBundleAssembliesStripStatus (appPath, shouldStrip);
}

[Test]
Expand Down
26 changes: 22 additions & 4 deletions tests/dotnet/UnitTests/TestBaseClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ protected void SetRuntimeIdentifiers (Dictionary<string, string> properties, str
properties [multiRid] = runtimeIdentifiers;
}

protected string GetProjectPath (string project, string runtimeIdentifiers, ApplePlatform platform, out string appPath, string? subdir = null)
protected string GetProjectPath (string project, string runtimeIdentifiers, ApplePlatform platform, out string appPath, string? subdir = null, string configuration = "Debug")
{
return GetProjectPath (project, null, runtimeIdentifiers, platform, out appPath);
return GetProjectPath (project, null, runtimeIdentifiers, platform, out appPath, configuration);
}

protected string GetProjectPath (string project, string? subdir, string runtimeIdentifiers, ApplePlatform platform, out string appPath)
protected string GetProjectPath (string project, string? subdir, string runtimeIdentifiers, ApplePlatform platform, out string appPath, string configuration = "Debug")
{
var rv = GetProjectPath (project, subdir, platform);
if (string.IsNullOrEmpty (runtimeIdentifiers))
runtimeIdentifiers = GetDefaultRuntimeIdentifier (platform);
var appPathRuntimeIdentifier = runtimeIdentifiers.IndexOf (';') >= 0 ? "" : runtimeIdentifiers;
appPath = Path.Combine (Path.GetDirectoryName (rv)!, "bin", "Debug", platform.ToFramework (), appPathRuntimeIdentifier, project + ".app");
appPath = Path.Combine (Path.GetDirectoryName (rv)!, "bin", configuration, platform.ToFramework (), appPathRuntimeIdentifier, project + ".app");
return rv;
}

Expand Down Expand Up @@ -141,6 +141,24 @@ protected string GetInfoPListPath (ApplePlatform platform, string app_directory)
}
}

protected void AssertBundleAssembliesStripStatus (string appPath, bool shouldStrip)
{
var assemblies = Directory.GetFiles (appPath, "*.dll", SearchOption.AllDirectories);
var assembliesWithOnlyEmptyMethods = new List<String> ();
foreach (var assembly in assemblies) {
ModuleDefinition definition = ModuleDefinition.ReadModule (assembly, new ReaderParameters { ReadingMode = ReadingMode.Deferred });

bool onlyHasEmptyMethods = definition.Assembly.MainModule.Types.All (t =>
t.Methods.Where (m => m.HasBody).All (m => m.Body.Instructions.Count == 1));
if (onlyHasEmptyMethods) {
assembliesWithOnlyEmptyMethods.Add (assembly);
}
}

// Some assemblies, such as Facades, will be completely empty even when not stripped
Assert.That (assemblies.Length == assembliesWithOnlyEmptyMethods.Count, Is.EqualTo (shouldStrip), $"Unexpected stripping status: of {assemblies.Length} assemblies {assembliesWithOnlyEmptyMethods.Count} were empty.");
}

protected string GetNativeExecutable (ApplePlatform platform, string app_directory)
{
var executableName = Path.GetFileNameWithoutExtension (app_directory);
Expand Down

4 comments on commit a300dfc

@vs-mobiletools-engineering-service2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 Tests failed catastrophically on Build (no summary found). 🔥

Result file $(TEST_SUMMARY_PATH) not found.

Pipeline on Agent
[msbuild] Add ILStrip'ing for net6 applications. Fixes #11445. (#12563)

@vs-mobiletools-engineering-service2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ [CI Build] Tests failed on Build ❌

Tests failed on Build.

API diff

✅ API Diff from stable

View API diff

API & Generator diff

API Diff (from PR only) (no change)
Generator Diff (only version changes)

Packages generated

View packages

Test results

2 tests failed, 216 tests passed.

Failed tests

  • introspection/Mac Catalyst [dotnet]/Debug [dotnet]: TimedOut (Execution timed out after 1200 seconds.
    No test log file was produced)
  • dont link/Mac Catalyst [dotnet]/Release [dotnet]: Failed (Test run crashed (exit code: 134).
    Tests run: 11 Passed: 6 Inconclusive: 0 Failed: 0 Ignored: 5)

Pipeline on Agent XAMBOT-1038.BigSur'
[msbuild] Add ILStrip'ing for net6 applications. Fixes #11445. (#12563)

@vs-mobiletools-engineering-service2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Tests were not ran (VSTS: device tests tvOS). ⚠️

Results were skipped for this run due to provisioning problems Azure Devops. Please contact the bot administrator.

Pipeline on Agent
[msbuild] Add ILStrip'ing for net6 applications. Fixes #11445. (#12563)

@vs-mobiletools-engineering-service2
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Tests were not ran (VSTS: device tests iOS). ⚠️

Results were skipped for this run due to provisioning problems Azure Devops. Please contact the bot administrator.

Pipeline on Agent
[msbuild] Add ILStrip'ing for net6 applications. Fixes #11445. (#12563)

Please sign in to comment.