Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,20 @@ Removing the dynamic registrar requires a static registrar (`Registrar=static` o
`Registrar=managed-static`) and trimming, so setting this property has no effect (and the
build warns) when those conditions aren't met.

## TrimExportAttributes

Controls whether `Foundation.ExportAttribute`, `Foundation.ActionAttribute`,
and `Foundation.OutletAttribute` instances are removed during trimming.

If this property is not specified, the build automatically removes these
attributes when assembly preparation and post-processing are enabled, the
trimmable static registrar is selected, dynamic registration is not required,
and no runtime fallback needs the attributes.

Set this property to `false` to preserve the attributes. Set it to `true` to
require their removal; the build will fail if it detects that the attributes
are needed at runtime.

## EmbedOnDemandResources

Controls where on-demand resource asset packs are placed when packaging an app
Expand Down
37 changes: 35 additions & 2 deletions dotnet/targets/Xamarin.Shared.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@
_PrepareAssemblies;
_SetDynamicRegistrationSupportedFeature;
_SetSmartEnumConversionsRootDescriptor;
_SetTrimExportAttributesFeature;
_ComputeFrameworkFilesToPublish;
_ComputeDynamicLibrariesToPublish;
ComputeFilesToPublish;
Expand Down Expand Up @@ -575,7 +576,7 @@
<!--
Enables the 'ObjCRuntime.Runtime.DynamicRegistrationSupported' trimmer feature switch so that ILLink
hardcodes Runtime.DynamicRegistrationSupported. When PrepareAssemblies is enabled, the assembly-preparer's
RegistrarRemovalTrackingStep computes whether the dynamic registrar is needed (instead of rewriting the
DetectApiUsageStep computes whether the dynamic registrar is needed (instead of rewriting the
platform assembly) and writes the value - as the 'DynamicRegistrationSupported' MSBuild output property -
to $(_AssemblyPreparerMSBuildOutputFile). This target runs after _PrepareAssemblies and before the trimmer.
It's not incremental-gated, so even if _PrepareAssemblies was skipped (because its outputs were up-to-date),
Expand Down Expand Up @@ -632,6 +633,30 @@
</ItemGroup>
</Target>

<!--
Enables ExportAttribute removal after the assembly-preparer has run the managed registrar and
verified that no runtime path still needs Export metadata. The resolved value is persisted in the
assembly-preparer output file so incremental builds make the same decision.
-->
<Target Name="_SetTrimExportAttributesFeature" Condition="'$(PrepareAssemblies)' == 'true'">
<ReadLinesFromFile
File="$(_AssemblyPreparerMSBuildOutputFile)"
Condition="Exists('$(_AssemblyPreparerMSBuildOutputFile)')">
<Output TaskParameter="Lines" ItemName="_AssemblyPreparerTrimExportAttributesProperty" />
</ReadLinesFromFile>
<ItemGroup>
<_TrimExportAttributesLine Include="@(_AssemblyPreparerTrimExportAttributesProperty)" Condition="$([System.String]::Copy('%(Identity)').StartsWith('TrimExportAttributes='))" />
</ItemGroup>
<PropertyGroup>
<_TrimExportAttributesLineText>@(_TrimExportAttributesLine)</_TrimExportAttributesLineText>
<_TrimExportAttributes>false</_TrimExportAttributes>
<_TrimExportAttributes Condition="'$(_TrimExportAttributesLineText)' != ''">$(_TrimExportAttributesLineText.Replace('TrimExportAttributes=', ''))</_TrimExportAttributes>
</PropertyGroup>
<ItemGroup Condition="'$(_TrimExportAttributes)' == 'true'">
<RuntimeHostConfigurationOption Include="ObjCRuntime.TrimExportAttributes" Value="true" Trim="true" />
</ItemGroup>
</Target>

<Target Name="_ComputeLinkerArguments" DependsOnTargets="$(_ComputeLinkerArgumentsDependsOn)" />

<Target Name="_ComputeLinkerInputs">
Expand All @@ -644,8 +669,16 @@
/>

<Warning Text="All assemblies must be processed by the linker when using NativeAOT. Please don't set neither the '$(_LinkModeProperty)' nor the 'TrimMode' property, so that the build can default to linking all assemblies." Condition="'$(_UseNativeAot)' == 'true' And '$(_LinkMode)' != 'Full'" />
<Error Text="The 'TrimExportAttributes' property must be either 'true', 'false', or unset." Condition="'$(TrimExportAttributes)' != '' And '$(TrimExportAttributes)' != 'true' And '$(TrimExportAttributes)' != 'false'" />
<Error
Text="Export attributes can only be removed when PrepareAssemblies=true, PostProcessAssemblies=true, and Registrar=trimmable-static."
Condition="'$(TrimExportAttributes)' == 'true' And ('$(PrepareAssemblies)' != 'true' Or '$(PostProcessAssemblies)' != 'true' Or '$(Registrar)' != 'trimmable-static')"
/>

<PropertyGroup>
<_TrimExportAttributes>false</_TrimExportAttributes>
<_TrimExportAttributes Condition="'$(PrepareAssemblies)' == 'true' And '$(PostProcessAssemblies)' == 'true' And '$(Registrar)' == 'trimmable-static'">$(TrimExportAttributes)</_TrimExportAttributes>

<_UseDynamicDependenciesInsteadOfMarking Condition="'$(_UseDynamicDependenciesInsteadOfMarking)' == ''">true</_UseDynamicDependenciesInsteadOfMarking>
<_UseDynamicDependenciesForProtocolPreservation Condition="'$(_UseDynamicDependenciesForProtocolPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking)</_UseDynamicDependenciesForProtocolPreservation>
<_UseDynamicDependenciesForSmartEnumPreservation Condition="'$(_UseDynamicDependenciesForSmartEnumPreservation)' == ''">$(_UseDynamicDependenciesInsteadOfMarking)</_UseDynamicDependenciesForSmartEnumPreservation>
Expand Down Expand Up @@ -923,7 +956,7 @@
<_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)' != ''" />
<!-- The final decision to remove/keep the dynamic registrar must be done before the linking step -->
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.RegistrarRemovalTrackingStep" Condition="'$(PrepareAssemblies)' != 'true'" />
<_TrimmerCustomSteps Include="$(_AdditionalTaskAssembly)" BeforeStep="MarkStep" Type="MonoTouch.Tuner.DetectApiUsageStep" Condition="'$(PrepareAssemblies)' != 'true'" />
<!-- TODO: these steps should probably run after mark. -->
<_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')" />
Expand Down
29 changes: 25 additions & 4 deletions msbuild/Xamarin.MacDev.Tasks/Tasks/PrepareAssemblies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ public class PrepareAssemblies : XamarinTask {

public bool PostProcessing { get; set; }

// The pre-trim (untrimmed) assemblies (the trimmer's input), used during post-processing to read
// the [ProtocolMember] attributes the trimmer removed from the post-trim assemblies.
public string? TrimExportAttributes { get; set; }

// The original assemblies from before preparation and trimming, used during post-processing to read
// selected registrar attributes removed during trimming.
public ITaskItem [] PreTrimAssemblies { get; set; } = [];

// When set (to ILC's output object file), the defined symbols in this file are used to determine
Expand Down Expand Up @@ -71,11 +73,24 @@ public override bool Execute ()
{
// Capture Console usage and show an error if anything uses Console.[Error.]Write*
using var consoleToLog = ConsoleToTaskWriter.EnsureNoConsoleUsage (Log);
var success = false;
var msbuildOutputFile = "";

try {
bool? trimExportAttributes = null;
if (!string.IsNullOrEmpty (TrimExportAttributes)) {
if (!bool.TryParse (TrimExportAttributes, out var parsedTrimExportAttributes)) {
Log.LogError ($"Unable to parse the TrimExportAttributes value: {TrimExportAttributes}");
return false;
}
trimExportAttributes = parsedTrimExportAttributes;
}

var infos = InputAssemblies.Select (GetAssemblyInfo).ToArray ();
using var preparer = new AssemblyPreparer (this, infos, OptionsFile?.ItemSpec ?? "");
msbuildOutputFile = PostProcessing ? preparer.Configuration.MSBuildPostProcessOutputFile : preparer.Configuration.MSBuildOutputFile;
preparer.MakeReproPath = MakeReproPath;
preparer.TrimExportAttributes = trimExportAttributes;
preparer.PreTrimAssemblies.AddRange (PreTrimAssemblies.Select (v => v.ItemSpec));
bool rv;
List<ProductException> exceptions;
Expand Down Expand Up @@ -121,8 +136,10 @@ public override bool Execute ()

outputAssemblies.AddRange (preparer.AddedAssemblies.Select (v => {
var rv = new TaskItem (v.Path);
var relativePath = preparer.Configuration.AssemblyPublishDir + Path.GetFileName (v.Path);
rv.SetMetadata ("PostprocessAssembly", "true");
rv.SetMetadata ("RelativePath", preparer.Configuration.AssemblyPublishDir + Path.GetFileName (v.Path));
rv.SetMetadata ("OriginalRelativePath", relativePath);
rv.SetMetadata ("RelativePath", relativePath);
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) {
Expand All @@ -141,10 +158,14 @@ public override bool Execute ()
OutputAssemblies = outputAssemblies.ToArray ();
if (!rv && !Log.HasLoggedErrors)
Log.LogError (MSBStrings.E0192);
return rv && !Log.HasLoggedErrors;
success = rv && !Log.HasLoggedErrors;
return success;
} catch (Exception e) {
((IToolLog) this).LogException (e);
return false;
} finally {
if (!success && !string.IsNullOrEmpty (msbuildOutputFile))
File.Delete (msbuildOutputFile);
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions msbuild/Xamarin.Shared/Xamarin.Shared.targets
Original file line number Diff line number Diff line change
Expand Up @@ -3690,13 +3690,20 @@ Copyright (C) 2018 Microsoft. All rights reserved.
</ItemGroup>
<PropertyGroup>
<PreparedAssembliesDirectory>$([MSBuild]::EnsureTrailingSlash('$(DeviceSpecificIntermediateOutputPath)prepared-assemblies'))</PreparedAssembliesDirectory>
<_AssemblyPreparerConfigurationStamp>$(DeviceSpecificIntermediateOutputPath)assembly-preparer-configuration.txt</_AssemblyPreparerConfigurationStamp>
</PropertyGroup>
<WriteLinesToFile
File="$(_AssemblyPreparerConfigurationStamp)"
Lines="DynamicRegistrationSupported=$(DynamicRegistrationSupported);LinkMode=$(_LinkMode);Optimize=$(_BundlerOptimize);PublishTrimmed=$(PublishTrimmed);TrimMode=$(TrimMode);TrimExportAttributes=$(_TrimExportAttributes);@(_AssembliesToPrepare -> 'Assembly=%(Identity)|IsTrimmable=%(IsTrimmable)|TrimMode=%(TrimMode)')"
Comment thread
rolfbjarne marked this conversation as resolved.
Overwrite="true"
WriteOnlyWhenDifferent="true"
/>
</Target>

<Target
Name="_PrepareAssemblies"
Condition="'$(PrepareAssemblies)' == 'true'"
Inputs="@(_AssembliesToPrepare)"
Inputs="@(_AssembliesToPrepare);$(_AssemblyPreparerConfigurationStamp)"
Outputs="@(_AssembliesToPrepare->'$(PreparedAssembliesDirectory)%(Filename)%(Extension)');$(_AssemblyPreparerMSBuildOutputFile)"
DependsOnTargets="_PrepareAssembliesForPreparation;_ComputeAssembliesToPostprocessOnPublish;SelectRegistrar"
>
Expand All @@ -3706,6 +3713,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
OptionsFile="$(_CustomLinkerOptionsFile)"
OutputDirectory="$(PreparedAssembliesDirectory)"
TargetFrameworkMoniker="$(_ComputedTargetFrameworkMoniker)"
TrimExportAttributes="$(_TrimExportAttributes)"
>
<Output TaskParameter="OutputAssemblies" ItemName="_PreparedAssemblies" />
</PrepareAssemblies>
Expand All @@ -3714,6 +3722,7 @@ Copyright (C) 2018 Microsoft. All rights reserved.
<ResolvedFileToPublish Remove="@(_AssembliesToPrepare)" />
<ResolvedFileToPublish Include="@(_PreparedAssemblies)" />
<FileWrites Include="@(_PreparedAssemblies)" />
<FileWrites Include="$(_AssemblyPreparerConfigurationStamp)" />
<FileWrites Include="$(_UnmanagedCallersOnlyMapPath)" />
<FileWrites Include="$(_AssemblyPreparerMSBuildOutputFile)" Condition="Exists('$(_AssemblyPreparerMSBuildOutputFile)')" />
</ItemGroup>
Expand Down Expand Up @@ -3754,8 +3763,9 @@ Copyright (C) 2018 Microsoft. All rights reserved.
OptionsFile="$(_CustomLinkerOptionsFile)"
OutputDirectory="$(_PostprocessedAssembliesDirectory)"
Postprocessing="true"
PreTrimAssemblies="@(ManagedAssemblyToLink)"
PreTrimAssemblies="@(_AssembliesToPrepare)"
TargetFrameworkMoniker="$(_ComputedTargetFrameworkMoniker)"
TrimExportAttributes="$(_TrimExportAttributes)"
>
<Output TaskParameter="OutputAssemblies" ItemName="_PostProcessedAssemblies" />
</PrepareAssemblies>
Expand Down
9 changes: 9 additions & 0 deletions src/ILLink.LinkAttributes.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
<type fullname="Foundation.FieldAttribute">
<attribute internal="RemoveAttributeInstances" />
</type>
<type fullname="Foundation.ExportAttribute" feature="ObjCRuntime.TrimExportAttributes" featurevalue="true">
<attribute internal="RemoveAttributeInstances" />
</type>
<type fullname="Foundation.ActionAttribute" feature="ObjCRuntime.TrimExportAttributes" featurevalue="true">
<attribute internal="RemoveAttributeInstances" />
</type>
<type fullname="Foundation.OutletAttribute" feature="ObjCRuntime.TrimExportAttributes" featurevalue="true">
<attribute internal="RemoveAttributeInstances" />
</type>
<type fullname="Foundation.NotImplementedAttribute">
<attribute internal="RemoveAttributeInstances" />
</type>
Expand Down
130 changes: 130 additions & 0 deletions tests/assembly-preparer/ComputeExportAttributeRemovalStepTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using MonoTouch.Tuner;

using Xamarin.Linker;

namespace AssemblyPreparerTests;

public class ComputeExportAttributeRemovalStepTests : BaseClass {
[TestCase (null, true)]
[TestCase (false, true)]
[TestCase (true, false)]
public void NonTrimmableRegistrar (bool? trimExportAttributes, bool expectedSuccess)
{
using var preparer = CreatePreparer (ApplePlatform.iOS, false, preparer => {
preparer.Registrar = RegistrarMode.ManagedStatic;
preparer.TrimExportAttributes = trimExportAttributes;
}, "public class C {}", out _);

var success = preparer.Prepare (out var exceptions);

Assert.That (success, Is.EqualTo (expectedSuccess), "Success");
if (expectedSuccess) {
Assert.That (exceptions, Is.Empty, "Exceptions");
Assert.That (preparer.TrimExportAttributes, Is.False, "Removal");
} else {
Assert.That (exceptions, Has.Count.EqualTo (1), "Exceptions");
Assert.That (exceptions [0].Message, Is.EqualTo ("Export attributes can only be trimmed with the trimmable static registrar."), "Error message");
}
}

[TestCase (false, false, false)]
[TestCase (null, false, true)]
[TestCase (true, false, true)]
[TestCase (null, true, false)]
public void DynamicRegistration (bool? trimExportAttributes, bool dynamicRegistrationSupported, bool expectedRemoval)
{
using var preparer = CreatePreparer (ApplePlatform.iOS, false, preparer => {
preparer.Registrar = RegistrarMode.TrimmableStatic;
preparer.TrimExportAttributes = trimExportAttributes;
preparer.Optimizations.RemoveDynamicRegistrar = !dynamicRegistrationSupported;
preparer.Optimizations.OptimizeBlockLiteralSetupBlock = true;
preparer.Optimizations.StaticBlockToDelegateLookup = true;
}, "public class C {}", out _);

var context = preparer.Configuration.DerivedLinkContext;
new LoadAssembliesStep ().Process (context);
new ComputeExportAttributeRemovalStep ().Process (context);

Assert.That (preparer.Configuration.Application.TrimExportAttributes, Is.EqualTo (expectedRemoval), "Removal");
}

[TestCase (true, true, true)]
[TestCase (false, true, false)]
[TestCase (true, false, false)]
public void RequiredOptimizations (bool optimizeBlockLiteralSetupBlock, bool staticBlockToDelegateLookup, bool expectedRemoval)
{
using var preparer = CreatePreparer (ApplePlatform.iOS, false, preparer => {
preparer.Registrar = RegistrarMode.TrimmableStatic;
preparer.TrimExportAttributes = true;
preparer.Optimizations.RemoveDynamicRegistrar = true;
preparer.Optimizations.OptimizeBlockLiteralSetupBlock = optimizeBlockLiteralSetupBlock;
preparer.Optimizations.StaticBlockToDelegateLookup = staticBlockToDelegateLookup;
}, "public class C {}", out _);

var context = preparer.Configuration.DerivedLinkContext;
new LoadAssembliesStep ().Process (context);
new ComputeExportAttributeRemovalStep ().Process (context);

Assert.That (preparer.Configuration.Application.TrimExportAttributes, Is.EqualTo (expectedRemoval), "Removal");
}

[Test]
public void ExplicitBlocker ()
{
using var preparer = CreatePreparer (ApplePlatform.iOS, false, preparer => {
preparer.Registrar = RegistrarMode.TrimmableStatic;
preparer.TrimExportAttributes = true;
preparer.Optimizations.RemoveDynamicRegistrar = true;
preparer.Optimizations.OptimizeBlockLiteralSetupBlock = true;
preparer.Optimizations.StaticBlockToDelegateLookup = true;
}, "public class C {}", out _);

var context = preparer.Configuration.DerivedLinkContext;
new LoadAssembliesStep ().Process (context);
preparer.Configuration.Application.TrimExportAttributesBlockers.Add (ExportAttributeRemovalBlocker.RuntimeGetBlockWrapperCreatorRequired);
new ComputeExportAttributeRemovalStep ().Process (context);

Assert.That (preparer.Configuration.Application.TrimExportAttributes, Is.False, "Removal");
}

[Test]
public void NSXpcInterfaceMethodInfoOverload ()
{
var code = """
using System.Reflection;
using Foundation;
using ObjCRuntime;

public class C {
public void GetAllowedClasses (NSXpcInterface value, MethodInfo method)
{
value.GetAllowedClasses (method, 0, false);
}

public void SetAllowedClasses (NSXpcInterface value, MethodInfo method, NSSet<Class> classes)
{
value.SetAllowedClasses (method, classes, 0, false);
}
}
""";

using var preparer = CreatePreparer (ApplePlatform.iOS, false, preparer => {
preparer.Registrar = RegistrarMode.TrimmableStatic;
preparer.TrimExportAttributes = true;
preparer.Optimizations.RemoveDynamicRegistrar = true;
preparer.Optimizations.OptimizeBlockLiteralSetupBlock = true;
preparer.Optimizations.StaticBlockToDelegateLookup = true;
}, code, out _);

var context = preparer.Configuration.DerivedLinkContext;
new LoadAssembliesStep ().Process (context);
new DetectApiUsageStep ().Process (context);
new ComputeExportAttributeRemovalStep ().Process (context);

Assert.That (preparer.Configuration.Application.TrimExportAttributes, Is.False, "Removal");
Assert.That (preparer.Configuration.Application.TrimExportAttributesBlockers.Single (), Is.EqualTo (ExportAttributeRemovalBlocker.NSXpcInterfaceMethodInfoOverloadUsed), "Blocker");
}
}
4 changes: 2 additions & 2 deletions tests/common/DotNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ public static ExecutionResult AssertRun (string project, Dictionary<string, stri
return Execute ("run", project, properties, true, timeout: timeout, extraArguments: extraArguments);
}

public static ExecutionResult AssertBuildFailure (string project, Dictionary<string, string>? properties = null)
public static ExecutionResult AssertBuildFailure (string project, Dictionary<string, string>? properties = null, string? target = null)
{
var rv = Execute ("build", project, properties, false);
var rv = Execute ("build", project, properties, false, target: target);
Assert.That (rv.ExitCode, Is.Not.EqualTo (0), "Unexpected success");
return rv;
}
Expand Down
Loading
Loading