diff --git a/dotnet/targets/Xamarin.Shared.Sdk.targets b/dotnet/targets/Xamarin.Shared.Sdk.targets
index 02926aa4ed8c..ec3045cc5df3 100644
--- a/dotnet/targets/Xamarin.Shared.Sdk.targets
+++ b/dotnet/targets/Xamarin.Shared.Sdk.targets
@@ -271,6 +271,7 @@
_ResolveAppExtensionReferences;
_ExtendAppExtensionReferences;
_ComputeLinkerArguments;
+ _PrepareAssemblies;
_ComputeFrameworkFilesToPublish;
_ComputeDynamicLibrariesToPublish;
ComputeFilesToPublish;
@@ -633,6 +634,7 @@
AOTOutputDirectory=$(_AOTOutputDirectory)
DedupAssembly=$(_DedupAssembly)
AppBundleManifestPath=$(_AppBundleManifestPath)
+ AssemblyPublishDir=$(_AssemblyPublishDir)
CacheDirectory=$(_LinkerCacheDirectory)
@(_BundlerDlsym -> 'Dlsym=%(Identity)')
Debug=$(_BundlerDebug)
@@ -662,6 +664,8 @@
PartialStaticRegistrarLibrary=$(_LibPartialStaticRegistrar)
Platform=$(_PlatformName)
PlatformAssembly=$(_PlatformAssemblyName).dll
+ PrepareAssemblies=$(PrepareAssemblies)
+ PublishTrimmed=$(PublishTrimmed)
RelativeAppBundlePath=$(_RelativeAppBundlePath)
Registrar=$(Registrar)
@(ReferenceNativeSymbol -> 'ReferenceNativeSymbol=%(SymbolType):%(SymbolMode):%(Identity)')
@@ -674,9 +678,11 @@
SkipMarkingNSObjectsInUserAssemblies=$(_SkipMarkingNSObjectsInUserAssemblies)
TargetArchitectures=$(TargetArchitectures)
TargetFramework=$(_ComputedTargetFrameworkMoniker)
+ TrimMode=$(TrimMode)
TypeMapAssemblyName=$(_TypeMapAssemblyName)
TypeMapFilePath=$(_TypeMapFilePath)
TypeMapOutputDirectory=$(_TypeMapOutputDirectory)
+ UnmanagedCallersOnlyMapPath=$(_UnmanagedCallersOnlyMapPath)
UseLlvm=$(MtouchUseLlvm)
Verbosity=$(_BundlerVerbosity)
Warn=$(_BundlerWarn)
@@ -784,44 +790,45 @@
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.CollectAssembliesStep" />
+
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.CoreTypeMapStep" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.ProcessExportedFields" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.PreserveProtocolsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' == 'true'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' == 'true'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveBlockCodeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' == 'true'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.OptimizeGeneratedCodeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' == 'true'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.ApplyPreserveAttributeStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseLinkDescriptionForApplyPreserveAttribute)' == 'true'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.MarkForStaticRegistrarStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkStaticRegistrar)' == 'true'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.MarkNSObjectsStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkNSObjects)' == 'true'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineDlfcnMethodsStep" Condition="'$(InlineDlfcnMethods)' != ''" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.PreserveProtocolsStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' == 'true'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' == 'true'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreserveBlockCodeStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' == 'true'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.OptimizeGeneratedCodeStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' == 'true'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.ApplyPreserveAttributeStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseLinkDescriptionForApplyPreserveAttribute)' == 'true'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.MarkForStaticRegistrarStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkStaticRegistrar)' == 'true'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.MarkNSObjectsStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkNSObjects)' == 'true'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineDlfcnMethodsStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(InlineDlfcnMethods)' != ''" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.RegistrarRemovalTrackingStep" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.RegistrarRemovalTrackingStep" Condition="'$(PrepareAssemblies)' != 'true'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PreMarkDispatcher" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.ManagedRegistrarStep" Condition="'$(Registrar)' == 'managed-static' Or '$(Registrar)' == 'trimmable-static'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.TrimmableRegistrarStep" Condition="'$(Registrar)' == 'trimmable-static'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineClassGetHandleStep" Condition="'$(InlineClassGetHandle)' != '' And '$(InlineClassGetHandle)' != 'disabled'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.PreMarkDispatcher" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.ManagedRegistrarStep" Condition="'$(PrepareAssemblies)' != 'true' And ('$(Registrar)' == 'managed-static' Or '$(Registrar)' == 'trimmable-static')" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.TrimmableRegistrarStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(Registrar)' == 'trimmable-static'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="Xamarin.Linker.Steps.InlineClassGetHandleStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(InlineClassGetHandle)' != '' And '$(InlineClassGetHandle)' != 'disabled'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveBlockCodeHandler" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' != 'true'" Type="Xamarin.Linker.OptimizeGeneratedCodeHandler" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.BackingFieldDelayHandler" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' != 'true'" Type="Xamarin.Linker.MarkIProtocolHandler" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForBlockCodePreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveBlockCodeHandler" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForGeneratedCodeOptimizations)' != 'true'" Type="Xamarin.Linker.OptimizeGeneratedCodeHandler" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.BackingFieldDelayHandler" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForProtocolPreservation)' != 'true'" Type="Xamarin.Linker.MarkIProtocolHandler" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkDispatcher)' != 'true'" Type="Xamarin.Linker.Steps.MarkDispatcher" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForMarkDispatcher)' != 'true'" Type="Xamarin.Linker.Steps.MarkDispatcher" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true' And '$(_UseDynamicDependenciesForSmartEnumPreservation)' != 'true'" Type="Xamarin.Linker.Steps.PreserveSmartEnumConversionsHandler" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="SweepStep" Type="Xamarin.Linker.ManagedRegistrarLookupTablesStep" Condition="'$(Registrar)' == 'managed-static'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="SweepStep" Type="Xamarin.Linker.ManagedRegistrarLookupTablesStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(Registrar)' == 'managed-static'" />
- <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" AfterStep="SweepStep" Condition="'$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PostSweepDispatcher" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" AfterStep="SweepStep" Condition="'$(PrepareAssemblies)' != 'true' And '$(_AreAnyAssembliesTrimmed)' == 'true'" Type="Xamarin.Linker.Steps.PostSweepDispatcher" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.ManagedRegistrarStep" Condition="'$(PrepareAssemblies)' == 'true' And ('$(Registrar)' == 'managed-static' Or '$(Registrar)' == 'trimmable-static')" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.TrimmableRegistrarStep" Condition="'$(PrepareAssemblies)' == 'true' And '$(Registrar)' == 'trimmable-static'" />
+ <_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.ManagedRegistrarLookupTablesStep" Condition="'$(PrepareAssemblies)' == 'true' And '$(Registrar)' == 'managed-static'" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.Linker.RegistrarStep" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.GenerateMainStep" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" Type="Xamarin.GenerateReferencesStep" />
@@ -1328,6 +1338,9 @@
<_TypeMapFilePath Condition="'$(_TypeMapFilePath)' == ''">$(DeviceSpecificIntermediateOutputPath)type-map.txt
+
+
+ <_UnmanagedCallersOnlyMapPath Condition="'$(_UnmanagedCallersOnlyMapPath)' == ''">$(DeviceSpecificIntermediateOutputPath)unmanaged_callers_only_map.txt
@@ -1413,13 +1426,27 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ <_IntermediateAssemblyProperty>@(IntermediateAssembly)
+ <_PreparedIntermediateAssemblyProperty>@(PreparedIntermediateAssembly->WithMetadataValue('BeforePrepareAssembliesPath','$(_IntermediateAssemblyProperty)'))
+
+
+ <_PreparedRootedIntermediateAssembly Include="@(TrimmerRootAssembly->'$(_PreparedIntermediateAssemblyProperty)')" Condition="'%(Identity)' == '$(_IntermediateAssemblyProperty)'" />
+
+
+
+
diff --git a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx
index 5de6bb55b800..0741be004f37 100644
--- a/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx
+++ b/msbuild/Xamarin.Localization.MSBuild/MSBStrings.resx
@@ -1661,4 +1661,9 @@
Shown when the tool reports a simulator runtime version mismatch.
{0} - The platform name (e.g. "iOS" or "tvOS").
+
+ Console.StandardOutput or Console.StandardError was accessed during a build task. This should not happen, use the MSBuild logging infrastructure instead. Stack trace: {0}
+ Shown when an MSBuild task writes to the console instead of using MSBuild logging.
+{0} - The stack trace of the offending call.
+
diff --git a/msbuild/Xamarin.MacDev.Tasks.slnx b/msbuild/Xamarin.MacDev.Tasks.slnx
index e3c799a54619..1f17eb9aa95f 100644
--- a/msbuild/Xamarin.MacDev.Tasks.slnx
+++ b/msbuild/Xamarin.MacDev.Tasks.slnx
@@ -36,4 +36,8 @@
+
+
+
+
diff --git a/msbuild/Xamarin.MacDev.Tasks/ConsoleToTaskWriter.cs b/msbuild/Xamarin.MacDev.Tasks/ConsoleToTaskWriter.cs
new file mode 100644
index 000000000000..1e97dc541697
--- /dev/null
+++ b/msbuild/Xamarin.MacDev.Tasks/ConsoleToTaskWriter.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO;
+using System.Text;
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+#nullable enable
+
+namespace Xamarin.Utils;
+
+class ConsoleToTaskWriter : TextWriter {
+ TaskLoggingHelper helper;
+ bool errorShown;
+
+ public ConsoleToTaskWriter (TaskLoggingHelper helper)
+ {
+ this.helper = helper;
+ }
+
+ public override Encoding Encoding => Encoding.UTF8;
+
+ public override void Write (char value)
+ {
+ ShowError ();
+ helper.LogMessage (MessageImportance.Low, value.ToString ());
+ }
+
+ public override void Write (char [] buffer, int index, int count)
+ {
+ ShowError ();
+ helper.LogMessage (MessageImportance.Low, new string (buffer, index, count));
+ }
+
+ public override void Write (string? value)
+ {
+ ShowError ();
+ helper.LogMessage (MessageImportance.Low, value ?? string.Empty);
+ }
+
+ public override void WriteLine ()
+ {
+ ShowError ();
+ }
+
+ public override void WriteLine (string? value)
+ {
+ ShowError ();
+ helper.LogMessage (MessageImportance.Low, value ?? string.Empty);
+ }
+
+ void ShowError ()
+ {
+ if (errorShown)
+ return;
+ errorShown = true;
+
+ helper.LogError (null, "MT7178" /* Console.StandardOutput or Console.StandardError was accessed during a build task. This should not happen, use the MSBuild logging infrastructure instead. Stack trace: {0} */, null, null, 0, 0, 0, 0, Xamarin.Localization.MSBuild.MSBStrings.E7178, Environment.StackTrace);
+ }
+
+ public static IDisposable EnsureNoConsoleUsage (TaskLoggingHelper log)
+ {
+ return new NoConsoleUsage (new ConsoleToTaskWriter (log));
+ }
+
+ class NoConsoleUsage : IDisposable {
+ TextWriter? originalStdout;
+ TextWriter? originalStderr;
+
+ public NoConsoleUsage (ConsoleToTaskWriter redirector)
+ {
+ originalStdout = Console.Out;
+ originalStderr = Console.Error;
+ Console.SetOut (redirector);
+ Console.SetError (redirector);
+ }
+
+ ~NoConsoleUsage ()
+ {
+ Restore ();
+ }
+
+ void IDisposable.Dispose ()
+ {
+ Restore ();
+ GC.SuppressFinalize (this);
+ }
+
+ void Restore ()
+ {
+ if (originalStdout is not null) {
+ Console.SetOut (originalStdout);
+ originalStdout = null;
+ }
+ if (originalStderr is not null) {
+ Console.SetError (originalStderr);
+ originalStderr = null;
+ }
+ }
+ }
+}
diff --git a/msbuild/Xamarin.MacDev.Tasks/ErrorHelper.msbuild.cs b/msbuild/Xamarin.MacDev.Tasks/ErrorHelper.msbuild.cs
deleted file mode 100644
index 8bda5c48e690..000000000000
--- a/msbuild/Xamarin.MacDev.Tasks/ErrorHelper.msbuild.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright 2021, Microsoft Corp. All rights reserved,
-
-using System.Collections.Generic;
-
-using Xamarin.Utils;
-
-#nullable enable
-
-namespace Xamarin.Bundler {
- public static partial class ErrorHelper {
- public static ApplePlatform Platform;
-
- internal static string GetPrefix (IToolLog? log)
- {
- return Xamarin.MacDev.Tasks.LoggingExtensions.ErrorPrefix;
- }
-
- public enum WarningLevel {
- Error = -1,
- Warning = 0,
- Disable = 1,
- }
-
- static Dictionary? warning_levels;
-
- public static WarningLevel GetWarningLevel (IToolLog log, int code)
- {
- WarningLevel level;
-
- if (warning_levels is null)
- return WarningLevel.Warning;
-
- // code -1: all codes
- if (warning_levels.TryGetValue (-1, out level))
- return level;
-
- if (warning_levels.TryGetValue (code, out level))
- return level;
-
- return WarningLevel.Warning;
- }
-
- public static void SetWarningLevel (WarningLevel level, int? code = null /* if null, apply to all warnings */)
- {
- if (warning_levels is null)
- warning_levels = new Dictionary ();
- if (code.HasValue) {
- warning_levels [code.Value] = level;
- } else {
- warning_levels [-1] = level; // code -1: all codes.
- }
- }
- }
-}
diff --git a/msbuild/Xamarin.MacDev.Tasks/LoggingExtensions.cs b/msbuild/Xamarin.MacDev.Tasks/LoggingExtensions.cs
index 367f3f5bd46a..2f0f9b7a5538 100644
--- a/msbuild/Xamarin.MacDev.Tasks/LoggingExtensions.cs
+++ b/msbuild/Xamarin.MacDev.Tasks/LoggingExtensions.cs
@@ -74,6 +74,19 @@ public static void LogTaskProperty (this TaskLoggingHelper log, string propertyN
log.LogMessage (TaskPropertyImportance, " {0}: {1}", propertyName, value);
}
+ ///
+ /// Creates an MSBuild error following our MTErrors convention.
+ ///
+ /// For every new error we need to update "docs/website/mtouch-errors.md" and "tools/mtouch/error.cs".
+ /// In the 7xxx range for MSBuild error.
+ /// The error's message to be displayed in the error pad.
+ /// Path to the known guilty file or null.
+ /// Line number in the file where the error was found, or 0 if unknown.
+ public static void LogError (this TaskLoggingHelper log, int errorCode, string? fileName, int lineNumber, string message, params object? [] args)
+ {
+ log.LogError (null, $"{ErrorPrefix}{errorCode}", null, fileName ?? "MSBuild", lineNumber, 0, 0, 0, message, args);
+ }
+
///
/// Creates an MSBuild error following our MTErrors convention.
///
@@ -91,6 +104,16 @@ public static void LogWarning (this TaskLoggingHelper log, int errorCode, string
log.LogWarning (null, $"{ErrorPrefix}{errorCode}", null, fileName ?? "MSBuild", 0, 0, 0, 0, message, args);
}
+ public static void LogWarning (this TaskLoggingHelper log, int errorCode, string? fileName, int lineNumber, string message, params object? [] args)
+ {
+ log.LogWarning (null, $"{ErrorPrefix}{errorCode}", null, fileName ?? "MSBuild", lineNumber, 0, 0, 0, message, args);
+ }
+
+ public static void LogMessage (this TaskLoggingHelper log, MessageImportance importance, int errorCode, string? fileName, int lineNumber, string message, params object? [] args)
+ {
+ log.LogMessage (null, $"{ErrorPrefix}{errorCode}", null, fileName ?? "MSBuild", lineNumber, 0, 0, 0, importance, message, args);
+ }
+
public static bool LogErrorsFromException (this TaskLoggingHelper log, Exception exception, bool showStackTrace = true, bool showDetail = true)
{
var exceptions = new List ();
diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareAssemblies.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareAssemblies.cs
new file mode 100644
index 000000000000..8be422665932
--- /dev/null
+++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareAssemblies.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+using Xamarin.Build;
+using Xamarin.Bundler;
+using Xamarin.Utils;
+
+#nullable enable
+
+namespace Xamarin.MacDev.Tasks {
+ public class PrepareAssemblies : XamarinTask {
+ const string ErrorPrefix = "MX";
+
+ #region Inputs
+ [Required]
+ public ITaskItem [] InputAssemblies { get; set; } = [];
+
+ public string MakeReproPath { get; set; } = "";
+
+ public string OutputDirectory { get; set; } = "";
+
+ [Required]
+ public ITaskItem? OptionsFile { get; set; }
+ #endregion
+
+ #region Outputs
+ [Output]
+ public ITaskItem [] OutputAssemblies { get; set; } = [];
+ #endregion
+
+ Dictionary map = new ();
+
+ AssemblyPreparerInfo GetAssemblyInfo (ITaskItem item)
+ {
+ var inputPath = item.ItemSpec;
+ var outputPath = Path.Combine (OutputDirectory, Path.GetFileName (inputPath));
+ var isTrimmableString = item.GetMetadata ("IsTrimmable");
+ var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase);
+ var trimMode = item.GetMetadata ("TrimMode");
+ var rv = new AssemblyPreparerInfo (inputPath, outputPath, isTrimmable, trimMode);
+ map [rv] = item;
+ return rv;
+ }
+
+ public override bool Execute ()
+ {
+ // Capture Console usage and show an error if anything uses Console.[Error.]Write*
+ using var consoleToLog = ConsoleToTaskWriter.EnsureNoConsoleUsage (Log);
+
+ try {
+ var infos = InputAssemblies.Select (GetAssemblyInfo).ToArray ();
+ using var preparer = new AssemblyPreparer (this, infos, OptionsFile?.ItemSpec ?? "");
+ preparer.MakeReproPath = MakeReproPath;
+ var rv = preparer.Prepare (out var exceptions);
+
+ foreach (var pe in exceptions) {
+ if (pe.IsError (this)) {
+ ((IToolLog) this).LogError (pe);
+ } else {
+ ((IToolLog) this).LogWarning (pe);
+ }
+ }
+
+ var outputAssemblies = preparer.Assemblies.Select (v => {
+ var item = new TaskItem (v.OutputPath);
+ map [v].CopyMetadataTo (item);
+ item.SetMetadata ("BeforePrepareAssembliesPath", v.InputPath);
+ return (ITaskItem) item;
+ }).ToList ();
+
+ outputAssemblies.AddRange (preparer.AddedAssemblies.Select (v => {
+ var rv = new TaskItem (v.Path);
+ rv.SetMetadata ("PostprocessAssembly", "true");
+ rv.SetMetadata ("RelativePath", preparer.Configuration.AssemblyPublishDir + Path.GetFileName (v.Path));
+ if (v.OriginatingAssembly is not null) {
+ var originatingItem = map.SingleOrDefault (kvp => Path.GetFileName (kvp.Key.InputPath) == Path.GetFileName (v.OriginatingAssembly)).Value;
+ if (originatingItem is null) {
+ Log.LogMessage (MessageImportance.Low, $"Could not find originating assembly for {v.Path} with originating assembly name {v.OriginatingAssembly}");
+ } else {
+ var metadata = originatingItem.MetadataNames.Cast ().ToList ();
+ if (metadata.Contains ("TrimMode"))
+ rv.SetMetadata ("TrimMode", originatingItem.GetMetadata ("TrimMode"));
+ if (metadata.Contains ("IsTrimmable"))
+ rv.SetMetadata ("IsTrimmable", originatingItem.GetMetadata ("IsTrimmable"));
+ }
+ }
+ return rv;
+ }));
+
+ OutputAssemblies = outputAssemblies.ToArray ();
+ return rv && !Log.HasLoggedErrors;
+ } catch (Exception e) {
+ ((IToolLog) this).LogException (e);
+ return false;
+ }
+ }
+ }
+}
diff --git a/msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs b/msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs
index 3cd5a9386821..e50cee0ce94a 100644
--- a/msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs
+++ b/msbuild/Xamarin.MacDev.Tasks/Tasks/XamarinTask.cs
@@ -13,7 +13,6 @@
using Xamarin.Localization.MSBuild;
using Xamarin.Messaging.Build.Client;
using Xamarin.Utils;
-using static Xamarin.Bundler.FileCopier;
#nullable enable
@@ -246,7 +245,7 @@ internal static bool ExecuteRemotely (T task) where T : Task, IHasSessionId
}
#if NET
- internal static bool ExecuteRemotely (T task, [NotNullWhen (true)] out TaskRunner? taskRunner, Action? preprocessTaskRunner = null) where T: Task, IHasSessionId
+ internal static bool ExecuteRemotely (T task, [NotNullWhen (true)] out TaskRunner? taskRunner, Action? preprocessTaskRunner = null) where T : Task, IHasSessionId
#else
internal static bool ExecuteRemotely (T task, out TaskRunner taskRunner, Action? preprocessTaskRunner = null) where T : Task, IHasSessionId
#endif
@@ -371,8 +370,11 @@ void ICustomLogger.LogError (string message, Exception? ex)
{
if (!string.IsNullOrEmpty (message))
Log.LogError (message);
- if (ex is not null)
+ if (ex is ProductException pe) {
+ LogDiagnostic (pe);
+ } else if (ex is not null) {
Log.LogErrorFromException (ex);
+ }
}
void ICustomLogger.LogWarning (string messageFormat, params object? [] args)
@@ -404,12 +406,37 @@ void IToolLog.LogError (string message)
void IToolLog.LogException (Exception exception)
{
- ((ICustomLogger) this).LogError ("", exception);
+ if (exception is ProductException pe) {
+ LogDiagnostic (pe);
+ } else {
+ ((ICustomLogger) this).LogError ($"Unexpected exception '{GetType ().Name}': {exception.Message}", exception);
+ }
+ }
+
+ void IToolLog.LogError (ProductException exception)
+ {
+ LogDiagnostic (exception);
}
- void IToolLog.LogError (Exception exception)
+ void IToolLog.LogWarning (ProductException exception)
{
- ((ICustomLogger) this).LogError ("", exception);
+ LogDiagnostic (exception);
+ }
+
+ protected void LogDiagnostic (ProductException exception)
+ {
+ switch (exception.GetWarningLevel (this)) {
+ case ErrorHelper.WarningLevel.Warning:
+ Log.LogWarning (exception.Code, exception.FileName, exception.LineNumber, exception.Message);
+ break;
+ case ErrorHelper.WarningLevel.Error:
+ Log.LogError (exception.Code, exception.FileName, exception.LineNumber, exception.Message);
+ break;
+ case ErrorHelper.WarningLevel.Disable:
+ default:
+ Log.LogMessage (MessageImportance.Low, exception.Code, exception.FileName, exception.LineNumber, exception.Message);
+ break;
+ }
}
#endregion
}
diff --git a/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj b/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj
index 713db306ebef..cbd090125523 100644
--- a/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj
+++ b/msbuild/Xamarin.MacDev.Tasks/Xamarin.MacDev.Tasks.csproj
@@ -1,7 +1,8 @@
- netstandard2.0;net$(BundledNETCoreAppTargetFrameworkVersion)
+ net$(BundledNETCoreAppTargetFrameworkVersion);netstandard2.0
+ net$(BundledNETCoreAppTargetFrameworkVersion)
false
compile
true
@@ -37,6 +38,7 @@
+
ProjectReference
@@ -69,45 +71,15 @@
-
- ApplePlatform.cs
-
- JsonExtensions.cs
-
-
- IToolLog.cs
+ external\JsonExtensions.cs
StringUtils.cs
-
- Symbols.cs
-
-
- FileCopier.cs
-
-
- TargetFramework.cs
-
-
- Execution.cs
-
-
- PathUtils.cs
-
PListExtensions.cs
-
- MachO.cs
-
-
- error.cs
-
-
- ErrorHelper.cs
-
external\RuntimeException.cs
diff --git a/msbuild/Xamarin.Shared/Xamarin.Shared.targets b/msbuild/Xamarin.Shared/Xamarin.Shared.targets
index 8307a85ebf48..84749fe8f117 100644
--- a/msbuild/Xamarin.Shared/Xamarin.Shared.targets
+++ b/msbuild/Xamarin.Shared/Xamarin.Shared.targets
@@ -84,6 +84,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
+
@@ -3395,6 +3396,54 @@ Copyright (C) 2018 Microsoft. All rights reserved.
+
+ false
+
+
+
+ <_PrepareAssembliesForPreparationDependsOn>
+ _ComputeAssembliesToPostprocessOnPublish;
+ _ComputeLinkerInputs;
+
+
+
+
+
+ <_AssembliesToPrepare Include="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.PostprocessAssembly)' == 'true'" />
+
+
+ $([MSBuild]::EnsureTrailingSlash('$(DeviceSpecificIntermediateOutputPath)prepared-assemblies'))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ObjCRuntime/Registrar.core.cs b/src/ObjCRuntime/Registrar.core.cs
index 958a17174874..86cd2436e8d0 100644
--- a/src/ObjCRuntime/Registrar.core.cs
+++ b/src/ObjCRuntime/Registrar.core.cs
@@ -8,7 +8,11 @@ abstract partial class Registrar {
[return: NotNullIfNotNull (nameof (getterSelector))]
internal static string? CreateSetterSelector (string? getterSelector)
{
+#if NET
if (string.IsNullOrEmpty (getterSelector))
+#else
+ if (string.IsNullOrEmpty (getterSelector) || getterSelector is null)
+#endif
return getterSelector;
var first = (int) getterSelector [0];
@@ -21,7 +25,11 @@ abstract partial class Registrar {
[return: NotNullIfNotNull (nameof (name))]
public static string? SanitizeObjectiveCName (string? name)
{
+#if NET
if (string.IsNullOrEmpty (name))
+#else
+ if (string.IsNullOrEmpty (name) || name is null)
+#endif
return name;
StringBuilder? sb = null;
diff --git a/src/ObjCRuntime/Registrar.cs b/src/ObjCRuntime/Registrar.cs
index d33f63746e84..303004248263 100644
--- a/src/ObjCRuntime/Registrar.cs
+++ b/src/ObjCRuntime/Registrar.cs
@@ -220,7 +220,11 @@ public void VerifyRegisterAttribute ([NotNullIfNotNull (nameof (exceptions))] re
return;
var name = RegisterAttribute.Name;
+#if NET
if (string.IsNullOrEmpty (name))
+#else
+ if (string.IsNullOrEmpty (name) || name is null)
+#endif
return;
for (int i = 0; i < name.Length; i++) {
@@ -540,7 +544,11 @@ abstract internal class ObjCMember {
public bool SetExportAttribute (ExportAttribute ea, [NotNullIfNotNull (nameof (exceptions))] ref List? exceptions)
{
+#if NET
if (string.IsNullOrEmpty (ea.Selector)) {
+#else
+ if (string.IsNullOrEmpty (ea.Selector) || ea.Selector is null) {
+#endif
AddException (ref exceptions, Registrar.CreateException (4135, this, Errors.MT4135, FullName));
return false;
}
@@ -2181,7 +2189,11 @@ void FlattenInterfaces (TType? [] ifaces)
objcType.Add (objcGetter, ref exceptions);
+#if NET
if (!string.IsNullOrEmpty (attrib.SetterSelector)) {
+#else
+ if (!string.IsNullOrEmpty (attrib.SetterSelector) && attrib.SetterSelector is not null) {
+#endif
var objcSetter = new ObjCMethod (this, objcType, null) {
Name = attrib.Name,
Selector = attrib.SetterSelector,
diff --git a/src/bgen/BindingTouch.cs b/src/bgen/BindingTouch.cs
index 7b9e2d32dadb..2744ae3f2547 100644
--- a/src/bgen/BindingTouch.cs
+++ b/src/bgen/BindingTouch.cs
@@ -587,7 +587,12 @@ public void LogError (string message)
Console.Error.WriteLine (message);
}
- public void LogError (Exception exception)
+ public void LogError (BindingException exception)
+ {
+ ErrorHelper.Show (exception);
+ }
+
+ public void LogWarning (BindingException exception)
{
ErrorHelper.Show (exception);
}
diff --git a/tests/Makefile b/tests/Makefile
index 6a0802b2f561..34087555249a 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -52,9 +52,7 @@ test.config: Makefile $(TOP)/Make.config $(TOP)/eng/Version.Details.xml
@echo "JENKINS_RESULTS_DIRECTORY=$(abspath $(JENKINS_RESULTS_DIRECTORY))" >> $@
@echo "XCODE_DEVELOPER_ROOT=$(XCODE_DEVELOPER_ROOT)" >> $@
@echo "DOTNET=$(DOTNET)" >> $@
- @echo "IOS_SDK_VERSION=$(IOS_SDK_VERSION)" >> $@
- @echo "TVOS_SDK_VERSION=$(TVOS_SDK_VERSION)" >> $@
- @echo "MACOS_SDK_VERSION=$(MACOS_SDK_VERSION)" >> $@
+ @printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),$(platform)_SDK_VERSION=$($(platform)_SDK_VERSION)\\n)" | sed 's/^ //' >> $@
@echo "DOTNET_BCL_DIR=$(DOTNET_BCL_DIR)" >> $@
@printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),DOTNET_$(platform)_RUNTIME_IDENTIFIERS_NO_ARCH='$(DOTNET_$(platform)_RUNTIME_IDENTIFIERS_NO_ARCH)'\\n)" | sed 's/^ //' >> $@
@printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),DOTNET_$(platform)_RUNTIME_IDENTIFIERS='$(DOTNET_$(platform)_RUNTIME_IDENTIFIERS)'\\n)" | sed 's/^ //' >> $@
@@ -83,9 +81,7 @@ test-system.config: Makefile $(TOP)/Make.config $(TOP)/eng/Version.Details.xml
@rm -f $@
@echo "JENKINS_RESULTS_DIRECTORY=$(abspath $(JENKINS_RESULTS_DIRECTORY))" >> $@
@echo "DOTNET=$(DOTNET)" >> $@
- @echo "IOS_SDK_VERSION=$(IOS_SDK_VERSION)" >> $@
- @echo "TVOS_SDK_VERSION=$(TVOS_SDK_VERSION)" >> $@
- @echo "MACOS_SDK_VERSION=$(MACOS_SDK_VERSION)" >> $@
+ @printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),$(platform)_SDK_VERSION=$($(platform)_SDK_VERSION)\\n)" | sed 's/^ //' >> $@
@echo "DOTNET_TFM=$(DOTNET_TFM)" >> $@
@echo "DOTNET_BCL_DIR=$(DOTNET_BCL_DIR)" >> $@
@printf "$(foreach platform,$(DOTNET_PLATFORMS_UPPERCASE),DOTNET_$(platform)_RUNTIME_IDENTIFIERS='$(DOTNET_$(platform)_RUNTIME_IDENTIFIERS)'\\n)" | sed 's/^ //' >> $@
diff --git a/tests/assembly-preparer/BaseClass.cs b/tests/assembly-preparer/BaseClass.cs
new file mode 100644
index 000000000000..93e3f02a0011
--- /dev/null
+++ b/tests/assembly-preparer/BaseClass.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace AssemblyPreparerTests;
+
+public abstract class BaseClass {
+ public void AssertPrepare (AssemblyPreparer preparer)
+ {
+ if (!preparer.Prepare (out var exceptions)) {
+ foreach (var ex in exceptions) {
+ Console.WriteLine (ex.ToString ());
+ if (ex.InnerException is not null)
+ Console.WriteLine ($" Inner: {ex.InnerException}");
+ }
+ Assert.Fail ($"Prepare failed, exceptions:\n\t{string.Join ("\n\t", exceptions.Select (v => v.ToString ()))}");
+ }
+ Assert.That (exceptions, Is.Empty, "Exceptions");
+ }
+
+ public bool AssertPrepare (ApplePlatform platform, bool isCoreCLR, string code, out AssemblyDefinition assemblyDefinition)
+ {
+ return AssertPrepare (platform, isCoreCLR, RegistrarMode.Dynamic, code, out assemblyDefinition);
+ }
+
+ // returns true if the test assembly was modified
+ public bool AssertPrepare (ApplePlatform platform, bool isCoreCLR, RegistrarMode registrar, string code, out AssemblyDefinition assemblyDefinition)
+ {
+ AssemblyPreparer? preparer = null;
+ var rv = AssertPrepareCode (platform, isCoreCLR, p => {
+ p.Registrar = registrar;
+ preparer = p;
+ }, code, out string outputPath);
+ var resolver = new DefaultAssemblyResolver ();
+ var dirs = preparer!.Assemblies.Select (v => Path.GetDirectoryName (v.OutputPath)).Distinct ().ToList ();
+ dirs.ForEach (v => resolver.AddSearchDirectory (v));
+ var readerParameters = new ReaderParameters {
+ ReadSymbols = true,
+ AssemblyResolver = resolver,
+ };
+ assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath, readerParameters);
+ return rv;
+ }
+
+ // returns true if the test assembly was modified
+ public bool AssertPrepareCode (ApplePlatform platform, bool isCoreCLR, Action? configure, string code, out string outputPath)
+ {
+ Configuration.IgnoreIfIgnoredPlatform (platform);
+
+ var csproj = $@"
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)-{platform.AsString ().ToLower ()}
+ false
+ true
+
+
+ ";
+
+ var tmpdir = Xamarin.Cache.CreateTemporaryDirectory ();
+
+ var config = $@"
+ AreAnyAssembliesTrimmed=true
+ PublishTrimmed=true
+ IntermediateOutputPath={Path.Combine (tmpdir, "intermediate")}
+ Platform={platform.AsString ()}
+ PlatformAssembly=Microsoft.{platform.AsString ()}.dll
+ SdkDevPath={Configuration.XcodeLocation}
+ SdkVersion={Configuration.GetSdkVersion (platform)}
+ TargetFramework={TargetFramework.GetTargetFramework (platform)}
+ ";
+ var configpath = Path.Combine (tmpdir, "config.txt");
+ File.WriteAllText (configpath, config);
+
+ File.WriteAllText (Path.Combine (tmpdir, "Test.cs"), code);
+ var csprojPath = Path.Combine (tmpdir, "Test.csproj");
+ File.WriteAllText (csprojPath, csproj);
+ var properties = new Dictionary {
+ { "TreatWarningsAsErrors", "false" },
+ };
+ DotNet.AssertBuild (csprojPath, properties);
+ var assemblyDir = Path.Combine (tmpdir, "bin", "Debug");
+
+ var assemblies = Configuration.GetImplementationAssemblies (platform, isCoreCLR);
+ assemblies.Add (Path.Combine (assemblyDir, "Test.dll"));
+ var infos = assemblies.Select (v => new AssemblyPreparerInfo (v, Path.Combine (assemblyDir, "out", Path.GetFileName (v)), true, "link")).ToArray ();
+ var logger = new TestLogger () { Platform = platform };
+ var preparer = new AssemblyPreparer (logger, infos, configpath);
+ if (configure is not null)
+ configure (preparer);
+ AssertPrepare (preparer);
+
+ var testInfo = infos.Single (v => Path.GetFileNameWithoutExtension (v.InputPath) == "Test");
+ outputPath = testInfo.OutputPath;
+ Console.WriteLine ("Output assembly: " + outputPath);
+ preparer.Dispose ();
+ return testInfo.InputPath != testInfo.OutputPath;
+ }
+}
diff --git a/tests/assembly-preparer/GlobalUsings.cs b/tests/assembly-preparer/GlobalUsings.cs
new file mode 100644
index 000000000000..4e06615734f1
--- /dev/null
+++ b/tests/assembly-preparer/GlobalUsings.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+global using System.IO;
+global using System.Runtime.InteropServices;
+
+global using Mono.Cecil;
+global using NUnit.Framework;
+
+global using Xamarin;
+global using Xamarin.Bundler;
+global using Xamarin.Build;
+global using Xamarin.Tests;
+global using Xamarin.Utils;
+
+// These tests are rather memory hungry, so running them in parallel really makes my machine crawl.
+// [assembly: Parallelizable (ParallelScope.Children)]
diff --git a/tests/assembly-preparer/InlineDlfcnMethodsStepTests.cs b/tests/assembly-preparer/InlineDlfcnMethodsStepTests.cs
new file mode 100644
index 000000000000..fa0d1f66f6a4
--- /dev/null
+++ b/tests/assembly-preparer/InlineDlfcnMethodsStepTests.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Mono.Cecil.Cil;
+using Mono.Cecil.Rocks;
+
+namespace AssemblyPreparerTests;
+
+public class InlineDlfcnMethodsStepTests : BaseClass {
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void MarkedTest (ApplePlatform platform, bool isCoreCLR)
+ {
+ var code = @"
+ using System;
+ using CoreAnimation;
+ using Foundation;
+ using ObjCRuntime;
+
+ class MyClass : NSObject {
+ void GetIntPtr ()
+ {
+ Console.WriteLine (Dlfcn.GetIntPtr (0, ""NativeSymbol""));
+ }
+ }";
+
+ AssertPrepare (platform, isCoreCLR, code, out var assemblyDefinition);
+
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass");
+ var platformReference = assemblyDefinition.MainModule.AssemblyReferences.Single (v => v.Name == $"Microsoft.{platform.AsString ()}");
+ var platformAssembly = assemblyDefinition.MainModule.AssemblyResolver.Resolve (platformReference);
+ var dlfcn = platformAssembly.MainModule.Types.Single (v => v.Name == "Dlfcn");
+
+ var cctor = type.GetStaticConstructor ();
+ Assert.That (cctor, Is.Null, "No static constructor should be needed.");
+
+ void AssertHasDlfcnPInvokeCall (MethodDefinition method)
+ {
+ var instructions = method.Body.Instructions;
+ var call = instructions.FirstOrDefault (v => v.OpCode == OpCodes.Call && v.Operand is MethodReference mr && mr.DeclaringType.FullName == dlfcn.FullName);
+ Assert.That (call, Is.Not.Null, $"Expected a call to Dlfcn in {method}");
+ var resolvedMethod = ((MethodReference) call.Operand).Resolve ();
+ Assert.That (resolvedMethod, Is.Not.Null, $"Expected the call to resolve to a method in Dlfcn for {method}");
+ Assert.That (resolvedMethod.PInvokeInfo, Is.Null, $"Expected the method to not be a PInvoke method for {method}");
+ }
+
+ Assert.Multiple (() => {
+ AssertHasDlfcnPInvokeCall (type.Methods.Single (v => v.Name == "GetIntPtr"));
+ });
+ }
+}
diff --git a/tests/assembly-preparer/Makefile b/tests/assembly-preparer/Makefile
new file mode 100644
index 000000000000..b01174cb5734
--- /dev/null
+++ b/tests/assembly-preparer/Makefile
@@ -0,0 +1,8 @@
+TOP=../..
+include $(TOP)/Make.config
+
+build:
+ $(DOTNET) build *.csproj
+
+run-tests:
+ $(DOTNET) test *.csproj
diff --git a/tests/assembly-preparer/MarkIProtocolHandlerTests.cs b/tests/assembly-preparer/MarkIProtocolHandlerTests.cs
new file mode 100644
index 000000000000..fb7f0fcd41dd
--- /dev/null
+++ b/tests/assembly-preparer/MarkIProtocolHandlerTests.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Mono.Cecil.Rocks;
+
+namespace AssemblyPreparerTests;
+
+public class MarkIProtocolHandlerTests : BaseClass {
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void DynamicRegistrar (ApplePlatform platform, bool isCoreCLR)
+ {
+ var code = @"
+ using System;
+ using Foundation;
+ using ObjCRuntime;
+
+ [Protocol]
+ interface IProtocol {
+ }
+
+ class MyClass : NSObject, IProtocol {
+ }
+ ";
+
+ AssertPrepare (platform, isCoreCLR, RegistrarMode.Dynamic, code, out var assemblyDefinition);
+
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass");
+ var cctor = type.GetStaticConstructor ();
+ Assert.That (cctor, Is.Not.Null, "Expected a static constructor to be added");
+ var attribs = cctor.CustomAttributes?.Where (v => v.AttributeType.Name == "DynamicDependencyAttribute").OrderBy (v => string.Join (", ", v.ConstructorArguments.Select (v => v.Value?.ToString ()))).ToArray ();
+ Assert.That (attribs, Is.Not.Null, "Attributes");
+ Assert.That (attribs.Count, Is.EqualTo (1), "Attribute count");
+ // PreserveProtocolsStep adds DDA(DynamicallyAccessedMemberTypes.Interfaces, typeof(MyClass))
+ Assert.That ((int) attribs [0].ConstructorArguments [0].Value, Is.EqualTo (0x2000), "First attribute's first argument (DynamicallyAccessedMemberTypes.Interfaces)");
+ Assert.That (((TypeReference) attribs [0].ConstructorArguments [1].Value).FullName, Is.EqualTo ("MyClass"), "First attribute's second argument");
+ }
+
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void ManagedStaticRegistrar (ApplePlatform platform, bool isCoreCLR)
+ {
+ var code = @"
+ using System;
+ using Foundation;
+ using ObjCRuntime;
+
+ [Protocol]
+ interface IProtocol {
+ }
+
+ class MyClass : NSObject, IProtocol {
+ }
+ ";
+
+ // With ManagedStatic registrar, PreserveProtocolsStep doesn't run,
+ // so MyClass's cctor should not have any DDA for protocol preservation.
+ AssertPrepare (platform, isCoreCLR, RegistrarMode.ManagedStatic, code, out var assemblyDefinition);
+
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass");
+ var cctor = type.GetStaticConstructor ();
+ if (cctor is not null) {
+ var ddaAttribs = cctor.CustomAttributes?.Where (v => v.AttributeType.Name == "DynamicDependencyAttribute").ToArray ();
+ Assert.That (ddaAttribs, Is.Empty, "No DynamicDependencyAttributes expected on MyClass's cctor");
+ }
+ }
+}
diff --git a/tests/assembly-preparer/OptimizeGeneratedCodeHandlerTests.cs b/tests/assembly-preparer/OptimizeGeneratedCodeHandlerTests.cs
new file mode 100644
index 000000000000..5a5ee83d47e4
--- /dev/null
+++ b/tests/assembly-preparer/OptimizeGeneratedCodeHandlerTests.cs
@@ -0,0 +1,251 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Mono.Cecil.Cil;
+using Mono.Cecil.Rocks;
+
+namespace AssemblyPreparerTests;
+
+public class OptimizeGeneratedCodeHandlerTests : BaseClass {
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void RemoveEnsureUIThread (ApplePlatform platform, bool isCoreCLR)
+ {
+ var ensureUIThreadCall = platform == ApplePlatform.MacOSX
+ ? "AppKit.NSApplication.EnsureUIThread ();"
+ : "UIKit.UIApplication.EnsureUIThread ();";
+
+ var usingDirective = platform == ApplePlatform.MacOSX
+ ? "using AppKit;"
+ : "using UIKit;";
+
+ var code = $@"
+ using System;
+ using Foundation;
+ using ObjCRuntime;
+ {usingDirective}
+
+ class MyClass : NSObject {{
+ [BindingImpl (BindingImplOptions.Optimizable)]
+ [Export (""myMethod"")]
+ public void MyMethod () {{
+ {ensureUIThreadCall}
+ }}
+ }}";
+
+ AssertPrepareCode (platform, isCoreCLR, preparer => {
+ preparer.Registrar = RegistrarMode.Dynamic;
+ preparer.Optimizations.RemoveUIThreadChecks = true;
+ }, code, out var outputPath);
+
+ using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath);
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass");
+ var method = type.Methods.Single (v => v.Name == "MyMethod");
+
+ var hasEnsureUIThread = method.Body.Instructions.Any (i =>
+ i.OpCode.Code == Code.Call &&
+ (i.Operand as MethodReference)?.Name == "EnsureUIThread");
+ Assert.That (hasEnsureUIThread, Is.False, "EnsureUIThread call should be removed");
+ }
+
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void KeepEnsureUIThreadWhenOptimizationDisabled (ApplePlatform platform, bool isCoreCLR)
+ {
+ var ensureUIThreadCall = platform == ApplePlatform.MacOSX
+ ? "AppKit.NSApplication.EnsureUIThread ();"
+ : "UIKit.UIApplication.EnsureUIThread ();";
+
+ var usingDirective = platform == ApplePlatform.MacOSX
+ ? "using AppKit;"
+ : "using UIKit;";
+
+ var code = $@"
+ using System;
+ using Foundation;
+ using ObjCRuntime;
+ {usingDirective}
+
+ class MyClass : NSObject {{
+ [BindingImpl (BindingImplOptions.Optimizable)]
+ [Export (""myMethod"")]
+ public void MyMethod () {{
+ {ensureUIThreadCall}
+ }}
+ }}";
+
+ AssertPrepareCode (platform, isCoreCLR, preparer => {
+ preparer.Registrar = RegistrarMode.Dynamic;
+ preparer.Optimizations.RemoveUIThreadChecks = false;
+ }, code, out var outputPath);
+
+ using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath);
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass");
+ var method = type.Methods.Single (v => v.Name == "MyMethod");
+
+ var hasEnsureUIThread = method.Body.Instructions.Any (i =>
+ i.OpCode.Code == Code.Call &&
+ (i.Operand as MethodReference)?.Name == "EnsureUIThread");
+ Assert.That (hasEnsureUIThread, Is.True, "EnsureUIThread call should be preserved when optimization is disabled");
+ }
+
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void OptimizeProtocolInterfaceStaticConstructor (ApplePlatform platform, bool isCoreCLR)
+ {
+ var code = @"
+ using System;
+ using Foundation;
+ using ObjCRuntime;
+
+ [Protocol]
+ interface IMyProtocol {
+ [BindingImpl (BindingImplOptions.Optimizable)]
+ static IMyProtocol () {
+ GC.KeepAlive (null);
+ }
+ }
+
+ class MyClass : NSObject, IMyProtocol {
+ }";
+
+ AssertPrepareCode (platform, isCoreCLR, preparer => {
+ preparer.Registrar = RegistrarMode.Dynamic;
+ preparer.Optimizations.RegisterProtocols = true;
+ }, code, out var outputPath);
+
+ using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath);
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "IMyProtocol");
+ var cctor = type.GetStaticConstructor ();
+
+ Assert.That (cctor, Is.Not.Null, "Static constructor should still exist");
+ Assert.That (cctor.Body.Instructions.Count, Is.EqualTo (1), "Static constructor should have only a ret instruction");
+ Assert.That (cctor.Body.Instructions [0].OpCode.Code, Is.EqualTo (Code.Ret), "Static constructor should only contain ret");
+ }
+
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void KeepProtocolStaticConstructorWhenOptimizationDisabled (ApplePlatform platform, bool isCoreCLR)
+ {
+ var code = @"
+ using System;
+ using Foundation;
+ using ObjCRuntime;
+
+ [Protocol]
+ interface IMyProtocol {
+ [BindingImpl (BindingImplOptions.Optimizable)]
+ static IMyProtocol () {
+ GC.KeepAlive (null);
+ }
+ }
+
+ class MyClass : NSObject, IMyProtocol {
+ }";
+
+ AssertPrepareCode (platform, isCoreCLR, preparer => {
+ preparer.Registrar = RegistrarMode.Dynamic;
+ preparer.Optimizations.RegisterProtocols = false;
+ }, code, out var outputPath);
+
+ using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath);
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "IMyProtocol");
+ var cctor = type.GetStaticConstructor ();
+
+ Assert.That (cctor, Is.Not.Null, "Static constructor should still exist");
+ Assert.That (cctor.Body.Instructions.Count, Is.GreaterThan (1), "Static constructor should not be optimized");
+ }
+
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void NoOptimizationWithoutBindingAttributes (ApplePlatform platform, bool isCoreCLR)
+ {
+ var ensureUIThreadCall = platform == ApplePlatform.MacOSX
+ ? "AppKit.NSApplication.EnsureUIThread ();"
+ : "UIKit.UIApplication.EnsureUIThread ();";
+
+ var usingDirective = platform == ApplePlatform.MacOSX
+ ? "using AppKit;"
+ : "using UIKit;";
+
+ var code = $@"
+ using System;
+ using Foundation;
+ using ObjCRuntime;
+ {usingDirective}
+
+ class MyClass : NSObject {{
+ [Export (""myMethod"")]
+ public void MyMethod () {{
+ {ensureUIThreadCall}
+ }}
+ }}";
+
+ AssertPrepareCode (platform, isCoreCLR, preparer => {
+ preparer.Registrar = RegistrarMode.Dynamic;
+ preparer.Optimizations.RemoveUIThreadChecks = true;
+ }, code, out var outputPath);
+
+ using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath);
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass");
+ var method = type.Methods.Single (v => v.Name == "MyMethod");
+
+ var hasEnsureUIThread = method.Body.Instructions.Any (i =>
+ i.OpCode.Code == Code.Call &&
+ (i.Operand as MethodReference)?.Name == "EnsureUIThread");
+ Assert.That (hasEnsureUIThread, Is.True, "EnsureUIThread call should be preserved without [BindingImpl(Optimizable)]");
+ }
+
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void DeadCodeElimination (ApplePlatform platform, bool isCoreCLR)
+ {
+ var code = @"
+ using System;
+ using Foundation;
+ using ObjCRuntime;
+
+ class MyClass : NSObject {
+ [BindingImpl (BindingImplOptions.Optimizable)]
+ [Export (""myMethod"")]
+ public int MyMethod () {
+ if (true) {
+ return 1;
+ }
+ return 2;
+ }
+ }";
+
+ AssertPrepareCode (platform, isCoreCLR, preparer => {
+ preparer.Registrar = RegistrarMode.Dynamic;
+ preparer.Optimizations.DeadCodeElimination = true;
+ }, code, out var outputPath);
+
+ using var assemblyDefinition = AssemblyDefinition.ReadAssembly (outputPath);
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass");
+ var method = type.Methods.Single (v => v.Name == "MyMethod");
+
+ // After dead code elimination, there should be no ldc.i4.2 (the unreachable return 2)
+ var hasDeadCode = method.Body.Instructions.Any (i =>
+ i.OpCode.Code == Code.Ldc_I4_2);
+ Assert.That (hasDeadCode, Is.False, "Dead code (return 2) should be eliminated");
+ }
+}
diff --git a/tests/assembly-preparer/PreserveBlockCodeHandlerTests.cs b/tests/assembly-preparer/PreserveBlockCodeHandlerTests.cs
new file mode 100644
index 000000000000..ad39cd811c5c
--- /dev/null
+++ b/tests/assembly-preparer/PreserveBlockCodeHandlerTests.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Mono.Cecil.Rocks;
+
+namespace AssemblyPreparerTests;
+
+public class PreserveBlockCodeHandlerTests : BaseClass {
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void MarkedTest (ApplePlatform platform, bool isCoreCLR)
+ {
+ var code = @"
+ using System;
+ using ObjCRuntime;
+ namespace ObjCRuntime;
+ class Trampolines {
+ static internal class SDInnerBlock {
+ // this field is not preserved by other means, but it must not be linked away
+ static internal readonly DInnerBlock Handler = Invoke;
+
+ [MonoPInvokeCallback (typeof (DInnerBlock))]
+ static internal void Invoke (IntPtr block, int magic_number)
+ {
+ }
+
+ public delegate void DInnerBlock (IntPtr block, int magic_number);
+ }
+ }";
+
+ AssertPrepare (platform, isCoreCLR, code, out var assemblyDefinition);
+
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "Trampolines").NestedTypes.Single (v => v.Name == "SDInnerBlock");
+ var cctor = type.GetStaticConstructor ();
+ var attribs = cctor.CustomAttributes?.OrderBy (v => string.Join (", ", v.ConstructorArguments.Select (v => v.Value?.ToString ()))).ToArray ();
+ Assert.That (attribs, Is.Not.Null, "Attributes");
+ Assert.That (attribs.Count, Is.EqualTo (2), "Attribute count");
+ Assert.That (attribs.All (v => v.AttributeType.Name == "DynamicDependencyAttribute"), Is.True, "Attribute name");
+ // Handler field: DDA(string memberSignature, string typeName, string assemblyName)
+ Assert.That (attribs [0].ConstructorArguments.Count, Is.EqualTo (3), "First attribute's argument count");
+ Assert.That ((string) attribs [0].ConstructorArguments [0].Value, Is.EqualTo ("Handler"), "First attribute's first argument");
+ Assert.That ((string) attribs [0].ConstructorArguments [1].Value, Is.EqualTo ("ObjCRuntime.Trampolines.SDInnerBlock"), "First attribute's second argument");
+ Assert.That ((string) attribs [0].ConstructorArguments [2].Value, Is.EqualTo ("Test"), "First attribute's third argument");
+
+ // Invoke method: DDA(string memberSignature) - same declaring type, so simpler constructor
+ Assert.That (attribs [1].ConstructorArguments.Count, Is.EqualTo (1), "Second attribute's argument count");
+ Assert.That ((string) attribs [1].ConstructorArguments [0].Value, Is.EqualTo ("Invoke(System.IntPtr,System.Int32)"), "Second attribute's first argument");
+ }
+}
diff --git a/tests/assembly-preparer/PreserveSmartEnumConversionsTest.cs b/tests/assembly-preparer/PreserveSmartEnumConversionsTest.cs
new file mode 100644
index 000000000000..7c690caa9037
--- /dev/null
+++ b/tests/assembly-preparer/PreserveSmartEnumConversionsTest.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Mono.Cecil.Rocks;
+
+namespace AssemblyPreparerTests;
+
+public class PreserveSmartEnumConversionsTests : BaseClass {
+ [Test]
+ [TestCase (ApplePlatform.MacCatalyst, false)]
+ [TestCase (ApplePlatform.iOS, false)]
+ [TestCase (ApplePlatform.TVOS, false)]
+ [TestCase (ApplePlatform.MacOSX, true)]
+ public void MarkedTest (ApplePlatform platform, bool isCoreCLR)
+ {
+ var code = @"
+ using System;
+ using CoreAnimation;
+ using Foundation;
+ using ObjCRuntime;
+
+ class MyClass : NSObject {
+ [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))]
+ public CAToneMapMode RWProperty { get; set; }
+
+ [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))]
+ public CAToneMapMode ROProperty { get { return default; } }
+
+ [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))]
+ public CAToneMapMode WOProperty { set { } }
+
+ [return: BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))]
+ public CAToneMapMode Method1 () { return default; }
+
+ public void Method2 ([BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p1) {}
+
+ public void Method3 ([BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p1, [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p2) {}
+
+ [return: BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))]
+ public CAToneMapMode Method4 ([BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p1, [BindAs (typeof (CAToneMapMode), OriginalType = typeof (NSString))] CAToneMapMode p2) { return default;}
+ }";
+
+ AssertPrepare (platform, isCoreCLR, code, out var assemblyDefinition);
+
+ var type = assemblyDefinition.MainModule.Types.Single (v => v.Name == "MyClass");
+ var cctor = type.GetStaticConstructor ();
+ Assert.That (cctor, Is.Null, "No static constructor should be needed.");
+
+ void AssertHasDynamicDependency (ICustomAttributeProvider provider, string memberSignature, string typeName, string assemblyName)
+ {
+ var ddaAttributes = provider.CustomAttributes.Where (v => v.AttributeType.FullName == "System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute").ToArray ();
+ var found = 0;
+ foreach (var ca in ddaAttributes) {
+ if (ca.ConstructorArguments.Count != 3)
+ continue;
+ var attribMemberSignature = (string) ca.ConstructorArguments [0].Value!;
+ if (attribMemberSignature != memberSignature)
+ continue;
+ var attribTypeName = (string) ca.ConstructorArguments [1].Value!;
+ if (attribTypeName != typeName)
+ continue;
+ var attribAssemblyName = (string) ca.ConstructorArguments [2].Value!;
+ if (attribAssemblyName != assemblyName)
+ continue;
+
+ found++;
+ }
+
+ if (found == 1)
+ return;
+
+ var attributesAsString = ddaAttributes
+ .Select (v => {
+ switch (v.ConstructorArguments.Count) {
+ case 3:
+ return $"[DynamicDependency (\"{v.ConstructorArguments [0].Value}\", \"{v.ConstructorArguments [1].Value}\", \"{v.ConstructorArguments [2].Value}\")]";
+ case 2:
+ return $"[DynamicDependency (\"{v.ConstructorArguments [0].Value}\", \"{v.ConstructorArguments [1].Value}\")]";
+ case 1:
+ return $"[DynamicDependency (\"{v.ConstructorArguments [0].Value}\")]";
+ default:
+ return string.Join (", ", v.ConstructorArguments.Select (x => x.Value?.ToString () ?? "null"));
+ }
+ })
+ .OrderBy (v => v)
+ .ToArray ();
+
+ string msg;
+ if (found == 0) {
+ if (attributesAsString.Length == 0) {
+ msg = $"Expected [DynamicDependency (\"{memberSignature}\", \"{typeName}\", \"{assemblyName}\")] on {provider}, got no attributes.";
+ } else {
+ msg = $"Expected [DynamicDependency (\"{memberSignature}\", \"{typeName}\", \"{assemblyName}\")] on {provider}, got:\n\t{string.Join ("\n\t", attributesAsString)}";
+ }
+ } else {
+ msg = $"Expected exactly one [DynamicDependency (\"{memberSignature}\", \"{typeName}\", \"{assemblyName}\")] on {provider}, got {found}:\n\t{string.Join ("\n\t", attributesAsString)}";
+ }
+ Console.WriteLine (msg);
+ Assert.Fail (msg);
+ }
+
+ void AssertHasDynamicDependencies (ICustomAttributeProvider provider)
+ {
+ AssertHasDynamicDependency (provider, "GetConstant(CoreAnimation.CAToneMapMode)", "CoreAnimation.CAToneMapModeExtensions", $"Microsoft.{platform.AsString ()}");
+ AssertHasDynamicDependency (provider, "GetValue(Foundation.NSString)", "CoreAnimation.CAToneMapModeExtensions", $"Microsoft.{platform.AsString ()}");
+ }
+
+ Assert.Multiple (() => {
+ AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "get_RWProperty"));
+ AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "set_RWProperty"));
+ AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "get_ROProperty"));
+ AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "set_WOProperty"));
+ AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "Method1"));
+ AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "Method2"));
+ AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "Method3"));
+ AssertHasDynamicDependencies (type.Methods.Single (v => v.Name == "Method4"));
+ });
+ }
+}
diff --git a/tests/assembly-preparer/ReproTest.cs b/tests/assembly-preparer/ReproTest.cs
new file mode 100644
index 000000000000..267f7f417568
--- /dev/null
+++ b/tests/assembly-preparer/ReproTest.cs
@@ -0,0 +1,203 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Logging.StructuredLogger;
+
+namespace AssemblyPreparerTests;
+
+public class ReproTest : BaseClass {
+ [TestCase (ApplePlatform.iOS, false)]
+ public void RoundTrip (ApplePlatform platform, bool isCoreCLR)
+ {
+ Configuration.IgnoreIfIgnoredPlatform (platform);
+
+ // build once with a repro path
+ // load everything from the repro path, prepare again (with a different repro path this time),
+ // and verify that the arguments.txt files from each preparation are identical
+ //
+ // this test can also be repurposed to run an existing repro by setting the _PrepareAssembliesMakeReproPath variable
+
+ var reproPath = Environment.GetEnvironmentVariable ("_PrepareAssembliesMakeReproPath");
+ var referenceAssemblies = Configuration.GetReferenceAssemblies (platform, false);
+ if (string.IsNullOrEmpty (reproPath)) {
+ var code = @"public class SomeLibrary {}";
+
+ reproPath = Xamarin.Cache.CreateTemporaryDirectory ();
+ Directory.Delete (reproPath); // the repro path can't exist prior to Prepare
+ AssertPrepareCode (platform, isCoreCLR, (preparer) => {
+ preparer.MakeReproPath = reproPath;
+ preparer.Registrar = RegistrarMode.Dynamic;
+ }, code, out string _);
+ }
+
+ var lines = File.ReadAllLines (Path.Combine (reproPath, "arguments.txt"));
+
+ var ap = AssemblyPreparer.LoadFromReproPath (reproPath);
+ ap.MakeReproPath = Xamarin.Cache.CreateTemporaryDirectory ();
+ Directory.Delete (ap.MakeReproPath); // the repro path can't exist prior to Prepare
+ AssertPrepare (ap);
+
+ var lines2 = File.ReadAllLines (Path.Combine (ap.MakeReproPath, "arguments.txt"));
+
+ // Normalize repro paths before comparison, since they will always differ
+ var normalizedLines = lines.Select (l => l.Replace (reproPath, "")).ToArray ();
+ var normalizedLines2 = lines2.Select (l => l.Replace (ap.MakeReproPath, "")).ToArray ();
+ Assert.That (normalizedLines, Is.EqualTo (normalizedLines2), "Repro arguments match");
+ }
+
+ // This test is here only to easily debug the PrepareAssemblies task from a build that produced a binlog.
+ // Just copy the binlog to /tmp/assembly-preparer.binlog and run this test. It will find the PrepareAssemblies
+ // task in the binlog, extract the relevant parameters, and run the preparation logic with those parameters.
+ [Test]
+ public void FromBinlog ()
+ {
+ var binlogPath = "/tmp/assembly-preparer.binlog";
+ if (!File.Exists (binlogPath))
+ Assert.Ignore (); // The binlog doesn't exist, so nothing to do
+
+ var reader = new BinLogReader ();
+ var records = reader.ReadRecords (binlogPath).ToArray ();
+ var originalBinlogPath = records
+ .Select (r => r.Args)
+ .OfType ()
+ .Where (r => r.SenderName == "BinaryLogger" && r.Message?.StartsWith ("BinLogFilePath=") == true)
+ .Select (v => v.Message?.Substring ("BinLogFilePath=".Length) ?? "")
+ .SingleOrDefault ();
+ var originalBinlogDirectory = Path.GetDirectoryName (originalBinlogPath)!;
+ foreach (var record in records) {
+ if (record is null)
+ continue;
+
+ if (record.Args is null)
+ continue;
+
+ if (record.Args is TaskStartedEventArgs tsea && tsea.TaskName == "PrepareAssemblies") {
+ var taskId = tsea.BuildEventContext?.TaskId;
+ if (taskId is null)
+ continue;
+ var relevantRecords = records.
+ Select (v => v.Args).
+ Where (v => v is not null).
+ Where (v => v.BuildEventContext?.TaskId == taskId).
+ ToArray ();
+
+ var taskParameters = relevantRecords.Where (v => v is TaskParameterEventArgs).Cast ().ToArray ();
+
+ string? getProperty (string name)
+ {
+ var param = taskParameters.SingleOrDefault (v => v.ItemType == name);
+ if (param is null)
+ return null;
+ if (param.Items is null)
+ return null;
+ if (param.Items.Count != 1)
+ return null;
+ var item = param.Items [0];
+ if (item is null)
+ return null;
+ return ((ITaskItem) item).ItemSpec;
+ }
+ ITaskItem []? getItems (string name)
+ {
+ var param = taskParameters.SingleOrDefault (v => v.ItemType == name);
+ if (param is null)
+ return null;
+ if (param.Items is null)
+ return null;
+ return param.Items.Cast ().ToArray ();
+ }
+ var outputDirectory = getProperty ("OutputDirectory");
+ var optionsFile = getProperty ("OptionsFile");
+ var targetFrameworkMoniker = getProperty ("TargetFrameworkMoniker");
+ var makeReproPath = getProperty ("MakeReproPath");
+ var inputAssemblies = getItems ("InputAssemblies");
+
+ if (string.IsNullOrEmpty (outputDirectory))
+ throw new InvalidOperationException ("OutputDirectory is required");
+ outputDirectory = Path.GetFullPath (outputDirectory, originalBinlogDirectory);
+
+ if (string.IsNullOrEmpty (optionsFile))
+ throw new InvalidOperationException ("OptionsFile is required");
+ optionsFile = Path.GetFullPath (optionsFile, originalBinlogDirectory);
+
+ if (string.IsNullOrEmpty (targetFrameworkMoniker))
+ throw new InvalidOperationException ("TargetFrameworkMoniker is required");
+
+ if (inputAssemblies is null || inputAssemblies.Length == 0)
+ throw new InvalidOperationException ("InputAssemblies is required");
+
+ AssemblyPreparerInfo GetAssemblyInfo (ITaskItem item)
+ {
+ var inputPath = Path.GetFullPath (item.ItemSpec, originalBinlogDirectory);
+ var outputPath = Path.Combine (outputDirectory, Path.GetFileName (inputPath));
+ var metadataNames = item.MetadataNames.Cast ().Select (v => v.ToLowerInvariant ()).ToHashSet ();
+ var isTrimmableString = item.GetMetadata ("IsTrimmable");
+ var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase);
+ var trimMode = item.GetMetadata ("TrimMode");
+
+ var rv = new AssemblyPreparerInfo (inputPath, outputPath, isTrimmable, trimMode);
+ return rv;
+ }
+
+ var platformString = File.ReadAllLines (optionsFile).Single (v => v.StartsWith ("Platform=")).Substring ("Platform=".Length);
+ var platform = ApplePlatformExtensions.Parse (platformString);
+
+ var infos = inputAssemblies.Select (GetAssemblyInfo).ToArray ();
+ var logger = new TestLogger () { Platform = platform };
+ using var preparer = new AssemblyPreparer (logger, infos, optionsFile);
+ preparer.MakeReproPath = makeReproPath ?? "";
+ var rv = preparer.Prepare (out var exceptions);
+ return;
+ }
+ }
+
+ Assert.Fail ("The task 'PrepareAssemblies' was not found in the provided binlog.");
+ }
+}
+
+
+class TestLogger : IToolLog {
+ public int Verbosity => 0;
+ public required ApplePlatform Platform { get; set; }
+
+ public void Log (string value)
+ {
+ Console.WriteLine (value);
+ }
+
+ public void Log (string format, params object? [] args)
+ {
+ Console.WriteLine (format, args);
+ }
+
+ public void LogException (Exception ex)
+ {
+ Console.WriteLine (ex.ToString ());
+ }
+
+ public void LogError (ProductException ex)
+ {
+ Console.WriteLine (ex.ToString ());
+ }
+
+ public void LogError (Exception ex)
+ {
+ Console.WriteLine (ex.ToString ());
+ }
+
+ public void LogError (string value)
+ {
+ Console.WriteLine (value);
+ }
+
+ public void LogWarning (ProductException ex)
+ {
+ Console.WriteLine (ex.ToString ());
+ }
+
+ public void LogWarning (Exception ex)
+ {
+ Console.WriteLine (ex.ToString ());
+ }
+}
diff --git a/tests/assembly-preparer/assembly-preparer-tests.csproj b/tests/assembly-preparer/assembly-preparer-tests.csproj
new file mode 100644
index 000000000000..aab0b2d3d64e
--- /dev/null
+++ b/tests/assembly-preparer/assembly-preparer-tests.csproj
@@ -0,0 +1,50 @@
+
+
+ net$(BundledNETCoreAppTargetFrameworkVersion)
+ latest
+ enable
+ enable
+ false
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ external/tests/common/BinLog.cs
+
+
+ external/tests/mtouch/Cache.cs
+
+
+ external/tests/common/Configuration.cs
+
+
+ external/tests/common/ConfigurationNUnit.cs
+
+
+ external/tests/common/DotNet.cs
+
+
+ external/tests/common/ExecutionHelper.cs
+
+
+ external/tools/common/SdkVersions.cs
+
+
+ external/tools/common/StringUtils.cs
+
+
+
+
diff --git a/tests/assembly-preparer/assembly-preparer.slnx b/tests/assembly-preparer/assembly-preparer.slnx
new file mode 100644
index 000000000000..990bc0a7a9fb
--- /dev/null
+++ b/tests/assembly-preparer/assembly-preparer.slnx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/common/Configuration.cs b/tests/common/Configuration.cs
index 8154d4e9b100..20f92ed994ff 100644
--- a/tests/common/Configuration.cs
+++ b/tests/common/Configuration.cs
@@ -10,25 +10,17 @@
namespace Xamarin.Tests {
static partial class Configuration {
- public const string XI_ProductName = "MonoTouch";
- public const string XM_ProductName = "Xamarin.Mac";
-
public static string DotNetBclDir = "";
public static string DotNetCscCommand = "";
public static string DotNetExecutable;
public static string DotNetTfm;
- public static string? mt_src_root;
- public static string sdk_version;
+ public static string ios_sdk_version;
public static string tvos_sdk_version;
public static string macos_sdk_version;
- public static string xcode_root;
+ public static string maccatalyst_sdk_version;
+ static string xcode_root;
public static string? XcodeVersionString;
- public static string xcode83_root;
- public static string xcode94_root;
-#if MONOMAC
- public static string mac_xcode_root;
-#endif
- public static Dictionary make_config = new Dictionary ();
+ static Dictionary make_config = new Dictionary ();
public static bool include_ios;
public static bool include_mac;
@@ -286,12 +278,11 @@ static Configuration ()
{
ParseConfigFiles ();
- sdk_version = GetVariable ("IOS_SDK_VERSION", "8.0");
+ ios_sdk_version = GetVariable ("IOS_SDK_VERSION", "8.0");
tvos_sdk_version = GetVariable ("TVOS_SDK_VERSION", "9.0");
macos_sdk_version = GetVariable ("MACOS_SDK_VERSION", "10.12");
+ maccatalyst_sdk_version = GetVariable ("MACCATALYST_SDK_VERSION", "14.0");
xcode_root = GetVariable ("XCODE_DEVELOPER_ROOT", "/Applications/Xcode.app/Contents/Developer");
- xcode83_root = GetVariable ("XCODE83_DEVELOPER_ROOT", "/Applications/Xcode83.app/Contents/Developer");
- xcode94_root = GetVariable ("XCODE94_DEVELOPER_ROOT", "/Applications/Xcode94.app/Contents/Developer");
include_ios = !string.IsNullOrEmpty (GetVariable ("INCLUDE_IOS", ""));
include_mac = !string.IsNullOrEmpty (GetVariable ("INCLUDE_MAC", ""));
include_tvos = !string.IsNullOrEmpty (GetVariable ("INCLUDE_TVOS", ""));
@@ -305,16 +296,10 @@ static Configuration ()
DOTNET_DIR = GetVariable ("DOTNET_DIR", "");
XcodeVersionString = GetVariable ("XCODE_VERSION", GetXcodeVersion (xcode_root));
-#if MONOMAC
- mac_xcode_root = xcode_root;
-#endif
Console.WriteLine ("Test configuration:");
- Console.WriteLine (" SDK_VERSION={0}", sdk_version);
Console.WriteLine (" XCODE_ROOT={0}", xcode_root);
-#if MONOMAC
- Console.WriteLine (" MAC_XCODE_ROOT={0}", mac_xcode_root);
-#endif
+ Console.WriteLine (" XCODE_VERSION={0}", XcodeVersionString);
Console.WriteLine (" INCLUDE_IOS={0}", include_ios);
Console.WriteLine (" INCLUDE_MAC={0}", include_mac);
Console.WriteLine (" INCLUDE_TVOS={0}", include_tvos);
@@ -371,13 +356,7 @@ public static bool TryGetRootPath ([NotNullWhen (true)] out string? rootPath)
}
}
- public static string SourceRoot {
- get {
- if (mt_src_root is null)
- mt_src_root = RootPath;
- return mt_src_root;
- }
- }
+ public static string SourceRoot => RootPath;
public static string TestProjectsDirectory {
get {
@@ -598,6 +577,63 @@ public static string GetRefLibrary (ApplePlatform platform)
{
return GetBaseLibrary (platform);
}
+
+ public static List GetBCLAssemblies (ApplePlatform platform, bool isCoreCLR)
+ {
+ var assemblies = new List ();
+ string rid;
+ string packageName;
+ switch (platform) {
+ case ApplePlatform.MacCatalyst:
+ rid = "maccatalyst-arm64";
+ packageName = isCoreCLR ? $"microsoft.netcore.app.runtime.maccatalyst-arm64" : $"microsoft.netcore.app.runtime.mono.maccatalyst-arm64";
+ break;
+ case ApplePlatform.iOS:
+ rid = "ios-arm64";
+ packageName = isCoreCLR ? $"microsoft.netcore.app.runtime.ios-arm64" : $"microsoft.netcore.app.runtime.mono.ios-arm64";
+ break;
+ case ApplePlatform.TVOS:
+ rid = "tvos-arm64";
+ packageName = isCoreCLR ? $"microsoft.netcore.app.runtime.tvOS-arm64" : $"microsoft.netcore.app.runtime.mono.tvOS-arm64";
+ break;
+ case ApplePlatform.MacOSX:
+ rid = "osx-arm64";
+ packageName = "microsoft.netcore.app.runtime.osx-arm64";
+ if (!isCoreCLR)
+ throw new InvalidOperationException ($"Mono doesn't support macOS, but was asked for the BCL assemblies for macOS");
+ break;
+ default:
+ throw new NotSupportedException ($"Unsupported platform: {platform}");
+ }
+ var microsoftNetCoreAppRefPackageVersion = File.ReadAllLines (Path.Combine (RootPath, "dotnet.config")).Single (v => v.StartsWith ("BUNDLED_NETCORE_PLATFORMS_PACKAGE_VERSION=", StringComparison.Ordinal)).Replace ("BUNDLED_NETCORE_PLATFORMS_PACKAGE_VERSION=", "");
+ var bclDir = Path.Combine (RootPath, "packages", packageName, microsoftNetCoreAppRefPackageVersion, "runtimes", rid, "lib", DotNetTfm);
+ var nativeDir = Path.Combine (RootPath, "packages", packageName, microsoftNetCoreAppRefPackageVersion, "runtimes", rid, "native");
+
+ assemblies.AddRange (Directory.GetFiles (bclDir, "*.dll"));
+ assemblies.AddRange (Directory.GetFiles (nativeDir, "*.dll"));
+
+ return assemblies;
+ }
+
+ public static List GetReferenceAssemblies (ApplePlatform platform, bool isCoreCLR)
+ {
+ var assemblies = new List ();
+
+ assemblies.AddRange (GetBCLAssemblies (platform, isCoreCLR));
+ assemblies.AddRange (GetRefLibrary (platform));
+
+ return assemblies;
+ }
+
+ public static List GetImplementationAssemblies (ApplePlatform platform, bool isCoreCLR)
+ {
+ var assemblies = new List ();
+
+ assemblies.AddRange (GetBCLAssemblies (platform, isCoreCLR));
+ assemblies.AddRange (GetBaseLibraryImplementations (platform).First ());
+
+ return assemblies;
+ }
#endif // !XAMMAC_TESTS
public static IEnumerable GetIncludedPlatforms ()
@@ -694,7 +730,7 @@ public static void SetBuildVariables (ApplePlatform platform, [NotNullIfNotNull
if (environment is null)
environment = new Dictionary ();
- environment ["DEVELOPER_DIR"] = Path.GetDirectoryName (Path.GetDirectoryName (xcode_root)!)!;
+ environment ["DEVELOPER_DIR"] = Path.GetDirectoryName (Path.GetDirectoryName (XcodeLocation)!)!;
// This is set by `dotnet test` and can cause building legacy projects to fail to build with:
// Microsoft.NET.Build.Extensions.ConflictResolution.targets(30,5):
@@ -732,6 +768,22 @@ public static string GetTestLibraryDirectory (ApplePlatform platform, bool? simu
return Path.Combine (SourceRoot, "tests", "test-libraries", ".libs", dir);
}
+ public static string GetSdkVersion (ApplePlatform platform)
+ {
+ switch (platform) {
+ case ApplePlatform.iOS:
+ return ios_sdk_version;
+ case ApplePlatform.MacOSX:
+ return macos_sdk_version;
+ case ApplePlatform.TVOS:
+ return tvos_sdk_version;
+ case ApplePlatform.MacCatalyst:
+ return maccatalyst_sdk_version;
+ default:
+ throw new NotImplementedException ($"Unknown platform: {platform}");
+ }
+ }
+
// This implementation of Touch is to update a timestamp (not to make sure a certain file exists).
public static void Touch (string file)
{
diff --git a/tests/common/DotNet.cs b/tests/common/DotNet.cs
index 1f7a5c73e868..9e01afeebe45 100644
--- a/tests/common/DotNet.cs
+++ b/tests/common/DotNet.cs
@@ -109,7 +109,7 @@ public static ExecutionResult AssertNew (string outputDirectory, string template
Console.WriteLine (output);
Assert.That (rv.ExitCode, Is.EqualTo (0), $"Exit code: {Executable} {StringUtils.FormatArguments (args)}");
}
- return new ExecutionResult (output, output, rv.ExitCode);
+ return new ExecutionResult (output, output, rv.ExitCode, rv.Duration);
}
public static ExecutionResult InstallWorkload (params string [] workloads)
@@ -136,7 +136,7 @@ public static ExecutionResult InstallWorkload (params string [] workloads)
Console.WriteLine (msg);
Assert.Fail (msg.ToString ());
}
- return new ExecutionResult (output, output, rv.ExitCode);
+ return new ExecutionResult (output, output, rv.ExitCode, rv.Duration);
}
public static ExecutionResult InstallTool (string tool, string path)
@@ -206,7 +206,7 @@ public static ExecutionResult ExecuteCommand (string exe, Dictionary? properties, bool assert_success = true, string? target = null, bool? msbuildParallelism = null, TimeSpan? timeout = null, params string [] extraArguments)
@@ -338,7 +338,7 @@ public static ExecutionResult Execute (string verb, string project, Dictionary
+
@@ -190,6 +191,11 @@
<_TestVariationApplied>true
+
+ true
+ <_TestVariationApplied>true
+
+
<_InvalidTestVariations Include="$(TestVariation.Split('|'))" Exclude="@(TestVariations)" />
diff --git a/tests/dotnet/UnitTests/AssetsTest.cs b/tests/dotnet/UnitTests/AssetsTest.cs
index 3466457299e9..c980bbf977a3 100644
--- a/tests/dotnet/UnitTests/AssetsTest.cs
+++ b/tests/dotnet/UnitTests/AssetsTest.cs
@@ -136,9 +136,9 @@ public static string GetFullSdkVersion (ApplePlatform platform, string runtimeId
switch (platform) {
case ApplePlatform.iOS:
if (runtimeIdentifiers.Contains ("simulator")) {
- return $"iphonesimulator{Configuration.sdk_version}";
+ return $"iphonesimulator{Configuration.ios_sdk_version}";
} else {
- return $"iphoneos{Configuration.sdk_version}";
+ return $"iphoneos{Configuration.ios_sdk_version}";
}
case ApplePlatform.TVOS:
if (runtimeIdentifiers.Contains ("simulator")) {
diff --git a/tests/dotnet/UnitTests/PerformanceTests.cs b/tests/dotnet/UnitTests/PerformanceTests.cs
new file mode 100644
index 000000000000..73f88a7af72e
--- /dev/null
+++ b/tests/dotnet/UnitTests/PerformanceTests.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace Xamarin.Tests {
+ [TestFixture]
+ public class PerformanceTests : TestBaseClass {
+ [Test]
+ [TestCase (ApplePlatform.iOS, "iossimulator-arm64")]
+ public void PrepareAssemblies (ApplePlatform platform, string runtimeIdentifiers)
+ {
+ Configuration.IgnoreIfIgnoredPlatform (platform);
+ Configuration.AssertRuntimeIdentifiersAvailable (platform, runtimeIdentifiers);
+
+ if (string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("ENABLE_PERFORMANCE_TESTS")))
+ Assert.Ignore ("Test ignored because ENABLE_PERFORMANCE_TESTS is not set.");
+
+ var projects = new string [] { "MySimpleApp", "monotouch-test" };
+ var results = new List ();
+ var attempts = 3;
+ foreach (var project in projects) {
+ var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath);
+
+ var result = new PrepareAssembliesTestResult {
+ Project = project,
+ };
+ foreach (var propertyValue in new bool [] { false, true }) {
+ var properties = GetDefaultProperties (runtimeIdentifiers);
+ properties ["PrepareAssemblies"] = propertyValue.ToString ();
+
+ for (var i = 1; i <= attempts; i++) {
+ Clean (project_path);
+ var rv = DotNet.AssertBuild (project_path, properties);
+ (propertyValue ? result.EnabledBuildTimes : result.DisabledBuildTimes).Add (rv.Duration);
+ }
+ }
+ results.Add (result);
+ }
+ foreach (var result in results.OrderBy (v => v.Project)) {
+ Console.WriteLine ($"Results for: {result.Project}");
+ Console.WriteLine ($" Enabled timings: {string.Join (", ", result.EnabledBuildTimes.Select (v => v.ToString ()))} average: {TimeSpan.FromTicks ((long) result.EnabledBuildTimes.Average (v => v.Ticks))}");
+ Console.WriteLine ($" Disabled timings: {string.Join (", ", result.DisabledBuildTimes.Select (v => v.ToString ()))} average: {TimeSpan.FromTicks ((long) result.DisabledBuildTimes.Average (v => v.Ticks))}");
+ }
+ }
+
+ class PrepareAssembliesTestResult {
+ public required string Project;
+ public List EnabledBuildTimes = new ();
+ public List DisabledBuildTimes = new ();
+ }
+ }
+}
+
diff --git a/tests/dotnet/UnitTests/TestBaseClass.cs b/tests/dotnet/UnitTests/TestBaseClass.cs
index e421218252eb..fbe234b535a6 100644
--- a/tests/dotnet/UnitTests/TestBaseClass.cs
+++ b/tests/dotnet/UnitTests/TestBaseClass.cs
@@ -133,6 +133,9 @@ protected static string GetDefaultRuntimeIdentifier (ApplePlatform platform, str
protected static string GetProjectPath (string project, string? subdir = null, ApplePlatform? platform = null)
{
+ if (TryGetTestProjectPath (project, platform ?? ApplePlatform.None, out var testProjectPath))
+ return testProjectPath;
+
var project_dir = Path.Combine (Configuration.SourceRoot, "tests", "dotnet", project);
if (!string.IsNullOrEmpty (subdir))
project_dir = Path.Combine (project_dir, subdir);
@@ -154,6 +157,18 @@ protected static string GetProjectPath (string project, string? subdir = null, A
return project_path;
}
+ static bool TryGetTestProjectPath (string project, ApplePlatform platform, [NotNullWhen (true)] out string? projectPath)
+ {
+ projectPath = null;
+
+ switch (project) {
+ case "monotouch-test":
+ projectPath = Path.Combine (Configuration.SourceRoot, "tests", project, "dotnet", platform.AsString (), project + ".csproj");
+ return true;
+ }
+ return false;
+ }
+
protected string GetPlugInsRelativePath (ApplePlatform platform)
{
switch (platform) {
diff --git a/tests/linker/link all/dotnet/shared.csproj b/tests/linker/link all/dotnet/shared.csproj
index 6b75ec2a2d4e..33fc00ceb233 100644
--- a/tests/linker/link all/dotnet/shared.csproj
+++ b/tests/linker/link all/dotnet/shared.csproj
@@ -9,6 +9,7 @@
$(MtouchLink)
--optimize=all,-remove-dynamic-registrar,-force-rejected-types-removal
$(MtouchExtraArgs) --optimize=-remove-uithread-checks
+ $(MtouchExtraArgs) --nowarn:2003
$(MtouchExtraArgs)
$(RootTestsDirectory)\linker\link all
true
diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/AssemblySetup.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/AssemblySetup.cs
index 5059aab86236..28575a0412a9 100644
--- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/AssemblySetup.cs
+++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/AssemblySetup.cs
@@ -17,7 +17,7 @@ public void AssemblyInitialization ()
const string msbuild_exe_path = "/Library/Frameworks/Mono.framework/Versions/Current/lib/mono/msbuild/15.0/bin/MSBuild.dll";
if (is_in_vsmac) {
var env = new Dictionary {
- { "DEVELOPER_DIR", Path.GetDirectoryName (Path.GetDirectoryName (Configuration.xcode_root)) ?? string.Empty },
+ { "DEVELOPER_DIR", Path.GetDirectoryName (Path.GetDirectoryName (Configuration.XcodeLocation)) ?? string.Empty },
{ "MSBUILD_EXE_PATH", msbuild_exe_path },
};
diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs
index 218b3cf4fa9e..958fcdb63f8b 100644
--- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs
+++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/ACToolTaskTest.cs
@@ -60,7 +60,7 @@ ACTool CreateACToolTask (ApplePlatform platform, string projectDir, out string i
task.MinimumOSVersion = Xamarin.SdkVersions.GetMinVersion (platform).ToString ();
task.OutputPath = Path.Combine (intermediateOutputPath, "OutputPath");
task.ProjectDir = projectDir;
- task.SdkDevPath = Configuration.xcode_root;
+ task.SdkDevPath = Configuration.XcodeLocation;
task.SdkPlatform = sdkPlatform;
task.SdkVersion = version.ToString ();
task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString ();
diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs
index c202f5e615aa..dc6ab813dde8 100644
--- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs
+++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/GetAvailableDevicesTest.cs
@@ -31,7 +31,7 @@ GetAvailableDevicesTaskWrapper CreateTask (ApplePlatform platform, string simctl
SimCtlJson = simctlJson,
DeviceCtlJson = devicectlJson,
};
- task.SdkDevPath = Configuration.xcode_root;
+ task.SdkDevPath = Configuration.XcodeLocation;
task.TargetFrameworkMoniker = TargetFramework.GetTargetFramework (platform).ToString ();
if (!string.IsNullOrEmpty (appManifest)) {
diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/IBToolTaskTests.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/IBToolTaskTests.cs
index 2b66ae4a5f93..bc6fdbf5f6a0 100644
--- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/IBToolTaskTests.cs
+++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/IBToolTaskTests.cs
@@ -44,7 +44,7 @@ IBTool CreateIBToolTask (ApplePlatform framework, string projectDir, string inte
task.MinimumOSVersion = PDictionary.OpenFile (Path.Combine (projectDir, "Info.plist")).GetMinimumOSVersion ();
task.ResourcePrefix = "Resources";
task.ProjectDir = projectDir;
- task.SdkDevPath = Configuration.xcode_root;
+ task.SdkDevPath = Configuration.XcodeLocation;
task.SdkPlatform = platform;
task.SdkVersion = version.ToString ();
task.TargetFrameworkMoniker = TargetFramework.DotNet_iOS_String;
diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeAppBundleTaskTest.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeAppBundleTaskTest.cs
index 309d9aa597f9..afe1a7eaa7b6 100644
--- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeAppBundleTaskTest.cs
+++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TaskTests/MergeAppBundleTaskTest.cs
@@ -60,7 +60,7 @@ MergeAppBundles CreateTask (string outputBundle, params string [] inputBundles)
var task = CreateTask ();
task.InputAppBundles = inputItems.ToArray ();
task.OutputAppBundle = outputBundle;
- task.SdkDevPath = Configuration.xcode_root;
+ task.SdkDevPath = Configuration.XcodeLocation;
return task;
}
diff --git a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TestHelpers/TestBase.cs b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TestHelpers/TestBase.cs
index 05b391b10fbf..8f0e1ba36e6d 100644
--- a/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TestHelpers/TestBase.cs
+++ b/tests/msbuild/Xamarin.MacDev.Tasks.Tests/TestHelpers/TestBase.cs
@@ -54,7 +54,7 @@ public virtual void Setup ()
var t = new T ();
t.BuildEngine = Engine;
if (t is XamarinTask xt)
- xt.SdkDevPath = Configuration.xcode_root;
+ xt.SdkDevPath = Configuration.XcodeLocation;
return t;
}
diff --git a/tests/mtouch/MLaunchTool.cs b/tests/mtouch/MLaunchTool.cs
index 5185e8f6cfd2..789d29a1dd4c 100644
--- a/tests/mtouch/MLaunchTool.cs
+++ b/tests/mtouch/MLaunchTool.cs
@@ -97,7 +97,7 @@ IList BuildArguments ()
}
if (!string.IsNullOrEmpty (platformName) && !string.IsNullOrEmpty (simType)) {
- var device = string.Format (":v2:runtime=com.apple.CoreSimulator.SimRuntime.{0}-{1},devicetype=com.apple.CoreSimulator.SimDeviceType.{2}", platformName, Configuration.sdk_version.Replace ('.', '-'), simType);
+ var device = string.Format (":v2:runtime=com.apple.CoreSimulator.SimRuntime.{0}-{1},devicetype=com.apple.CoreSimulator.SimDeviceType.{2}", platformName, Configuration.ios_sdk_version.Replace ('.', '-'), simType);
sb.Add ($"--device:{device}");
}
diff --git a/tests/xharness/Harness.cs b/tests/xharness/Harness.cs
index 33cf6b5ffb93..a8490b532f08 100644
--- a/tests/xharness/Harness.cs
+++ b/tests/xharness/Harness.cs
@@ -355,6 +355,13 @@ void PopulateUnitTestProjects ()
Timeout = (TimeSpan?) TimeSpan.FromMinutes (10),
Filter = "",
},
+ new {
+ Label = TestLabel.AssemblyProcessing,
+ ProjectPath = Path.GetFullPath (Path.Combine (HarnessConfiguration.RootDirectory, "assembly-preparer", "assembly-preparer-tests.csproj")),
+ Name = "Assembly processing tests",
+ Timeout = (TimeSpan?) TimeSpan.FromMinutes (10),
+ Filter = "",
+ },
new {
Label = TestLabel.Generator,
ProjectPath = Path.GetFullPath (Path.Combine (HarnessConfiguration.RootDirectory, "bgen", "bgen-tests.csproj")),
diff --git a/tests/xharness/Jenkins/TestVariationsFactory.cs b/tests/xharness/Jenkins/TestVariationsFactory.cs
index 167d8f6247bb..b623ed22710c 100644
--- a/tests/xharness/Jenkins/TestVariationsFactory.cs
+++ b/tests/xharness/Jenkins/TestVariationsFactory.cs
@@ -33,6 +33,7 @@ IEnumerable GetTestData (RunTestTask test)
var arm64_sim_runtime_identifier = string.Empty;
var x64_sim_runtime_identifier = string.Empty;
var supports_coreclr = test.Platform == TestPlatform.Mac || jenkins.Harness.DotNetVersion.Major >= 11;
+ var supports_monovm = test.Platform != TestPlatform.Mac;
var supports_x64 = string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("ACES")); // x64 is not supported on ACES machines
switch (test.Platform) {
@@ -55,7 +56,39 @@ IEnumerable GetTestData (RunTestTask test)
}
switch (test.TestName) {
+ case "dont link":
+ if (supports_coreclr) {
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Dynamic Registrar)", TestVariation = "coreclr|prepare-assemblies|dynamic-registrar", Ignored = ignore };
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Managed Static Registrar)", TestVariation = "coreclr|prepare-assemblies|managed-static-registrar", Ignored = ignore };
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Trimmable Static Registrar)", TestVariation = "coreclr|prepare-assemblies|trimmable-static-registrar", Ignored = ignore };
+ }
+ if (supports_monovm) {
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Dynamic Registrar)", TestVariation = "monovm|prepare-assemblies|dynamic-registrar", Ignored = ignore };
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Managed Static Registrar)", TestVariation = "monovm|prepare-assemblies|managed-static-registrar", Ignored = ignore };
+ if (jenkins.Harness.DotNetVersion.Major >= 11) { // on Mono, the trimmable static registrar only works in .NET 11
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Trimmable Static Registrar)", TestVariation = "monovm|prepare-assemblies|trimmable-static-registrar", Ignored = ignore };
+ }
+ }
+ break;
+ case "link sdk":
+ if (supports_coreclr) {
+ // if prepare-assemblies is enabled, then linking only works in any meaningful way when using the trimmable static registrar, which only works on CoreCLR in .NET 10
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Trimmable Static Registrar)", TestVariation = "coreclr|prepare-assemblies|trimmable-static-registrar", Ignored = ignore };
+ }
+ if (supports_monovm && jenkins.Harness.DotNetVersion.Major >= 11) {
+ // if prepare-assemblies is enabled, then linking only works in any meaningful way when using the trimmable static registrar, which, on Mono, only works in .NET 11
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Trimmable Static Registrar)", TestVariation = "monovm|prepare-assemblies|trimmable-static-registrar", Ignored = ignore };
+ }
+ break;
case "link all":
+ if (supports_coreclr) {
+ // if prepare-assemblies is enabled, then linking only works in any meaningful way when using the trimmable static registrar, which only works on CoreCLR in .NET 10
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, CoreCLR, Trimmable Static Registrar)", TestVariation = "coreclr|prepare-assemblies|trimmable-static-registrar", Ignored = ignore };
+ }
+ if (supports_monovm && jenkins.Harness.DotNetVersion.Major >= 11) {
+ // if prepare-assemblies is enabled, then linking only works in any meaningful way when using the trimmable static registrar, which, on Mono, only works in .NET 11
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies, MonoVM, Trimmable Static Registrar)", TestVariation = "monovm|prepare-assemblies|trimmable-static-registrar", Ignored = ignore };
+ }
if (test.ProjectConfiguration == "Debug") {
yield return new TestData { Variation = "Debug (don't bundle original resources)", TestVariation = "do-not-bundle-original-resources" };
}
@@ -63,6 +96,7 @@ IEnumerable GetTestData (RunTestTask test)
case "monotouch-test":
yield return new TestData { Variation = "Release (link sdk)", TestVariation = "release|linksdk", Ignored = ignore };
yield return new TestData { Variation = "Release (link all)", TestVariation = "release|linkall", Ignored = ignore };
+ yield return new TestData { Variation = $"{test.ProjectConfiguration} (PrepareAssemblies)", TestVariation = "prepare-assemblies", Ignored = ignore };
break;
}
diff --git a/tests/xharness/TestLabel.cs b/tests/xharness/TestLabel.cs
index 9085a1eb815f..0bc5e9d78910 100644
--- a/tests/xharness/TestLabel.cs
+++ b/tests/xharness/TestLabel.cs
@@ -44,7 +44,8 @@ public enum TestLabel : Int64 {
Generator = 1 << 11,
[Label ("interdependent-binding-projects")]
InterdependentBindingProjects = 1 << 12,
- // 1 << 13 is unused
+ [Label ("assembly-processing")]
+ AssemblyProcessing = 1 << 13,
[Label ("introspection")]
Introspection = 1 << 14,
[Label ("linker")]
diff --git a/tools/.vscode/tasks.json b/tools/.vscode/tasks.json
new file mode 100644
index 000000000000..45465f774797
--- /dev/null
+++ b/tools/.vscode/tasks.json
@@ -0,0 +1,17 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "shell",
+ "command": "make",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [
+ "$msCompile"
+ ],
+ "label": "make"
+ }
+ ]
+}
diff --git a/tools/Makefile b/tools/Makefile
index 00d896286809..c4c969bf82b7 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -14,3 +14,4 @@ SUBDIRS+=mlaunch
SUBDIRS += dotnet-linker
+SUBDIRS += assembly-preparer
diff --git a/tools/ap-launcher/.gitignore b/tools/ap-launcher/.gitignore
new file mode 100644
index 000000000000..af1e7137432d
--- /dev/null
+++ b/tools/ap-launcher/.gitignore
@@ -0,0 +1,2 @@
+.build-stamp
+
diff --git a/tools/ap-launcher/.vscode/tasks.json b/tools/ap-launcher/.vscode/tasks.json
new file mode 100644
index 000000000000..69a9b5f361b2
--- /dev/null
+++ b/tools/ap-launcher/.vscode/tasks.json
@@ -0,0 +1,17 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "shell",
+ "command": "make",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [
+ "$msCompile"
+ ],
+ "label": "build"
+ }
+ ]
+}
diff --git a/tools/ap-launcher/Makefile b/tools/ap-launcher/Makefile
new file mode 100644
index 000000000000..adb26c8c4e3c
--- /dev/null
+++ b/tools/ap-launcher/Makefile
@@ -0,0 +1,8 @@
+TOP=../..
+include $(TOP)/Make.config
+include $(TOP)/mk/rules.mk
+
+build:
+ $(Q_BUILD) $(DOTNET) build /bl:$@.binlog *.csproj $(DOTNET_BUILD_VERBOSITY)
+
+all-local:: build
diff --git a/tools/ap-launcher/Program.cs b/tools/ap-launcher/Program.cs
new file mode 100644
index 000000000000..44da342c46a9
--- /dev/null
+++ b/tools/ap-launcher/Program.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Xamarin.Build;
+using Xamarin.Bundler;
+using Xamarin.Utils;
+
+class Program {
+ public static int Main (string [] args)
+ {
+ var optionsFile = args.Single (v => v.StartsWith ("--options-file=")).Substring ("--options-file=".Length);
+ var makeReproPath = args.SingleOrDefault (v => v.StartsWith ("--make-repro-path="))?.Substring ("--make-repro-path=".Length) ?? "";
+
+ var api = new List ();
+ foreach (var inputArgs in args.Where (v => v.StartsWith ("--input-assembly=")).Select (v => v.Substring ("--input-assembly=".Length))) {
+ var ia = inputArgs.Split ('|');
+ var inputPath = ia.Single (v => v.StartsWith ("InputPath="))?.Substring ("InputPath=".Length) ?? throw new InvalidOperationException ("InputPath is required");
+ var outputPath = ia.Single (v => v.StartsWith ("OutputPath="))?.Substring ("OutputPath=".Length) ?? throw new InvalidOperationException ("OutputPath is required");
+ var isTrimmableString = ia.Single (v => v.StartsWith ("IsTrimmable="))?.Substring ("IsTrimmable=".Length);
+ var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase);
+ var trimMode = ia.Single (v => v.StartsWith ("TrimMode="))?.Substring ("TrimMode=".Length) ?? "";
+
+ api.Add (new AssemblyPreparerInfo (inputPath, outputPath, isTrimmable, trimMode));
+ }
+
+ var platformString = File.ReadAllLines (optionsFile).Single (v => v.StartsWith ("Platform=")).Substring ("Platform=".Length);
+ var platform = ApplePlatformExtensions.Parse (platformString);
+
+ var infos = api.ToArray ();
+ var logger = new TestLogger () {
+ Platform = platform,
+ };
+ using var preparer = new AssemblyPreparer (logger, infos, optionsFile);
+ preparer.MakeReproPath = makeReproPath ?? "";
+ var rv = preparer.Prepare (out var exceptions);
+
+ return rv && exceptions.Count == 0 ? 0 : 1;
+ }
+}
+
+class TestLogger : IToolLog {
+ public int Verbosity => 0;
+ public required ApplePlatform Platform { get; set; }
+
+ public void Log (string value)
+ {
+ Console.WriteLine (value);
+ }
+
+ public void LogError (string value)
+ {
+ Console.WriteLine (value);
+ }
+
+ public void Log (string format, params object? [] args)
+ {
+ Console.WriteLine (format, args);
+ }
+
+ public void LogException (Exception ex)
+ {
+ Console.Error.WriteLine (ex.ToString ());
+ }
+
+ public void LogError (ProductException ex)
+ {
+ Console.Error.WriteLine (ex.ToString ());
+ }
+
+ public void LogWarning (ProductException ex)
+ {
+ Console.WriteLine (ex.ToString ());
+ }
+}
diff --git a/tools/ap-launcher/README.md b/tools/ap-launcher/README.md
new file mode 100644
index 000000000000..5007c9cc4226
--- /dev/null
+++ b/tools/ap-launcher/README.md
@@ -0,0 +1,2 @@
+This is just a simple project that references the assembly-preparer library, to make it easy to debug the library in a debugger.
+
diff --git a/tools/ap-launcher/ap-launcher.csproj b/tools/ap-launcher/ap-launcher.csproj
new file mode 100644
index 000000000000..ea7daa76d3f5
--- /dev/null
+++ b/tools/ap-launcher/ap-launcher.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net$(BundledNETCoreAppTargetFrameworkVersion)
+ false
+ false
+ ap_launcher
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/tools/ap-launcher/ap-launcher.slnx b/tools/ap-launcher/ap-launcher.slnx
new file mode 100644
index 000000000000..f543c9a91276
--- /dev/null
+++ b/tools/ap-launcher/ap-launcher.slnx
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tools/assembly-preparer/.gitignore b/tools/assembly-preparer/.gitignore
new file mode 100644
index 000000000000..65bf06abaee5
--- /dev/null
+++ b/tools/assembly-preparer/.gitignore
@@ -0,0 +1,2 @@
+*.csproj.inc
+
diff --git a/tools/assembly-preparer/.vscode/tasks.json b/tools/assembly-preparer/.vscode/tasks.json
new file mode 100644
index 000000000000..69a9b5f361b2
--- /dev/null
+++ b/tools/assembly-preparer/.vscode/tasks.json
@@ -0,0 +1,17 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "shell",
+ "command": "make",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": [
+ "$msCompile"
+ ],
+ "label": "build"
+ }
+ ]
+}
diff --git a/tools/assembly-preparer/AssemblyPreparer.cs b/tools/assembly-preparer/AssemblyPreparer.cs
new file mode 100644
index 000000000000..9f2936c32eaa
--- /dev/null
+++ b/tools/assembly-preparer/AssemblyPreparer.cs
@@ -0,0 +1,331 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.IO;
+using System.Runtime.Serialization;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.CompilerServices.SymbolWriter;
+using Mono.Linker;
+using Mono.Linker.Steps;
+using Mono.Tuner;
+using MonoTouch.Tuner;
+using Xamarin.Bundler;
+using Xamarin.Linker;
+using Xamarin.Linker.Steps;
+using Xamarin.Tuner;
+using Xamarin.Utils;
+
+namespace Xamarin.Build;
+
+public class AssemblyPreparer : IDisposable {
+ AggregateLog log = new AggregateLog ();
+
+ LinkerConfiguration configuration;
+
+ public LinkerConfiguration Configuration => configuration;
+
+ public string MakeReproPath { get; set; } = string.Empty;
+
+ public RegistrarMode Registrar {
+ get => configuration.Application.Registrar;
+ set => configuration.Application.Registrar = value;
+ }
+
+ public string IntermediateOutputPath {
+ get => configuration.IntermediateOutputPath;
+ }
+
+ public Optimizations Optimizations => configuration.Application.Optimizations;
+
+ public List Assemblies { get; set; } = new List ();
+
+ public IList<(string Path, AssemblyDefinition Assembly, string? OriginatingAssembly)> AddedAssemblies => configuration.AddedAssemblies;
+
+ LinkerConfiguration.Configurator GetConfigurator (string? reproPath = null, Func? assemblyPreparerInfoFactory = null)
+ {
+ var dict = new LinkerConfiguration.Configurator () {
+ { "AssemblyPreparer", (
+ new LinkerConfiguration.LoadValue ((key, value) => {
+ var split = value.Split ('|');
+ var input = split[0];
+ var output = split[1];
+ var isTrimmableString = split[2];
+ var isTrimmable = string.IsNullOrEmpty (isTrimmableString) ? (bool?) null : string.Equals (isTrimmableString, "true", StringComparison.OrdinalIgnoreCase);
+ var trimMode = split[3];
+ var apinfo = assemblyPreparerInfoFactory is not null ? assemblyPreparerInfoFactory (input, output) : new AssemblyPreparerInfo (input, output, isTrimmable, trimMode);
+ Assemblies.Add (apinfo);
+ }),
+ new LinkerConfiguration.SaveValue ((key, storage) => SaveAssemblies (key, storage, reproPath, Assemblies))
+ )},
+ };
+ return dict;
+ }
+
+ static void SaveAssemblies (string key, List storage, string? reproPath, IList assemblies)
+ {
+ foreach (var assembly in assemblies) {
+ var input = assembly.InputPath;
+ var output = assembly.OutputPath;
+ if (!string.IsNullOrEmpty (reproPath)) {
+ output = Path.Combine (reproPath, Path.GetFileName (output));
+ File.Copy (input, output);
+ }
+ storage.Add ($"{key}={input}|{output}|{(assembly.IsTrimmable.HasValue ? (assembly.IsTrimmable.Value ? "true" : "false") : "")}|{assembly.TrimMode}");
+ }
+ }
+
+ public AssemblyPreparer (IToolLog log, AssemblyPreparerInfo [] assemblies, string linker_file)
+ {
+ var lines = File.ReadAllLines (linker_file).ToList ();
+ SaveAssemblies ("AssemblyPreparer", lines, null, assemblies);
+ configuration = new LinkerConfiguration (log, lines, linker_file, GetConfigurator (null, assemblies.Length == 0 ? null : (input, output) => assemblies.Single (a => a.InputPath == input && a.OutputPath == output)));
+ }
+
+ public void AddLog (IAssemblyPreparerLog log)
+ {
+ if (log is null)
+ throw new ArgumentNullException (nameof (log));
+ this.log.Add (log);
+ }
+
+ bool SaveToReproPath (List exceptions)
+ {
+ if (File.Exists (MakeReproPath) || Directory.Exists (MakeReproPath)) {
+ exceptions.Add (ErrorHelper.CreateError (99, $"Repro location already exists: {MakeReproPath}"));
+ return false;
+ }
+ Directory.CreateDirectory (MakeReproPath);
+ var lines = new List ();
+ configuration.Save (lines, GetConfigurator (MakeReproPath));
+ File.WriteAllLines (Path.Combine (MakeReproPath, "arguments.txt"), lines);
+ log.Log ($"Created repro in {MakeReproPath}");
+
+ return true;
+ }
+
+ public static AssemblyPreparer LoadFromReproPath (string reproPath)
+ {
+ var file = Path.Combine (reproPath, "arguments.txt");
+ if (!File.Exists (file))
+ throw new FileNotFoundException ($"Repro arguments file not found: {file}");
+ return new AssemblyPreparer (ConsoleLog.Instance, Array.Empty (), file);
+ }
+
+ public bool Prepare (out List exceptions)
+ {
+ exceptions = configuration.Exceptions;
+
+ if (Registrar == RegistrarMode.Default) {
+ exceptions.Add (ErrorHelper.CreateError (99, "RegistrarMode must be explicitly set."));
+ return false;
+ }
+
+ if (!string.IsNullOrEmpty (MakeReproPath) && !SaveToReproPath (exceptions))
+ return false;
+
+ var steps = new ConfigurationAwareStep [] {
+ // All the same steps as the custom trimmer steps that are run before MarkStep in Xamarin.Shared.Sdk.targets (and in the same order).
+ // CollectAssembliesStep
+ new CoreTypeMapStep (),
+ // ProcessExportedFields
+ new PreserveProtocolsStep (),
+ new PreserveSmartEnumConversionsStep (),
+ new PreserveBlockCodeStep (),
+ new OptimizeGeneratedCodeStep (),
+ new ApplyPreserveAttributeStep (),
+ new MarkForStaticRegistrarStep (),
+ new MarkNSObjectsStep (),
+ new InlineDlfcnMethodsStep (),
+ new RegistrarRemovalTrackingStep (),
+ // PreMarkDispatcher: I don't think we need this one
+ new ManagedRegistrarStep (),
+ new TrimmableRegistrarStep (),
+ new ManagedRegistrarLookupTablesStep (),
+ };
+
+ var linkContext = configuration.DerivedLinkContext;
+
+ var parameters = new ReaderParameters {
+ AssemblyResolver = configuration.AssemblyResolver,
+ MetadataResolver = configuration.MetadataResolver,
+ ReadSymbols = true,
+ SymbolReaderProvider = new DefaultSymbolReaderProvider (throwIfNoSymbol: false),
+ };
+
+ foreach (var assembly in Assemblies) {
+ var assemblyDefinition = AssemblyDefinition.ReadAssembly (assembly.InputPath, parameters);
+ linkContext.Assemblies.Add (assemblyDefinition);
+ assembly.Assembly = assemblyDefinition;
+ configuration.Context.Annotations.SetAction (assemblyDefinition, ComputeAssemblyAction (assemblyDefinition, assembly));
+ configuration.AssemblyResolver.ResolverCache.Add (assemblyDefinition.Name.Name, assemblyDefinition);
+ }
+
+ configuration.Context.Annotations.CollectOverrides (linkContext.Assemblies, linkContext);
+
+ foreach (var step in steps) {
+ step.Process (linkContext);
+ }
+
+ // save assemblies
+
+ foreach (var assembly in Assemblies) {
+ var assemblyDefinition = assembly.Assembly!;
+
+ var action = configuration.Context.Annotations.GetAction (assemblyDefinition);
+ switch (action) {
+ case AssemblyAction.Copy:
+ case AssemblyAction.CopyUsed:
+ assembly.OutputPath = assembly.InputPath;
+ continue;
+ case AssemblyAction.Link:
+ case AssemblyAction.Save:
+ log.Log ($"Saving {assembly.InputPath} to {assembly.OutputPath}");
+ break;
+ default:
+ exceptions.Add (ErrorHelper.CreateError (99, $"Unknown link action: {action} for assembly {assemblyDefinition.Name}"));
+ return false;
+ }
+
+ Directory.CreateDirectory (Path.GetDirectoryName (assembly.OutputPath)!);
+ var writerParameters = new WriterParameters ();
+ if (assemblyDefinition.MainModule.HasSymbols) {
+ var provider = new CustomSymbolWriterProvider ();
+ try {
+ using (var tmp = provider.GetSymbolWriter (assemblyDefinition.MainModule, Path.ChangeExtension (assembly.OutputPath, ".pdb"))) { }
+ File.Delete (Path.ChangeExtension (assembly.OutputPath, ".pdb"));
+ writerParameters.WriteSymbols = true;
+ writerParameters.SymbolWriterProvider = provider;
+ } catch (Exception e) {
+ log.Log ($"Failed to create symbol writer for {assembly.OutputPath}, not writing symbols: {e.Message}");
+ }
+ }
+
+ RemoveCrossGen (assemblyDefinition);
+
+ try {
+ assemblyDefinition.Write (assembly.OutputPath, writerParameters);
+ ModuleAttributes m = assemblyDefinition.MainModule.Attributes;
+ } catch (Exception e) {
+ exceptions.Add (ErrorHelper.CreateError (99, e, $"Failed to write {assembly.OutputPath}: {e.Message}"));
+ log.Log ($"Failed to write {assembly.OutputPath}: {e}");
+ return false;
+ }
+ }
+
+ return exceptions.Count == 0;
+ }
+
+ void RemoveCrossGen (AssemblyDefinition assemblyDefinition)
+ {
+ // Drop crossgened code from the assembly
+ // Ref: https://github.com/dotnet/runtime/blob/b86458593223f866effa63122b05bec37f83015e/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs#L95-L105
+ foreach (var module in assemblyDefinition.Modules) {
+ var moduleAttributes = module.Attributes;
+ var isCrossGened = (moduleAttributes & ModuleAttributes.ILOnly) == 0 && (moduleAttributes & ModuleAttributes.ILLibrary) == ModuleAttributes.ILLibrary;
+ if (isCrossGened) {
+ moduleAttributes |= ModuleAttributes.ILOnly;
+ moduleAttributes &= ~ModuleAttributes.ILLibrary;
+ module.Attributes = moduleAttributes;
+ module.Architecture = TargetArchitecture.I386;
+ module.Characteristics |= ModuleCharacteristics.NoSEH;
+ }
+ }
+ }
+
+ // Figure out if an assembly is trimmed or not.
+ // This must be identical to how it's done for ILLink/ILC.
+ AssemblyAction ComputeAssemblyAction (AssemblyDefinition assembly, AssemblyPreparerInfo info)
+ {
+ // Unless 'PublishTrimmed=true', nothing is trimmed, because we won't run the trimmer.
+ if (!configuration.PublishTrimmed)
+ return AssemblyAction.Copy;
+
+ // Then if 'TrimMode' is set on the assembly, then that takes precedence
+ switch (info.TrimMode?.ToLowerInvariant () ?? "") {
+ case "link":
+ return AssemblyAction.Link;
+ case "copy":
+ return AssemblyAction.Copy;
+ case "":
+ break;
+ default:
+ throw new ArgumentException ($"Unknown trim mode: {info.TrimMode} for assembly {assembly.Name}");
+ }
+
+ // Then if 'IsTrimmable' is set on the assembly, that takes precedence over the default for the platform.
+ if (info.IsTrimmable == false)
+ return AssemblyAction.CopyUsed;
+ else if (info.IsTrimmable == true)
+ return AssemblyAction.Link;
+
+ // Check the global 'TrimMode' property, if it's not 'link', 'partial' or 'full', then we're not trimming anything
+ var globalTrimMode = configuration.TrimMode.ToLowerInvariant ();
+ switch (globalTrimMode) {
+ case "copy":
+ case "":
+ return AssemblyAction.Copy;
+ case "partial":
+ case "full":
+ case "link":
+ break;
+ default:
+ throw new ArgumentException ($"Unknown global trim mode: {configuration.TrimMode}");
+ }
+
+ // Check the [AssemblyMetadata] attribute
+ var isTrimmableAttribute = assembly.CustomAttributes
+ .Where (v => v.AttributeType.FullName == "System.Reflection.AssemblyMetadataAttribute")
+ .Where (v => v.HasConstructorArguments && v.ConstructorArguments.Count == 2 && v.ConstructorArguments [0].Type.Is ("System", "String") && v.ConstructorArguments [1].Type.Is ("System", "String"))
+ .Where (v => (v.ConstructorArguments [0].Value as string) == "IsTrimmable" && string.Equals (v.ConstructorArguments [1].Value as string, "true", StringComparison.OrdinalIgnoreCase))
+ .SingleOrDefault ();
+
+ if (isTrimmableAttribute is null) {
+ // If the attribute is not present, then we trim if the global 'TrimMode' is 'full'
+ return globalTrimMode switch {
+ "link" => AssemblyAction.Copy,
+ "partial" => AssemblyAction.Copy,
+ "full" => AssemblyAction.Link,
+ _ => throw new ArgumentException ($"Unknown global trim mode: {configuration.TrimMode}"),
+ };
+ }
+
+ // if the attribute is present, then we trim if the global 'TrimMode' is 'partial', 'full' or 'link', which are the only values it should have at this point
+ switch (globalTrimMode) {
+ case "partial":
+ case "full":
+ case "link":
+ break;
+ default:
+ // we shouldn't get here for any other trim mode value
+ throw new ArgumentException ($"Unexpected global trim mode: {configuration.TrimMode}");
+ }
+
+ return AssemblyAction.Link;
+ }
+
+ public void Dispose ()
+ {
+ foreach (var assembly in Assemblies)
+ assembly.Assembly?.Dispose ();
+ configuration.AssemblyResolver.ResolverCache.Clear ();
+ configuration.DerivedLinkContext.Assemblies.Clear ();
+ }
+}
+
+public class AssemblyPreparerInfo {
+ internal AssemblyDefinition? Assembly { get; set; }
+
+ public string InputPath { get; private set; }
+ public bool? IsTrimmable { get; set; }
+ public string TrimMode { get; set; }
+ public string OutputPath { get; set; }
+
+ public AssemblyPreparerInfo (string inputPath, string outputPath, bool? isTrimmable, string trimMode)
+ {
+ InputPath = inputPath;
+ OutputPath = outputPath;
+ IsTrimmable = isTrimmable;
+ TrimMode = trimMode;
+ }
+}
diff --git a/tools/assembly-preparer/DynamicallyAccessedMemberTypes.cs b/tools/assembly-preparer/DynamicallyAccessedMemberTypes.cs
new file mode 100644
index 000000000000..4a42e76ab54c
--- /dev/null
+++ b/tools/assembly-preparer/DynamicallyAccessedMemberTypes.cs
@@ -0,0 +1,176 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// This file is needed because we compile the current project under netstandard2.0, and this type doesn't exist there.
+
+#if !NET
+
+// From https://raw.githubusercontent.com/dotnet/dotnet/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/DynamicallyAccessedMemberTypes.cs
+
+global using DynamicallyAccessedMemberTypes = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes2;
+
+namespace System.Diagnostics.CodeAnalysis {
+ using System.ComponentModel;
+
+ ///
+ /// Specifies the types of members that are dynamically accessed.
+ ///
+ /// This enumeration has a attribute that allows a
+ /// bitwise combination of its member values.
+ ///
+ [Flags]
+ enum DynamicallyAccessedMemberTypes2 {
+ ///
+ /// Specifies no members.
+ ///
+ None = 0,
+
+ ///
+ /// Specifies the default, parameterless public constructor.
+ ///
+ PublicParameterlessConstructor = 0x0001,
+
+ ///
+ /// Specifies all public constructors.
+ ///
+ PublicConstructors = 0x0002 | PublicParameterlessConstructor,
+
+ ///
+ /// Specifies all non-public constructors.
+ ///
+ NonPublicConstructors = 0x0004,
+
+ ///
+ /// Specifies all public methods.
+ ///
+ PublicMethods = 0x0008,
+
+ ///
+ /// Specifies all non-public methods.
+ ///
+ NonPublicMethods = 0x0010,
+
+ ///
+ /// Specifies all public fields.
+ ///
+ PublicFields = 0x0020,
+
+ ///
+ /// Specifies all non-public fields.
+ ///
+ NonPublicFields = 0x0040,
+
+ ///
+ /// Specifies all public nested types.
+ ///
+ PublicNestedTypes = 0x0080,
+
+ ///
+ /// Specifies all non-public nested types.
+ ///
+ NonPublicNestedTypes = 0x0100,
+
+ ///
+ /// Specifies all public properties.
+ ///
+ PublicProperties = 0x0200,
+
+ ///
+ /// Specifies all non-public properties.
+ ///
+ NonPublicProperties = 0x0400,
+
+ ///
+ /// Specifies all public events.
+ ///
+ PublicEvents = 0x0800,
+
+ ///
+ /// Specifies all non-public events.
+ ///
+ NonPublicEvents = 0x1000,
+
+ ///
+ /// Specifies all interfaces implemented by the type.
+ ///
+ Interfaces = 0x2000,
+
+ ///
+ /// Specifies all non-public constructors, including those inherited from base classes.
+ ///
+ NonPublicConstructorsWithInherited = NonPublicConstructors | 0x4000,
+
+ ///
+ /// Specifies all non-public methods, including those inherited from base classes.
+ ///
+ NonPublicMethodsWithInherited = NonPublicMethods | 0x8000,
+
+ ///
+ /// Specifies all non-public fields, including those inherited from base classes.
+ ///
+ NonPublicFieldsWithInherited = NonPublicFields | 0x10000,
+
+ ///
+ /// Specifies all non-public nested types, including those inherited from base classes.
+ ///
+ NonPublicNestedTypesWithInherited = NonPublicNestedTypes | 0x20000,
+
+ ///
+ /// Specifies all non-public properties, including those inherited from base classes.
+ ///
+ NonPublicPropertiesWithInherited = NonPublicProperties | 0x40000,
+
+ ///
+ /// Specifies all non-public events, including those inherited from base classes.
+ ///
+ NonPublicEventsWithInherited = NonPublicEvents | 0x80000,
+
+ ///
+ /// Specifies all public constructors, including those inherited from base classes.
+ ///
+ PublicConstructorsWithInherited = PublicConstructors | 0x100000,
+
+ ///
+ /// Specifies all public nested types, including those inherited from base classes.
+ ///
+ PublicNestedTypesWithInherited = PublicNestedTypes | 0x200000,
+
+ ///
+ /// Specifies all constructors, including those inherited from base classes.
+ ///
+ AllConstructors = PublicConstructorsWithInherited | NonPublicConstructorsWithInherited,
+
+ ///
+ /// Specifies all methods, including those inherited from base classes.
+ ///
+ AllMethods = PublicMethods | NonPublicMethodsWithInherited,
+
+ ///
+ /// Specifies all fields, including those inherited from base classes.
+ ///
+ AllFields = PublicFields | NonPublicFieldsWithInherited,
+
+ ///
+ /// Specifies all nested types, including those inherited from base classes.
+ ///
+ AllNestedTypes = PublicNestedTypesWithInherited | NonPublicNestedTypesWithInherited,
+
+ ///
+ /// Specifies all properties, including those inherited from base classes.
+ ///
+ AllProperties = PublicProperties | NonPublicPropertiesWithInherited,
+
+ ///
+ /// Specifies all events, including those inherited from base classes.
+ ///
+ AllEvents = PublicEvents | NonPublicEventsWithInherited,
+
+ ///
+ /// Specifies all members.
+ ///
+ [EditorBrowsable (EditorBrowsableState.Never)]
+ All = ~None
+ }
+}
+
+#endif // !NET
diff --git a/tools/assembly-preparer/GlobalUsings.cs b/tools/assembly-preparer/GlobalUsings.cs
new file mode 100644
index 000000000000..ba0a28435170
--- /dev/null
+++ b/tools/assembly-preparer/GlobalUsings.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+global using System;
+global using System.Collections.Generic;
+global using System.Diagnostics.CodeAnalysis;
+global using System.Linq;
+global using System.Runtime.InteropServices;
+
+global using Foundation;
+global using ObjCRuntime;
+
+global using Xamarin.Linker;
+
+namespace Xamarin.Tuner { }
+namespace Mono.Linker.Steps { }
diff --git a/tools/assembly-preparer/IAssemblyPreparerLog.cs b/tools/assembly-preparer/IAssemblyPreparerLog.cs
new file mode 100644
index 000000000000..f41de7ca475e
--- /dev/null
+++ b/tools/assembly-preparer/IAssemblyPreparerLog.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using Xamarin.Bundler;
+
+namespace Xamarin.Build;
+
+public interface IAssemblyPreparerLog {
+ void Log (string message);
+}
+
+class AggregateLog : IAssemblyPreparerLog {
+ List logs = new ();
+
+ public void Add (IAssemblyPreparerLog log)
+ {
+ logs.Add (log);
+ }
+
+ public void Log (string message)
+ {
+ foreach (var log in logs)
+ log.Log (message);
+ }
+}
diff --git a/tools/assembly-preparer/Makefile b/tools/assembly-preparer/Makefile
new file mode 100644
index 000000000000..587a1d6116f5
--- /dev/null
+++ b/tools/assembly-preparer/Makefile
@@ -0,0 +1,25 @@
+TOP=../..
+include $(TOP)/Make.config
+include $(TOP)/mk/rules.mk
+include ../common/Make.common
+
+# assembly-preparer.csproj.inc contains the $(assembly_preparer_dependencies) variable used to determine if assembly-preparer needs to be rebuilt or not.
+assembly-preparer.csproj.inc: export BUILD_VERBOSITY=$(MSBUILD_VERBOSITY)
+-include assembly-preparer.csproj.inc
+
+ASSEMBLY_PREPARER_NETCORE=bin/Debug/$(DOTNET_TFM)/assembly-preparer.dll
+ASSEMBLY_PREPARER_NETSTD=bin/Debug/netstandard2.0/assembly-preparer.dll
+ASSEMBLIES=$(ASSEMBLY_PREPARER_NETCORE) $(ASSEMBLY_PREPARER_NETSTD)
+
+.build-stamp: $(assembly_preparer_dependencies)
+ $(Q_BUILD) $(DOTNET) build /bl:$@.binlog *.csproj $(DOTNET_BUILD_VERBOSITY)
+ $(Q) touch $(ASSEMBLIES)
+
+$(ASSEMBLIES): .build-stamp
+
+all-local:: .build-stamp
+
+run-tests:
+ $(Q_BUILD) $(DOTNET) test $(TOP)/tests/assembly-preparer/assembly-preparer-tests.csproj --logger "console;verbosity=detailed" $(DOTNET_BUILD_VERBOSITY) -bl:$@.binlog
+
+generated-files: $(abspath ../common/SdkVersions.cs) $(abspath ../common/ProductConstants.cs)
diff --git a/tools/assembly-preparer/NetStandardExtensions.cs b/tools/assembly-preparer/NetStandardExtensions.cs
new file mode 100644
index 000000000000..5f81fd7f1b93
--- /dev/null
+++ b/tools/assembly-preparer/NetStandardExtensions.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// This file is needed because we compile the current project under netstandard2.0, and the types here don't exist in netstandard2.0
+
+#if !NET
+
+public static class QueueExtensions {
+ public static bool TryDequeue (this Queue queue, [MaybeNullWhen (false)] out T item)
+ {
+ if (queue.Count == 0) {
+ item = default;
+ return false;
+ }
+ item = queue.Dequeue ();
+ return true;
+ }
+
+ public static bool TryAdd (this Dictionary dictionary, T key, V value)
+ {
+ if (dictionary.ContainsKey (key))
+ return false;
+ dictionary.Add (key, value);
+ return true;
+ }
+}
+
+public static class DictionaryExtensions {
+ public static bool Remove (this Dictionary dictionary, T key, [MaybeNullWhen (false)] out V value)
+ {
+ if (dictionary.TryGetValue (key, out value)) {
+ dictionary.Remove (key);
+ return true;
+ }
+ return false;
+ }
+}
+
+public static class EnumerableExtensions {
+ public static IEnumerable SkipLast (this IEnumerable source, int count)
+ {
+ // very naive implementation, but it's only for netstandard2.0, which will go away soon, so no need to optimize it
+ var rv = source.ToList ();
+ if (rv.Count <= count)
+ return [];
+ rv.RemoveRange (rv.Count - count, count);
+ return rv;
+ }
+}
+
+// From: https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RequiredMemberAttribute.cs
+namespace System.Runtime.CompilerServices {
+ using System.ComponentModel;
+
+ /// Specifies that a type has required members or that a member is required.
+ [AttributeUsage (AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+ [EditorBrowsable (EditorBrowsableState.Never)]
+ internal sealed class RequiredMemberAttribute : Attribute { }
+}
+
+// From: https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CompilerFeatureRequiredAttribute.cs
+namespace System.Runtime.CompilerServices {
+ using System.ComponentModel;
+
+ ///
+ /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied.
+ ///
+ [AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)]
+ internal sealed class CompilerFeatureRequiredAttribute : Attribute {
+ public CompilerFeatureRequiredAttribute (string featureName)
+ {
+ FeatureName = featureName;
+ }
+
+ ///
+ /// The name of the compiler feature.
+ ///
+ public string FeatureName { get; }
+
+ ///
+ /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand .
+ ///
+ public bool IsOptional { get; init; }
+
+ ///
+ /// The used for the ref structs C# feature.
+ ///
+ public const string RefStructs = nameof (RefStructs);
+
+ ///
+ /// The used for the required members C# feature.
+ ///
+ public const string RequiredMembers = nameof (RequiredMembers);
+ }
+}
+
+// From: https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IsExternalInit.cs
+namespace System.Runtime.CompilerServices {
+ using System.ComponentModel;
+
+ ///
+ /// Reserved to be used by the compiler for tracking metadata.
+ /// This class should not be used by developers in source code.
+ ///
+ [EditorBrowsable (EditorBrowsableState.Never)]
+ internal static class IsExternalInit {
+ }
+}
+#endif
diff --git a/tools/assembly-preparer/README.md b/tools/assembly-preparer/README.md
new file mode 100644
index 000000000000..696974bf073f
--- /dev/null
+++ b/tools/assembly-preparer/README.md
@@ -0,0 +1,35 @@
+# Assembly preparer
+
+This is a library that will modify assemblies when a project is built for a few reasons:
+
+* Collect required information for a successful build
+* Transform some code patterns so that they can be properly recognized and handled correctly by trimmers.
+* Optimize some code patterns we can easily recognize
+* Precompute some things at build time to be able to make apps smaller and run faster.
+
+Currently it can:
+
+* PreserveCodeBlockHandler: in some cases user assemblies might contain code
+ created by the generator that's not trimmer safe; this handler will inject
+ code to ensure that trimmers don't trim way some things that shouldn't be
+ trimmed away.
+
+## Design principles
+
+* Easy to test (there's a unit test project, VSCode can run & debug its tests)
+* Can be called from an MSBuild task (which means it currently needs to target `netstandard2.0`).
+* Good error handling/reporting.
+* We have two main scenarios:
+ * Debug loop, where we shouldn't do more than absolutely necessary to make
+ debug builds as fast as possible. In particular, we'll only do whatever
+ is necessary for a correct build, and if possible, no assembly
+ modification, only information gathering.
+ * Release builds, where we want to optimize as much as possible.
+* To ease integration with existing custom linker steps, it's provides a
+ very simplified API of ILLink's custom linker step API - much of it is
+ stubbed out until it's needed.
+* Until fully complete and both correctness and performance have been
+ validated, it should be possible to use either custom linker steps or the
+ assembly preparer, and as such any code that's used by both will have to
+ keep working in both modes.
+
diff --git a/tools/assembly-preparer/Scaffolding/AnnotationStore.cs b/tools/assembly-preparer/Scaffolding/AnnotationStore.cs
new file mode 100644
index 000000000000..e1fc186bbf11
--- /dev/null
+++ b/tools/assembly-preparer/Scaffolding/AnnotationStore.cs
@@ -0,0 +1,167 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Diagnostics;
+using Mono.Cecil;
+
+namespace Mono.Linker;
+
+public class AnnotationStore {
+ Dictionary assemblyActions = new Dictionary ();
+ Dictionary> overrides = new Dictionary> ();
+ public AssemblyAction GetAction (AssemblyDefinition assembly)
+ {
+ if (assemblyActions.TryGetValue (assembly, out var action))
+ return action;
+ throw new InvalidOperationException ($"Assembly {assembly.Name} not found in the annotation store");
+ }
+
+ public void SetAction (AssemblyDefinition assembly, AssemblyAction action)
+ {
+ assemblyActions [assembly] = action;
+ }
+
+ public IEnumerable? GetOverrides (MethodDefinition method)
+ {
+ if (overrides.TryGetValue (method, out var list))
+ return list;
+ return null;
+ }
+
+ // Scan all types in the given assemblies and build the overrides map.
+ // For each method that overrides a base virtual method (or explicitly implements an interface method),
+ // record an OverrideInformation entry keyed by the base method.
+ public void CollectOverrides (IEnumerable assemblies, LinkContext context)
+ {
+ foreach (var assembly in assemblies) {
+ foreach (var module in assembly.Modules) {
+ foreach (var type in module.GetTypes ()) {
+ CollectOverridesForType (type, context);
+ }
+ }
+ }
+ }
+
+ void CollectOverridesForType (TypeDefinition type, LinkContext context)
+ {
+ if (!type.HasMethods)
+ return;
+
+ foreach (var method in type.Methods) {
+ // Handle explicit overrides (.override directive in IL / method.Overrides in Cecil)
+ if (method.HasOverrides) {
+ foreach (var overriddenRef in method.Overrides) {
+ var baseMethod = TryResolve (overriddenRef);
+ if (baseMethod is not null)
+ AddOverride (baseMethod, method);
+ }
+ }
+
+ // Handle implicit virtual method overrides via the type hierarchy
+ if (method.IsVirtual && !method.IsNewSlot) {
+ var baseMethod = GetBaseMethodInTypeHierarchy (type, method, context);
+ if (baseMethod is not null)
+ AddOverride (baseMethod, method);
+ }
+ }
+ }
+
+ void AddOverride (MethodDefinition baseMethod, MethodDefinition overridingMethod)
+ {
+ if (!overrides.TryGetValue (baseMethod, out var list)) {
+ list = new List ();
+ overrides [baseMethod] = list;
+ }
+ list.Add (new OverrideInformation (overridingMethod));
+ }
+
+ static MethodDefinition? GetBaseMethodInTypeHierarchy (TypeDefinition type, MethodDefinition method, LinkContext context)
+ {
+ var baseTypeRef = type.BaseType;
+ while (baseTypeRef is not null) {
+ TypeDefinition? baseType;
+ try {
+ baseType = context.Resolve (baseTypeRef);
+ } catch {
+ break;
+ }
+
+ if (baseType.HasMethods) {
+ foreach (var candidate in baseType.Methods) {
+ if (candidate.IsVirtual && MethodMatch (candidate, method))
+ return candidate;
+ }
+ }
+
+ baseTypeRef = baseType.BaseType;
+ }
+ return null;
+ }
+
+ static bool MethodMatch (MethodDefinition candidate, MethodDefinition method)
+ {
+ if (candidate.Name != method.Name)
+ return false;
+ if (candidate.HasGenericParameters != method.HasGenericParameters)
+ return false;
+ if (candidate.GenericParameters.Count != method.GenericParameters.Count)
+ return false;
+ if (candidate.ReturnType.FullName != method.ReturnType.FullName)
+ return false;
+ if (candidate.HasParameters != method.HasParameters)
+ return false;
+ if (!candidate.HasParameters)
+ return true;
+ if (candidate.Parameters.Count != method.Parameters.Count)
+ return false;
+ for (int i = 0; i < candidate.Parameters.Count; i++) {
+ if (candidate.Parameters [i].ParameterType.FullName != method.Parameters [i].ParameterType.FullName)
+ return false;
+ }
+ return true;
+ }
+
+ static MethodDefinition? TryResolve (MethodReference methodRef)
+ {
+ try {
+ return methodRef.Resolve ();
+ } catch {
+ // FIXME: figure out a way that doesn't require throwing and catching exceptions.
+ return null;
+ }
+ }
+
+ Dictionary