diff --git a/build/Uno.UI.Build.csproj b/build/Uno.UI.Build.csproj index d1efedfd1239..52af782a4be3 100644 --- a/build/Uno.UI.Build.csproj +++ b/build/Uno.UI.Build.csproj @@ -166,10 +166,9 @@ + <_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\**\*.cs" /> <_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\Uno.UI.Tasks.csproj" /> - <_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\Assets\RetargetAssets.cs" /> <_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\Content\Uno.UI.Tasks.targets" /> - <_Sha1Replace Include="..\src\SourceGenerators\Uno.UI.Tasks\ResourcesGenerator\ResourcesGenerationTask.cs" /> + + diff --git a/build/run-net6-template-tests.ps1 b/build/run-net6-template-tests.ps1 index 87ccefb3a9a6..72ebca69a94a 100644 --- a/build/run-net6-template-tests.ps1 +++ b/build/run-net6-template-tests.ps1 @@ -81,3 +81,8 @@ for($i = 0; $i -lt $dotnetBuildNet6Configurations.Length; $i++) } popd + +# XAML Trimming build smoke test +dotnet new unoapp-net6 -n MyAppXamlTrim +& dotnet build -c Debug MyAppXamlTrim\MyAppXamlTrim.Wasm\MyAppXamlTrim.Wasm.csproj /p:UnoXamlResourcesTrimming=true +Assert-ExitCodeIsZero diff --git a/doc/articles/features/resources-trimming.md b/doc/articles/features/resources-trimming.md new file mode 100644 index 000000000000..0e2714cbe04b --- /dev/null +++ b/doc/articles/features/resources-trimming.md @@ -0,0 +1,46 @@ +# XAML Resource Trimming + +XAML Resource and Binding trimming is an optional feature used to reduce the size of the final payload of an Uno Platform application. + +The trimming phase happens after the compilation phase and tries to determine which UI controls are not used explicitly, and removes the associated XAML styles. The XAML styles are found through the value specified in the `TargetType` attribute. + +As of Uno 3.9, XAML Resources Trimming is only available for WebAssembly projects. + +## Using XAML Resources trimming for applications + +In order for an application to enable resources trimming, the following needs to be added in the project file: + +```xml + + true + +``` + +## Enabling XAML Resources trimming for libraries +For libraries to be eligible for resources trimming, the `UnoXamlResourcesTrimming` tag must also be added. + +## Troubleshooting + +### Aggressive trimming +The XAML trimming phase may remove controls for which the use cannot be detected statically. + +For instance, if your application relies on the `XamlReader` class, trimmed controls will not be available and will fail to load. + +If XAML trimming is still needed, the [IL Linker configuration](using-il-linker-webassembly.md) can be adjusted to keep controls individually or by namespace. + +### Size is not reduced even if enabled +The IL Linker tool is used to implement this feature, and can be [controlled with its configuration file](using-il-linker-webassembly.md). + +For instance, if the linker configuration file contains ``, none of the UI Controls will be excluded, and the final app size will remain close as without trimming. + +Note that for libraries, Uno 3.9 or later must be used to build the library, as additional metadata needs to be added at compile time. 3.8 and earlier libraries can be used in any case, but won't be eligible for trimming and may degrade the trimming phase effect. + + +## Size reduction statistics + +As of Uno 3.9, for a `dotnet new unoapp` created app: + +| | w/o XAML Trimming | w/ XAML Trimming | +| -------------------- | ----------------- | --------------- | +| Total IL Payload | 12.9MB | 9.12 MB | +| dotnet.wasm | 53MB | 28.9MB MB | diff --git a/doc/articles/features/using-il-linker-webassembly.md b/doc/articles/features/using-il-linker-webassembly.md index efd321a4ebed..e42875a43264 100644 --- a/doc/articles/features/using-il-linker-webassembly.md +++ b/doc/articles/features/using-il-linker-webassembly.md @@ -22,7 +22,7 @@ When parts of an application fail to work, you can: As a troubleshooting step to determine if the linker is causing your code to break, you can also disable the linker completely by adding the following to your `csproj` file: ```xml - true + false ``` Note that disabling the linker is not recommended as it can force the compiler to generate very large WebAssembly modules AOT process, and go over the browser's size limits. diff --git a/doc/articles/toc.yml b/doc/articles/toc.yml index dd15d8a006b3..62d13a420d0e 100644 --- a/doc/articles/toc.yml +++ b/doc/articles/toc.yml @@ -313,6 +313,8 @@ href: uno-fluent-assets.md - name: Lottie animations href: features/Lottie.md + - name: Working with XAML Trimming + href: features/resources-trimming.md - name: Working with cookies href: features/working-with-cookies.md - name: Using pointer cursors @@ -464,3 +466,5 @@ href: uno-development\troubleshooting-memory-issues.md - name: Troubleshooting Source Generation href: uno-development\troubleshooting-source-generation.md + - name: The XAML Trimming phase + href: uno-development\Uno-UI-XAML-ResourceTrimming.md diff --git a/doc/articles/uno-development/Uno-UI-XAML-ResourceTrimming.md b/doc/articles/uno-development/Uno-UI-XAML-ResourceTrimming.md new file mode 100644 index 000000000000..f7d7cecff054 --- /dev/null +++ b/doc/articles/uno-development/Uno-UI-XAML-ResourceTrimming.md @@ -0,0 +1,28 @@ +# XAML Resource Trimming + +This document provides technical details about the [XAML Resource trimming phase](../features/resources-trimming.md). + +## Technical Details + +In Uno, XAML is generating C# code in order to speed up the creation of the UI. This allows for compile-time optimizations, such as type conversions or `x:Bind` integration. + +The drawback of this approach is that code may be bundled with an app even if it's not used. In common use cases, the IL Linker is able to remove code not referenced, but in the case of XAML resources, this code is conditionally referenced through string cases only. This makes it impossible for the linker to remove that code through out-of-the-box means. + +In order to dermine what is actually used by the application, the Uno Platform tooling runs a sequence of IL Linker passes and substitutions. + +In order to prepare the linking pass: +- The tooling determines the presence of the `UnoXamlResourcesTrimming` msbuild property +- During the source generation, the tooling generates a `LinkerHints` class, which contains a set of properties for all `DependencyObject` inheriting classes. +- The source generation creates XAML Resources and Bindable Metadata code that conditionally uses those classes behind `LinkerHints` properties. +- The tooling also embeds an ILLinker substitution file allowing the linker to unconditionally remove the code that conditionally references those properties. For instance, for `LinkerHints.Is_Windows_UI_Xaml_Controls_Border_Available`, any block of `if (LinkerHints.Is_Windows_UI_Xaml_Controls_Border_Available)` will be removed when the `--feature Is_Windows_UI_Xaml_Controls_Border_Available false` parameter is provided to the linker. + +Then the multiple passes of IL Linker are done: +- The first pass runs the IL Linker with all XAML resources and Binding Metadata disabled by setting all `LinkerHints` properties to false. This removes all code directly associated to those Bindable Metadata and XAML Resources. This has the effect of only keeping framework code which is directly referenced from user code. +- The tooling then reads the result of the linker to determine which types in the `LinkerHints` are still available in the assemblies. +- The subsequent passes run the IL Linker with `LinkerHints` enabled only for types detected to be used during the first pass. This will enable types indirectly referenced by XAML Resources (e.g. a ScrollBar inside a ScrollViewer style) to be kept by the linker. +- The tooling then reads again the result of the linker to determine which types in the `LinkerHints` are still available in the assemblies. +- The tooling re-runs this last pass until the available types list stops changing. + +The resulting `LinkerHints` types are now passed as features to the final linker pass (the one bundled with the .NET tooling) to generate the final binary, containing only the used types. + +As of Uno 3.9, the Uno.UI WebAssembly assembly is 7.5MB, trimmed down to 3.1MB for a `dotnet new unoapp` template. diff --git a/src/SolutionTemplate/UnoSolutionTemplate.net6/Wasm/UnoQuickStart.Wasm.csproj b/src/SolutionTemplate/UnoSolutionTemplate.net6/Wasm/UnoQuickStart.Wasm.csproj index 3fd03b617c76..ec6e742aaa49 100644 --- a/src/SolutionTemplate/UnoSolutionTemplate.net6/Wasm/UnoQuickStart.Wasm.csproj +++ b/src/SolutionTemplate/UnoSolutionTemplate.net6/Wasm/UnoQuickStart.Wasm.csproj @@ -48,8 +48,8 @@ - - + + diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/BindableTypeProvidersGenerationTask.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/BindableTypeProvidersGenerationTask.cs index 63076acf30b0..30e08ba45737 100644 --- a/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/BindableTypeProvidersGenerationTask.cs +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/BindableTypeProvidersGenerationTask.cs @@ -19,6 +19,7 @@ using Microsoft.CodeAnalysis.CSharp; using System.Reflection.Metadata.Ecma335; using Uno.UI.SourceGenerators.Helpers; +using System.Xml; #if NETFRAMEWORK using Uno.SourceGeneration; @@ -49,8 +50,10 @@ class Generator private string? _defaultNamespace; private Dictionary _typeMap = new Dictionary(); + private Dictionary members)> _substitutions = new Dictionary members)>(); private INamedTypeSymbol[]? _bindableAttributeSymbol; private ITypeSymbol? _dependencyPropertySymbol; + private INamedTypeSymbol? _dependencyObjectSymbol; private INamedTypeSymbol? _objectSymbol; private INamedTypeSymbol? _javaObjectSymbol; private INamedTypeSymbol? _nsObjectSymbol; @@ -59,6 +62,12 @@ class Generator private IModuleSymbol? _currentModule; private IReadOnlyDictionary? _namedSymbolsLookup; private INamedTypeSymbol? _stringSymbol; + private string? _projectFullPath; + private string? _projectDirectory; + private string? _baseIntermediateOutputPath; + private string? _intermediatePath; + private string? _assemblyName; + private bool _xamlResourcesTrimming; public string[]? AnalyzerSuppressions { get; set; } @@ -68,18 +77,35 @@ internal void Generate(GeneratorExecutionContext context) { var validPlatform = PlatformHelper.IsValidPlatform(context); var isDesignTime = DesignTimeHelper.IsDesignTime(context); - var isApplication = IsApplication(context); + var isApplication = Helpers.IsApplication(context); if (validPlatform && !isDesignTime) { if (isApplication) { + _projectFullPath = context.GetMSBuildPropertyValue("MSBuildProjectFullPath"); + _projectDirectory = Path.GetDirectoryName(_projectFullPath) + ?? throw new InvalidOperationException($"MSBuild property MSBuildProjectFullPath value {_projectFullPath} is not valid"); + + if(!bool.TryParse(context.GetMSBuildPropertyValue("UnoXamlResourcesTrimming"), out _xamlResourcesTrimming)) + { + _xamlResourcesTrimming = false; + } + + _baseIntermediateOutputPath = context.GetMSBuildPropertyValue("BaseIntermediateOutputPath"); + _intermediatePath = Path.Combine( + _projectDirectory, + _baseIntermediateOutputPath + ); + _assemblyName = context.GetMSBuildPropertyValue("AssemblyName"); + _defaultNamespace = context.GetMSBuildPropertyValue("RootNamespace"); _namedSymbolsLookup = context.Compilation.GetSymbolNameLookup(); _bindableAttributeSymbol = FindBindableAttributes(context); _dependencyPropertySymbol = context.Compilation.GetTypeByMetadataName(XamlConstants.Types.DependencyProperty); + _dependencyObjectSymbol = context.Compilation.GetTypeByMetadataName(XamlConstants.Types.DependencyObject); _objectSymbol = context.Compilation.GetTypeByMetadataName("System.Object"); _javaObjectSymbol = context.Compilation.GetTypeByMetadataName("Java.Lang.Object"); @@ -100,6 +126,8 @@ from module in sym.Modules modules = modules.Concat(context.Compilation.SourceModule); context.AddSource("BindableMetadata", GenerateTypeProviders(modules)); + + GenerateLinkerSubstitutionDefinition(); } else { @@ -119,23 +147,10 @@ from module in sym.Modules this.Log().Error("Failed to generate type providers.", new Exception("Failed to generate type providers." + message, e)); } } + private INamedTypeSymbol[] FindBindableAttributes(GeneratorExecutionContext context) => _namedSymbolsLookup!.TryGetValue("BindableAttribute", out var types) ? types : new INamedTypeSymbol[0]; - private bool IsApplication(GeneratorExecutionContext context) - { - var isAndroidApp = context.GetMSBuildPropertyValue("AndroidApplication")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; - var isiOSApp = context.GetMSBuildPropertyValue("ProjectTypeGuidsProperty")?.Equals("{FEACFBD2-3405-455C-9665-78FE426C6842},{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", StringComparison.OrdinalIgnoreCase) ?? false; - var ismacOSApp = context.GetMSBuildPropertyValue("ProjectTypeGuidsProperty")?.Equals("{A3F8F2AB-B479-4A4A-A458-A89E7DC349F1},{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", StringComparison.OrdinalIgnoreCase) ?? false; - var isExe = context.GetMSBuildPropertyValue("OutputType")?.Equals("Exe", StringComparison.OrdinalIgnoreCase) ?? false; - var isUnoHead = context.GetMSBuildPropertyValue("IsUnoHead")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; - - return isAndroidApp - || (isiOSApp && isExe) - || (ismacOSApp && isExe) - || isUnoHead; - } - private string GenerateTypeProviders(IEnumerable modules) { var q = from module in modules @@ -211,10 +226,7 @@ private void GenerateProviderTable(IEnumerable q, IndentedStri ); } - using (writer.BlockInvariant("static BindableMetadataProvider()")) - { - GenerateTypeTable(writer, q); - } + GenerateTypeTable(writer, q); writer.AppendLineInvariant(@"#if DEBUG"); writer.AppendLineInvariant(@"private global::System.Collections.Generic.List _knownMissingTypes = new global::System.Collections.Generic.List();"); @@ -394,6 +406,8 @@ where field.IsStatic writer.AppendLineInvariant("[System.Diagnostics.CodeAnalysis.SuppressMessage(\"Microsoft.Maintainability\", \"CA1505:AvoidUnmaintainableCode\", Justification = \"Must be ignored even if generated code is checked.\")]"); using (writer.BlockInvariant("internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)")) { + RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "Uno.UI.DataBinding.IBindableType Build(Uno.UI.DataBinding.BindableType)"); + writer.AppendLineInvariant( @"var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType({0}, typeof({1}));", flattenedProperties @@ -418,6 +432,8 @@ where field.IsStatic { writer.AppendLineInvariant(@"bindableType.AddActivator(CreateInstance);"); postWriter.AppendLineInvariant($@"private static object CreateInstance() => new {ownerTypeName}();"); + + RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "System.Object CreateInstance()"); } } @@ -431,10 +447,12 @@ where field.IsStatic writer.AppendLineInvariant("bindableType.AddIndexer(GetIndexer, SetIndexer);"); postWriter.AppendLineInvariant($@"private static object GetIndexer(object instance, string name) => (({ownerTypeName})instance)[name];"); + RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "System.Object GetIndexer(System.Object, System.String)"); if (property.SetMethod != null) { postWriter.AppendLineInvariant($@"private static void SetIndexer(object instance, string name, object value) => (({ownerTypeName})instance)[name] = ({propertyTypeName})value;"); + RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, "System.Void SetIndexer(System.Object,System.String,System.Object)"); } else { @@ -477,12 +495,18 @@ where field.IsStatic postWriter.AppendLineInvariant($@"private static object Get{propertyName}(object instance, Windows.UI.Xaml.DependencyPropertyValuePrecedences? precedence) => (({ownerTypeName})instance).{propertyName};"); postWriter.AppendLineInvariant($@"private static void Set{propertyName}(object instance, object value, Windows.UI.Xaml.DependencyPropertyValuePrecedences? precedence) => (({ownerTypeName})instance).{propertyName} = ({propertyTypeName})value;"); } + + RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, $"System.Object Get{propertyName}(System.Object,System.Nullable`1)"); + RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, $"System.Void Set{propertyName}(System.Object,System.Object,System.Nullable`1)"); + } else if (HasPublicGetter(property)) { writer.AppendLineInvariant($@"bindableType.AddProperty(""{propertyName}"", typeof({propertyTypeName}), Get{propertyName});"); postWriter.AppendLineInvariant($@"private static object Get{propertyName}(object instance, Windows.UI.Xaml.DependencyPropertyValuePrecedences? precedence) => (({ownerTypeName})instance).{propertyName};"); + + RegisterHintMethod($"MetadataBuilder_{typeInfo.Index:000}", ownerType, $"System.Object Get{propertyName}(System.Object,System.Nullable`1)"); } } @@ -515,6 +539,17 @@ where field.IsStatic writer.AppendLine(); } + private void RegisterHintMethod(string type, INamedTypeSymbol targetType, string signature) + { + type = _defaultNamespace + "." + type; + + if (!_substitutions.TryGetValue(type, out var hint)) + { + _substitutions[type] = hint = (LinkerHintsHelpers.GetPropertyAvailableName(targetType.ToDisplayString()), new List()); + } + + hint.members.Add(signature); + } private static string ExpandType(INamedTypeSymbol ownerType) { @@ -621,14 +656,86 @@ private string SanitizeTypeName(string name) private void GenerateTypeTable(IndentedStringBuilder writer, IEnumerable types) { + using (writer.BlockInvariant("static BindableMetadataProvider()")) + { + foreach (var type in _typeMap.Where(k => !k.Key.IsGenericType)) + { + writer.AppendLineInvariant($"RegisterBuilder{type.Value.Index:000}();"); + } + } + foreach (var type in _typeMap.Where(k => !k.Key.IsGenericType)) { - writer.AppendLineInvariant( - "_bindableTypeCacheByFullName[\"{0}\"] = CreateMemoized(MetadataBuilder_{1:000}.Build);", - type.Key, - type.Value.Index - ); + using (writer.BlockInvariant($"static void RegisterBuilder{type.Value.Index:000}()")) + { + if (_xamlResourcesTrimming && type.Key.GetAllInterfaces().Any(i => SymbolEqualityComparer.Default.Equals(i, _dependencyObjectSymbol))) + { + var linkerHintsClassName = LinkerHintsHelpers.GetLinkerHintsClassName(_defaultNamespace); + var safeTypeName = LinkerHintsHelpers.GetPropertyAvailableName(type.Key.GetFullMetadataName()); + + writer.AppendLineInvariant($"if(global::{linkerHintsClassName}.{safeTypeName})"); + } + + writer.AppendLineInvariant( + "_bindableTypeCacheByFullName[\"{0}\"] = CreateMemoized(MetadataBuilder_{1:000}.Build);", + type.Key, + type.Value.Index + ); + } + } + } + + private void GenerateLinkerSubstitutionDefinition() + { + if (!_xamlResourcesTrimming) + { + return; } + + // + // + // + // + // + // + // + // + var doc = new XmlDocument(); + + var xmlDeclaration = doc.CreateXmlDeclaration("1.0", "UTF-8", null); + + var root = doc.DocumentElement; + doc.InsertBefore(xmlDeclaration, root); + + var linkerNode = doc.CreateElement(string.Empty, "linker", string.Empty); + doc.AppendChild(linkerNode); + + var assemblyNode = doc.CreateElement(string.Empty, "assembly", string.Empty); + assemblyNode.SetAttribute("fullname", _assemblyName); + linkerNode.AppendChild(assemblyNode); + + + foreach(var substitution in _substitutions) + { + var typeNode = doc.CreateElement(string.Empty, "type", string.Empty); + typeNode.SetAttribute("fullname", substitution.Key); + typeNode.SetAttribute("feature", substitution.Value.type); + typeNode.SetAttribute("featurevalue", "false"); + assemblyNode.AppendChild(typeNode); + + foreach(var method in substitution.Value.members) + { + var methodNode = doc.CreateElement(string.Empty, "method", string.Empty); + methodNode.SetAttribute("signature", method); + methodNode.SetAttribute("body", "remove"); + typeNode.AppendChild(methodNode); + } + } + + var fileName = Path.Combine(_intermediatePath, "Substitutions", "BindableMetadata.Substitutions.xml"); + Directory.CreateDirectory(Path.GetDirectoryName(fileName)); + + doc.Save(fileName); } } diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/DependencyObjectAvailabilityGenerator.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/DependencyObjectAvailabilityGenerator.cs new file mode 100644 index 000000000000..35e6e80c02dc --- /dev/null +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/DependencyObjectAvailabilityGenerator.cs @@ -0,0 +1,226 @@ +#nullable enable + +using Uno.Logging; +using Uno.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.IO; +using System.Xml.Serialization; +using System.Collections; +using System.Diagnostics; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Text; +using Uno.Roslyn; +using Uno.UI.SourceGenerators.XamlGenerator; +using Microsoft.CodeAnalysis.CSharp; +using System.Reflection.Metadata.Ecma335; +using Uno.UI.SourceGenerators.Helpers; +using System.Xml; + +#if NETFRAMEWORK +using Uno.SourceGeneration; +#endif + +namespace Uno.UI.SourceGenerators.BindableTypeProviders +{ +#if NETFRAMEWORK + [GenerateAfter("Uno.ImmutableGenerator")] +#endif + [Generator] + public class DependencyObjectAvailabilityGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + DependenciesInitializer.Init(); + } + + public void Execute(GeneratorExecutionContext context) + { + new Generator().Generate(context); + } + + class Generator + { + private string? _defaultNamespace; + private string? _projectFullPath; + private string? _projectDirectory; + private string? _baseIntermediateOutputPath; + private string? _intermediatePath; + private string? _assemblyName; + private INamedTypeSymbol[]? _bindableAttributeSymbol; + private INamedTypeSymbol? _dependencyObjectSymbol; + private INamedTypeSymbol? _resourceDictionarySymbol; + private IModuleSymbol? _currentModule; + private IReadOnlyDictionary? _namedSymbolsLookup; + private bool _xamlResourcesTrimming; + + public string[]? AnalyzerSuppressions { get; set; } + + internal void Generate(GeneratorExecutionContext context) + { + try + { + var validPlatform = PlatformHelper.IsValidPlatform(context); + var isDesignTime = DesignTimeHelper.IsDesignTime(context); + var isApplication = Helpers.IsApplication(context); + + if (!bool.TryParse(context.GetMSBuildPropertyValue("UnoXamlResourcesTrimming"), out _xamlResourcesTrimming)) + { + _xamlResourcesTrimming = false; + } + + if (validPlatform && _xamlResourcesTrimming) + { + _defaultNamespace = context.GetMSBuildPropertyValue("RootNamespace"); + + _projectFullPath = context.GetMSBuildPropertyValue("MSBuildProjectFullPath"); + _projectDirectory = Path.GetDirectoryName(_projectFullPath) + ?? throw new InvalidOperationException($"MSBuild property MSBuildProjectFullPath value {_projectFullPath} is not valid"); + + _baseIntermediateOutputPath = context.GetMSBuildPropertyValue("BaseIntermediateOutputPath"); + _intermediatePath = Path.Combine( + _projectDirectory, + _baseIntermediateOutputPath + ); + _assemblyName = context.GetMSBuildPropertyValue("AssemblyName"); + _namedSymbolsLookup = context.Compilation.GetSymbolNameLookup(); + + _bindableAttributeSymbol = FindBindableAttributes(context); + _dependencyObjectSymbol = context.Compilation.GetTypeByMetadataName("Windows.UI.Xaml.DependencyObject"); + _resourceDictionarySymbol = context.Compilation.GetTypeByMetadataName("Windows.UI.Xaml.ResourceDictionary"); + _currentModule = context.Compilation.SourceModule; + + AnalyzerSuppressions = new string[0]; + + var modules = from ext in context.Compilation.ExternalReferences + let sym = context.Compilation.GetAssemblyOrModuleSymbol(ext) as IAssemblySymbol + where sym != null + from module in sym.Modules + select module; + + modules = modules.Concat(context.Compilation.SourceModule); + + var bindableTypes = from module in modules + from type in module.GlobalNamespace.GetNamespaceTypes() + where ( + ( + type.GetAllInterfaces().Any(i => SymbolEqualityComparer.Default.Equals(i, _dependencyObjectSymbol)) + ) + ) + select type; + + bindableTypes = bindableTypes.ToArray(); + + context.AddSource("DependencyObjectAvailability", GenerateTypeProviders(bindableTypes)); + + GenerateLinkerSubstitutionDefinition(bindableTypes, isApplication); + } + } + catch (Exception e) + { + string? message = e.Message + e.StackTrace; + + if (e is AggregateException) + { + message = (e as AggregateException)?.InnerExceptions.Select(ex => ex.Message + e.StackTrace).JoinBy("\r\n"); + } + + this.Log().Error("Failed to generate type providers.", new Exception("Failed to generate type providers." + message, e)); + } + } + + private void GenerateLinkerSubstitutionDefinition(IEnumerable bindableTypes, bool isApplication) + { + // + // + // + // + // + // + // + // + var doc = new XmlDocument(); + + var xmlDeclaration = doc.CreateXmlDeclaration("1.0", "UTF-8", null); + + var root = doc.DocumentElement; + doc.InsertBefore(xmlDeclaration, root); + + var linkerNode = doc.CreateElement(string.Empty, "linker", string.Empty); + doc.AppendChild(linkerNode); + + var assemblyNode = doc.CreateElement(string.Empty, "assembly", string.Empty); + assemblyNode.SetAttribute("fullname", _assemblyName); + linkerNode.AppendChild(assemblyNode); + + + // + // Linker hints features from dependency objects + // + var typeNode = doc.CreateElement(string.Empty, "type", string.Empty); + typeNode.SetAttribute("fullname", LinkerHintsHelpers.GetLinkerHintsClassName(_defaultNamespace)); + assemblyNode.AppendChild(typeNode); + + foreach (var type in bindableTypes) + { + var propertyName = LinkerHintsHelpers.GetPropertyAvailableName(type.GetFullMetadataName()); + + var methodNode = doc.CreateElement(string.Empty, "method", string.Empty); + methodNode.SetAttribute("signature", $"System.Boolean get_{propertyName}()"); + methodNode.SetAttribute("body", "stub"); + methodNode.SetAttribute("value", "false"); + methodNode.SetAttribute("feature", propertyName); + methodNode.SetAttribute("featurevalue", "false"); + typeNode.AppendChild(methodNode); + } + + var fileName = Path.Combine(_intermediatePath, "Substitutions", "DependencyObjectHints.Substitutions.xml"); + Directory.CreateDirectory(Path.GetDirectoryName(fileName)); + + doc.Save(fileName); + } + + private INamedTypeSymbol[] FindBindableAttributes(GeneratorExecutionContext context) => + _namedSymbolsLookup!.TryGetValue("BindableAttribute", out var types) ? types : new INamedTypeSymbol[0]; + + private string GenerateTypeProviders(IEnumerable bindableTypes) + { + var writer = new IndentedStringBuilder(); + + writer.AppendLineInvariant("// "); + writer.AppendLineInvariant("// *****************************************************************************"); + writer.AppendLineInvariant("// This file has been generated by Uno.UI (BindableTypeProvidersSourceGenerator)"); + writer.AppendLineInvariant("// *****************************************************************************"); + writer.AppendLineInvariant("// "); + writer.AppendLine(); + writer.AppendLineInvariant("#pragma warning disable 618 // Ignore obsolete members warnings"); + writer.AppendLineInvariant("#pragma warning disable 1591 // Ignore missing XML comment warnings"); + writer.AppendLineInvariant("using System;"); + writer.AppendLineInvariant("using System.Linq;"); + writer.AppendLineInvariant("using System.Diagnostics;"); + + writer.AppendLineInvariant($"// _intermediatePath: {_intermediatePath}"); + writer.AppendLineInvariant($"// _baseIntermediateOutputPath: {_baseIntermediateOutputPath}"); + + using (writer.BlockInvariant("namespace {0}", _defaultNamespace)) + { + using (writer.BlockInvariant("internal class " + LinkerHintsHelpers.GetLinkerHintsClassName())) + { + foreach(var type in bindableTypes) + { + var safeTypeName = LinkerHintsHelpers.GetPropertyAvailableName(type.GetFullMetadataName()); + + writer.AppendLineInvariant($"internal static bool {safeTypeName} => true;"); + } + } + } + + return writer.ToString(); + } + } + } +} diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/Helpers.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/Helpers.cs new file mode 100644 index 000000000000..2be757f3a475 --- /dev/null +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/Helpers.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Uno.Roslyn; + +#if NETFRAMEWORK +using Uno.SourceGeneration; +#endif + +namespace Uno.UI.SourceGenerators.BindableTypeProviders +{ + internal static class Helpers + { + internal static bool IsApplication(GeneratorExecutionContext context) + { + var isAndroidApp = context.GetMSBuildPropertyValue("AndroidApplication")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; + var isiOSApp = context.GetMSBuildPropertyValue("ProjectTypeGuidsProperty")?.Equals("{FEACFBD2-3405-455C-9665-78FE426C6842},{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", StringComparison.OrdinalIgnoreCase) ?? false; + var ismacOSApp = context.GetMSBuildPropertyValue("ProjectTypeGuidsProperty")?.Equals("{A3F8F2AB-B479-4A4A-A458-A89E7DC349F1},{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", StringComparison.OrdinalIgnoreCase) ?? false; + var isExe = context.GetMSBuildPropertyValue("OutputType")?.Equals("Exe", StringComparison.OrdinalIgnoreCase) ?? false; + var isUnoHead = context.GetMSBuildPropertyValue("IsUnoHead")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false; + + return isAndroidApp + || (isiOSApp && isExe) + || (ismacOSApp && isExe) + || isUnoHead; + } + + } +} diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/LinkerHintsHelpers.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/LinkerHintsHelpers.cs new file mode 100644 index 000000000000..9994c926e379 --- /dev/null +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/BindableTypeProviders/LinkerHintsHelpers.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Uno.UI.SourceGenerators.BindableTypeProviders +{ + internal static class LinkerHintsHelpers + { + internal static string GetPropertyAvailableName(string name) + => "Is_" + name + .Replace("`", "_") + .Replace("<", "_") + .Replace(">", "_") + .Replace("+", "_") + .Replace("[", "_") + .Replace("]", "_") + .Replace(".", "_") + .Replace(",", "_") + + "_Available"; + + internal static string GetLinkerHintsClassName(string defaultNamespace) + => $"{defaultNamespace}.__LinkerHints"; + internal static string GetLinkerHintsClassName() + => $"__LinkerHints"; + } +} diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/Content/Uno.UI.SourceGenerators.props b/src/SourceGenerators/Uno.UI.SourceGenerators/Content/Uno.UI.SourceGenerators.props index 7e5da64c4d15..9cb215a7d781 100644 --- a/src/SourceGenerators/Uno.UI.SourceGenerators/Content/Uno.UI.SourceGenerators.props +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/Content/Uno.UI.SourceGenerators.props @@ -60,6 +60,9 @@ + + + diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlCodeGeneration.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlCodeGeneration.cs index b03ac4066b3d..b5be2ba54d45 100644 --- a/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlCodeGeneration.cs +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlCodeGeneration.cs @@ -50,6 +50,7 @@ internal partial class XamlCodeGeneration private readonly string _projectDirectory; private readonly string _projectFullPath; private readonly bool _outputSourceComments = true; + private readonly bool _xamlResourcesTrimming; private readonly RoslynMetadataHelper _metadataHelper; internal const string Title = "XAML Generation Failed"; @@ -187,6 +188,11 @@ public XamlCodeGeneration(GeneratorExecutionContext context) _isLazyVisualStateManagerEnabled = isLazyVisualStateManagerEnabled; } + if (bool.TryParse(context.GetMSBuildPropertyValue("UnoXamlResourcesTrimming"), out var xamlResourcesTrimming)) + { + _xamlResourcesTrimming = xamlResourcesTrimming; + } + _targetPath = Path.Combine( _projectDirectory, context.GetMSBuildPropertyValue("IntermediateOutputPath") @@ -317,7 +323,8 @@ public KeyValuePair[] Generate() skipUserControlsInVisualTree: _skipUserControlsInVisualTree, shouldAnnotateGeneratedXaml: _shouldAnnotateGeneratedXaml, isUnoAssembly: IsUnoAssembly, - isLazyVisualStateManagerEnabled: _isLazyVisualStateManagerEnabled + isLazyVisualStateManagerEnabled: _isLazyVisualStateManagerEnabled, + xamlResourcesTrimming: _xamlResourcesTrimming ) .GenerateFile() ) diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs index b32fb696691a..56ea9232b9ba 100644 --- a/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs @@ -23,6 +23,7 @@ using System.Runtime.CompilerServices; using Uno.UI.Xaml; using Uno.Disposables; +using Uno.UI.SourceGenerators.BindableTypeProviders; namespace Uno.UI.SourceGenerators.XamlGenerator { @@ -85,6 +86,12 @@ internal partial class XamlFileGenerator /// True if VisualStateManager children can be set lazily /// private readonly bool _isLazyVisualStateManagerEnabled; + + /// + /// True if XAML resource trimming is enabled + /// + private readonly bool _xamlResourcesTrimming; + /// /// True if the generator is currently creating child subclasses (for templates, etc) /// @@ -197,7 +204,8 @@ public XamlFileGenerator( bool skipUserControlsInVisualTree, bool shouldAnnotateGeneratedXaml, bool isUnoAssembly, - bool isLazyVisualStateManagerEnabled + bool isLazyVisualStateManagerEnabled, + bool xamlResourcesTrimming ) { _fileDefinition = file; @@ -217,6 +225,7 @@ bool isLazyVisualStateManagerEnabled _skipUserControlsInVisualTree = skipUserControlsInVisualTree; _shouldAnnotateGeneratedXaml = shouldAnnotateGeneratedXaml; _isLazyVisualStateManagerEnabled = isLazyVisualStateManagerEnabled; + _xamlResourcesTrimming = xamlResourcesTrimming; InitCaches(); @@ -1273,11 +1282,15 @@ private bool BuildDefaultStylesRegistration(IIndentedStringBuilder writer, XamlM if (SymbolEqualityComparer.Default.Equals(targetType.ContainingAssembly, _metadataHelper.Compilation.Assembly)) { var isNativeStyle = style.Members.FirstOrDefault(m => m.Member.Name == "IsNativeStyle")?.Value as string == "True"; - writer.AppendLineInvariant("global::Windows.UI.Xaml.Style.RegisterDefaultStyleForType({0}, {1}, /*isNativeStyle:*/{2});", - implicitKey, - SingletonInstanceAccess, - isNativeStyle.ToString().ToLowerInvariant() - ); + + using (TrySingleLineIfForLinkerHint(writer, style)) + { + writer.AppendLineInvariant("global::Windows.UI.Xaml.Style.RegisterDefaultStyleForType({0}, {1}, /*isNativeStyle:*/{2});", + implicitKey, + SingletonInstanceAccess, + isNativeStyle.ToString().ToLowerInvariant() + ); + } } else { @@ -2475,11 +2488,21 @@ private void BuildResourceDictionary(IIndentedStringBuilder writer, XamlMemberDe if (isInInitializer) { - writer.AppendLineInvariant("[{0}] = ", wrappedKey); + writer.AppendLineInvariant("["); + using (TryTernaryForLinkerHint(writer, resource)) + { + writer.AppendLineInvariant(wrappedKey); + } + writer.AppendLineInvariant("] = ", wrappedKey); } else { - writer.AppendLineInvariant("{0}[{1}] = ", dictIdentifier, wrappedKey); + writer.AppendLineInvariant("{0}[", dictIdentifier); + using (TryTernaryForLinkerHint(writer, resource)) + { + writer.AppendLineInvariant(wrappedKey); + } + writer.AppendLineInvariant("] = "); } if (resource.Type.Name == "StaticResource") @@ -2500,14 +2523,19 @@ private void BuildResourceDictionary(IIndentedStringBuilder writer, XamlMemberDe && !_isInChildSubclass && GetResourceDictionaryInitializerName(key) is string initializerName) { - // - writer.AppendLineInvariant("new global::Uno.UI.Xaml.WeakResourceInitializer(this, {0})", initializerName); + using (TryTernaryForLinkerHint(writer, resource)) + { + writer.AppendLineInvariant("new global::Uno.UI.Xaml.WeakResourceInitializer(this, {0})", initializerName); + } } else { - using (BuildLazyResourceInitializer(writer)) + using (TryTernaryForLinkerHint(writer, resource)) { - BuildChild(writer, null, resource); + using (BuildLazyResourceInitializer(writer)) + { + BuildChild(writer, null, resource); + } } } @@ -2525,6 +2553,55 @@ private void BuildResourceDictionary(IIndentedStringBuilder writer, XamlMemberDe } } + private IDisposable TryTernaryForLinkerHint(IIndentedStringBuilder writer, XamlObjectDefinition resource) + => TryLinkerHint( + resource, + s => writer.AppendLineInvariant($"{s} ? ("), + () => writer.AppendLineInvariant("): null") + ); + + private IDisposable TrySingleLineIfForLinkerHint(IIndentedStringBuilder writer, XamlObjectDefinition resource) + => TryLinkerHint( + resource, + s => writer.AppendLineInvariant($"if({s})"), + () => { } + ); + + private IDisposable TryLinkerHint(XamlObjectDefinition resource, Action prefix, Action suffix) + { + var hintEnabled = false; + + if (_xamlResourcesTrimming) + { + var styleTargetType = resource.Type.Name == "Style" + ? resource.Members.FirstOrDefault(m => m.Member.Name == "TargetType")?.Value as string + : null; + + if (styleTargetType != null) + { + var symbol = GetType(styleTargetType); + + if (symbol.GetAllInterfaces().Any(i => SymbolEqualityComparer.Default.Equals(i, _dependencyObjectSymbol))) + { + var safeTypeName = LinkerHintsHelpers.GetPropertyAvailableName(symbol.GetFullMetadataName()); + var linkerHintClass = LinkerHintsHelpers.GetLinkerHintsClassName(_defaultNamespace); + + prefix($"global::{linkerHintClass}.{safeTypeName}"); + + hintEnabled = true; + } + } + } + + return Disposable.Create(() => + { + if (hintEnabled) + { + suffix(); + } + }); + } + /// /// Whether this resource should be lazily initialized. /// diff --git a/src/SourceGenerators/Uno.UI.Tasks/Content/Uno.UI.Tasks.targets b/src/SourceGenerators/Uno.UI.Tasks/Content/Uno.UI.Tasks.targets index 0b4491e8282b..afad4d4210ca 100644 --- a/src/SourceGenerators/Uno.UI.Tasks/Content/Uno.UI.Tasks.targets +++ b/src/SourceGenerators/Uno.UI.Tasks/Content/Uno.UI.Tasks.targets @@ -1,16 +1,18 @@ - ..\Uno.UI.Tasks - true + ..\Uno.UI.Tasks + true - + + + + BeforeTargets="_GetLibraryImports;PrepareForBuild;_CheckForContent" + DependsOnTargets="ResolveProjectReferences" + Condition="'$(DesignTimeBuild)' != 'true' and '$(BuildingInsideUnoSourceGenerator)' == ''"> - - - - $(DefaultApplicationLanguage) - - en - - - - + + + $(DefaultApplicationLanguage) + + en + + + + - - - - - - + + + + + + - - - True - - + + + True + + - - - - - - + + + + @@ -79,28 +81,97 @@ + + + + + + + <_UnoLinkerHintsPass1AssembliesForReferencePaths + Include="@(ReferencePath)" + Condition="'%(Extension)' == '.dll'" /> + + + + <_SubstitutionFiles Include="$(BaseIntermediateOutputPath)\Substitutions\*.Substitutions.xml" /> + + + + + + + + + + + <_UnoLinkerHintsPass1AssembliesForReferencePaths + Include="@(ReferencePath)" + Condition="'%(Extension)' == '.dll'" /> + + + + + + + + + + + + + + <_UnoWasmBootstrapVersion Include="@(PackageReference->'%(Version)')" + Condition="'%(Identity)'=='Uno.Wasm.Bootstrap'"/> + + + <_UnoWasmBootstrapVersionString>@(_UnoWasmBootstrapVersion) + + + + + - - - - - - - + + + + + + + - - + + diff --git a/src/SourceGenerators/Uno.UI.Tasks/LinkerHintsGenerator/LinkerDefinitionMergerTask.cs b/src/SourceGenerators/Uno.UI.Tasks/LinkerHintsGenerator/LinkerDefinitionMergerTask.cs new file mode 100644 index 000000000000..3ed11170f3de --- /dev/null +++ b/src/SourceGenerators/Uno.UI.Tasks/LinkerHintsGenerator/LinkerDefinitionMergerTask.cs @@ -0,0 +1,128 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Transactions; +using System.Xml; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mono.Cecil; + +namespace Uno.UI.Tasks.LinkerHintsGenerator +{ + /// + /// A task used to merge linker definition files and embed the result in an assembly + /// + public class LinkerDefinitionMergerTask_v0 : Microsoft.Build.Utilities.Task + { + private const MessageImportance DefaultLogMessageLevel + +#if DEBUG + = MessageImportance.High; +#else + = MessageImportance.Low; +#endif + + private string[]? _referencePaths; + + [Required] + public Microsoft.Build.Framework.ITaskItem[]? DefinitionFiles { get; set; } + + [Required] + public string TargetAssembly { get; set; } = ""; + + [Required] + public string TargetResourceName { get; set; } = ""; + + [Required] + public Microsoft.Build.Framework.ITaskItem[]? ReferencePath { get; set; } + + + public override bool Execute() + { + // Debugger.Launch(); + + if (DefinitionFiles != null) + { + var doc = new XmlDocument(); + + var xmlDeclaration = doc.CreateXmlDeclaration("1.0", "UTF-8", null); + + var root = doc.DocumentElement; + doc.InsertBefore(xmlDeclaration, root); + + var linkerNode = doc.CreateElement(string.Empty, "linker", string.Empty); + doc.AppendChild(linkerNode); + + foreach (var definition in DefinitionFiles) + { + Log.LogMessage(DefaultLogMessageLevel, $"Merging substitution file {definition}"); + + var defDoc = new XmlDocument(); + defDoc.Load(definition.ItemSpec); + + linkerNode.InnerXml += defDoc.DocumentElement.InnerXml; + } + + var outputPath = Path.GetTempFileName(); + doc.Save(outputPath); + + Log.LogMessage(DefaultLogMessageLevel, $"Writing substitution file to {TargetAssembly}"); + + var tempFile = Path.GetTempFileName(); + + var resolver = new DefaultAssemblyResolver(); + foreach(var path in BuildReferencesPaths()) + { + resolver.AddSearchDirectory(path); + } + + using (var asm = AssemblyDefinition.ReadAssembly(TargetAssembly, new ReaderParameters() { AssemblyResolver = resolver } )) + { + asm.MainModule.Resources.Add(new EmbeddedResource(TargetResourceName, ManifestResourceAttributes.Public, File.ReadAllBytes(outputPath))); + + asm.Write(tempFile); + } + + WaitForUnlockedFile(TargetAssembly); + + File.Delete(TargetAssembly); + File.Move(tempFile, TargetAssembly); + } + + return true; + } + + private string[] BuildReferencesPaths() => ReferencePath + .Select(p => Path.GetDirectoryName(p.ItemSpec)) + .Distinct() + .ToArray(); + + private void WaitForUnlockedFile(string filePath) + { + var sw = Stopwatch.StartNew(); + + while (sw.Elapsed < TimeSpan.FromSeconds(5)) + { + try + { + File.OpenWrite(filePath).Dispose(); + } + catch + { + Log.LogMessage(MessageImportance.Low, $"Waiting for availability for {TargetAssembly}"); + } + + Thread.Sleep(100); + } + } + } +} diff --git a/src/SourceGenerators/Uno.UI.Tasks/LinkerHintsGenerator/LinkerHintGeneratorTask.cs b/src/SourceGenerators/Uno.UI.Tasks/LinkerHintsGenerator/LinkerHintGeneratorTask.cs new file mode 100644 index 000000000000..936391c1f861 --- /dev/null +++ b/src/SourceGenerators/Uno.UI.Tasks/LinkerHintsGenerator/LinkerHintGeneratorTask.cs @@ -0,0 +1,357 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http.Headers; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Transactions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using Uno.UI.SourceGenerators.BindableTypeProviders; + +namespace Uno.UI.Tasks.LinkerHintsGenerator +{ + /// + /// Task used to generate a linker feature list, based on all features being disabled. + /// + public class LinkerHintGeneratorTask_v0 : Microsoft.Build.Utilities.Task + { + private const MessageImportance DefaultLogMessageLevel +#if DEBUG + = MessageImportance.High; +#else + = MessageImportance.Low; +#endif + + private List _referencedAssemblies = new List(); + private string[] _searchPaths = new string[0]; + private DefaultAssemblyResolver? _assemblyResolver; + + [Required] + public string AssemblyPath { get; set; } = ""; + + [Required] + public string CurrentProjectPath { get; set; } = ""; + + [Required] + public string ILLinkerPath { get; set; } = ""; + + [Required] + public string OutputPath { get; set; } = ""; + + [Required] + public Microsoft.Build.Framework.ITaskItem[]? ReferencePath { get; set; } + + [Output] + public Microsoft.Build.Framework.ITaskItem[]? OutputFeatures { get; set; } + + public override bool Execute() + { + // Debugger.Launch(); + + BuildReferences(); + OutputPath = AlignPath(OutputPath); + + var pass1Path = Path.Combine(OutputPath, "pass1"); + + var features = BuildLinkerFeaturesList(); + + Log.LogMessage(DefaultLogMessageLevel, $"Running linker pass 1"); + + RunLinker(pass1Path, features); + var currrentPassFeatures = BuildResultingFeaturesList(pass1Path); + + int pass= 1; + do + { + pass++; + + var currentPassPath = Path.Combine(OutputPath, $"pass{pass}"); + var currentPassLinkerFeatures = FormatFeaturesForLinker(currrentPassFeatures); + + Log.LogMessage(DefaultLogMessageLevel, $"Running linker pass {pass}"); + Log.LogMessage(DefaultLogMessageLevel, $"Pass features {currentPassLinkerFeatures}"); + RunLinker(currentPassPath, currentPassLinkerFeatures); + + var newFeatures = BuildResultingFeaturesList(currentPassPath); + + var newList = ToOrderedFeatureArray(newFeatures); + var currentList = ToOrderedFeatureArray(currrentPassFeatures); + + currrentPassFeatures = newFeatures; + + if (newList.SequenceEqual(currentList)) + { + Log.LogMessage(DefaultLogMessageLevel, $"Found stable features list, copying to output directory"); + Log.LogMessage(DefaultLogMessageLevel, $"Final features {FormatFeaturesForLinker(currrentPassFeatures)}"); + + foreach (var file in Directory.GetFiles(currentPassPath, "*.dll")) + { + var targetPath = Path.Combine(OutputPath, Path.GetFileName(file)); + File.Copy(file, targetPath, true); + } + + break; + } + else + { + Log.LogMessage(DefaultLogMessageLevel, $"Feature list changed, linking again"); + } + } + while (true); + + OutputFeatures = currrentPassFeatures + .Select(f => new TaskItem(f.Key, new Dictionary { ["Value"] = f.Value })) + .ToArray(); + + return true; + } + + private static string FormatFeaturesForLinker(Dictionary currrentPassFeatures) => + string.Join(" ", currrentPassFeatures.Select(h => $"--feature {h.Key} {h.Value}")); + private static (string Key, string Value)[] ToOrderedFeatureArray(Dictionary features) => features + .Select(p => (p.Key, p.Value)) + .OrderBy(p => p.Key) + .ToArray(); + + private void RunLinker(string outputPath, string features) + { + var linkerPath = Path.Combine(ILLinkerPath, "illink.dll"); + + var linkerSearchPaths = string.Join(" ", _referencedAssemblies.Select(Path.GetDirectoryName).Distinct().Select(p => $"-d \"{p}\" ")); + + var parameters = new List() + { + $"--feature UnoBindableMetadata false", + $"--verbose", + $"--deterministic", + $"--used-attrs-only true", + $"--skip-unresolved true", + $"-b true", + $"-a {AssemblyPath}", + $"-out {outputPath}", + linkerSearchPaths, + features, + }; + + var paramString = string.Join("\n", parameters); + var file = Path.GetTempFileName(); + File.WriteAllText(file, paramString); + + Directory.CreateDirectory(OutputPath); + + var res = StartProcess("dotnet", $"{linkerPath} @{file}", CurrentProjectPath); + + if (!string.IsNullOrEmpty(res.error)) + { + Log.LogError(res.error); + } + } + + private Dictionary BuildResultingFeaturesList(string resultPath) + { + var sourceList = new List(); + sourceList.AddRange(Directory.GetFiles(resultPath, "*.dll")); + + var assemblies = new List( + from asmPath in sourceList.Distinct() + let asm = ReadAssembly(asmPath) + where asm != null + select asm + ); + + var features = new Dictionary(); + + var availableLinkerHints = FindAvailableLinkerHints(assemblies); + + foreach(var hint in availableLinkerHints) + { + features[hint] = "false"; + } + + foreach (var asm in assemblies) + { + foreach(var type in asm.MainModule.Types) + { + if (IsDependencyObject(type)) + { + features[LinkerHintsHelpers.GetPropertyAvailableName(type.FullName)] = "true"; + } + } + } + + return features; + } + + private bool IsDependencyObject(TypeDefinition type) + { + if(type.Interfaces.Any(c => c.InterfaceType.FullName == "Windows.UI.Xaml.DependencyObject")) + { + return true; + } + + try + { + if (type.BaseType != null && IsDependencyObject(type.BaseType.Resolve())) + { + return true; + } + } + catch(Exception e) + { + Log.LogMessage(DefaultLogMessageLevel, $"Failed to resolve base types for {type.FullName}: {e.Message}"); + } + + return false; + } + + private string BuildLinkerFeaturesList() + { + var assemblySearchList = BuildResourceSearchList(); + + var hints = FindAvailableLinkerHints(assemblySearchList); + + var output = string.Join(" ", hints.Select(h => $"--feature {h} false")); + + assemblySearchList.ForEach(a => a.Dispose()); + + return output; + } + + private static List FindAvailableLinkerHints(List assemblySearchList) + { + var hints = new List(); + + foreach (var asm in assemblySearchList) + { + if (asm.MainModule.Types.FirstOrDefault(t => t.Name == "__LinkerHints") is { } linkerHints) + { + foreach (var prop in linkerHints.Properties) + { + hints.Add(prop.Name); + } + } + } + + return hints.Distinct().ToList(); + } + + private List BuildResourceSearchList() + { + var sourceList = new List(); + + sourceList.AddRange(_referencedAssemblies); + + // Add the main assembly last so it can have a final say + sourceList.Add(AssemblyPath); + + return new List( + from asmPath in sourceList.Distinct() + let asm = ReadAssembly(asmPath) + where asm != null + select asm + ); + } + + private AssemblyDefinition? ReadAssembly(string asmPath) + { + try + { + return AssemblyDefinition.ReadAssembly(asmPath, new ReaderParameters { AssemblyResolver = _assemblyResolver }); + } + catch (Exception ex) + { + Log.LogMessage(MessageImportance.Low, $"Failed to read assembly {ex}"); + return null; + } + } + + private void BuildReferences() + { + if (ReferencePath != null) + { + foreach (var referencePath in ReferencePath) + { + var isReferenceAssembly = referencePath.GetMetadata("PathInPackage")?.StartsWith("ref/", StringComparison.OrdinalIgnoreCase) ?? false; + var hasConcreteAssembly = isReferenceAssembly && ReferencePath.Any(innerReference => HasConcreteAssemblyForReferenceAssembly(innerReference, referencePath)); + + var name = Path.GetFileName(referencePath.ItemSpec); + _referencedAssemblies.Add(referencePath.ItemSpec); + } + + _searchPaths = ReferencePath + .Select(p => Path.GetDirectoryName(p.ItemSpec)) + .Distinct() + .ToArray(); + + _assemblyResolver = new DefaultAssemblyResolver(); + + foreach (var assembly in _searchPaths) + { + _assemblyResolver.AddSearchDirectory(assembly); + } + } + } + + private static bool HasConcreteAssemblyForReferenceAssembly(ITaskItem other, ITaskItem referenceAssembly) + => Path.GetFileName(other.ItemSpec) == Path.GetFileName(referenceAssembly.ItemSpec) && (other.GetMetadata("PathInPackage")?.StartsWith("lib/", StringComparison.OrdinalIgnoreCase) ?? false); + + + private string AlignPath(string outputPath) + => outputPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).Replace(new string(Path.DirectorySeparatorChar, 2), Path.DirectorySeparatorChar.ToString()); + + private (int exitCode, string output, string error) StartProcess(string executable, string parameters, string workingDirectory) + { + Log.LogMessage( + DefaultLogMessageLevel, + $"[{workingDirectory}] {executable} {parameters}"); + + var p = new Process + { + StartInfo = + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = executable, + Arguments = parameters + } + }; + + if (workingDirectory != null) + { + p.StartInfo.WorkingDirectory = workingDirectory; + } + + var output = new StringBuilder(); + var error = new StringBuilder(); + var elapsed = Stopwatch.StartNew(); + + p.OutputDataReceived += (s, e) => { if (e.Data != null) { Log.LogMessage(DefaultLogMessageLevel, $"[{elapsed.Elapsed}] {e.Data}"); output.Append(e.Data); } }; + p.ErrorDataReceived += (s, e) => { if (e.Data != null) { Log.LogError($"[{elapsed.Elapsed}] {e.Data}"); error.Append(e.Data); } }; + + if (p.Start()) + { + p.BeginOutputReadLine(); + p.BeginErrorReadLine(); + p.WaitForExit(); + var exitCore = p.ExitCode; + p.CancelErrorRead(); + p.CancelOutputRead(); + p.Close(); + + return (exitCore, output.ToString(), error.ToString()); + } + else + { + throw new Exception($"Failed to start [{executable}]"); + } + + } + } +} diff --git a/src/SourceGenerators/Uno.UI.Tasks/Uno.UI.Tasks.csproj b/src/SourceGenerators/Uno.UI.Tasks/Uno.UI.Tasks.csproj index cfd895869f37..e6f033e86609 100644 --- a/src/SourceGenerators/Uno.UI.Tasks/Uno.UI.Tasks.csproj +++ b/src/SourceGenerators/Uno.UI.Tasks/Uno.UI.Tasks.csproj @@ -28,6 +28,7 @@ 15.4.8 runtime + @@ -46,6 +47,11 @@ Resources\AndroidResourceNameEncoder.cs + + + + + @@ -65,7 +71,7 @@ <_OverrideTargetFramework>$(TargetFramework) <_baseNugetPath Condition="'$(USERPROFILE)'!=''">$(USERPROFILE) <_baseNugetPath Condition="'$(HOME)'!=''">$(HOME) - <_TargetNugetFolder>$(_baseNugetPath)\.nuget\packages\Uno.UI\$(UnoNugetOverrideVersion)\build\Uno.UI.Tasks + <_TargetNugetFolder>$(_baseNugetPath)\.nuget\packages\Uno.UI\$(UnoNugetOverrideVersion)\buildTransitive\Uno.UI.Tasks <_OutputFiles Include="$(TargetDir)**" /> diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/Mono.Cecil.Pdb.dll b/src/SourceGenerators/Uno.UI.Tasks/external/linker/Mono.Cecil.Pdb.dll new file mode 100644 index 000000000000..32233bda9eb4 Binary files /dev/null and b/src/SourceGenerators/Uno.UI.Tasks/external/linker/Mono.Cecil.Pdb.dll differ diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/Mono.Cecil.dll b/src/SourceGenerators/Uno.UI.Tasks/external/linker/Mono.Cecil.dll new file mode 100644 index 000000000000..91cf0fb2c8c3 Binary files /dev/null and b/src/SourceGenerators/Uno.UI.Tasks/external/linker/Mono.Cecil.dll differ diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/System.Json.dll b/src/SourceGenerators/Uno.UI.Tasks/external/linker/System.Json.dll new file mode 100644 index 000000000000..1e154aff6d47 Binary files /dev/null and b/src/SourceGenerators/Uno.UI.Tasks/external/linker/System.Json.dll differ diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/System.Reflection.MetadataLoadContext.dll b/src/SourceGenerators/Uno.UI.Tasks/external/linker/System.Reflection.MetadataLoadContext.dll new file mode 100644 index 000000000000..51d9cf0152a2 Binary files /dev/null and b/src/SourceGenerators/Uno.UI.Tasks/external/linker/System.Reflection.MetadataLoadContext.dll differ diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.deps.json b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.deps.json new file mode 100644 index 000000000000..5cbb32bcd275 --- /dev/null +++ b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.deps.json @@ -0,0 +1,171 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v5.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v5.0": { + "illink/6.0.100-dev": { + "dependencies": { + "Microsoft.CodeAnalysis.CSharp.CodeStyle": "3.10.0-2.21203.6", + "Microsoft.DotNet.CodeAnalysis": "6.0.0-beta.21105.12", + "Microsoft.Net.Compilers.Toolset": "3.10.0-2.21203.6", + "Microsoft.SourceLink.AzureRepos.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.GitHub": "1.1.0-beta-20206-02", + "Mono.Cecil": "0.11.3", + "Mono.Cecil.Pdb": "0.11.3", + "XliffTasks": "1.0.0-beta.21224.1" + }, + "runtime": { + "illink.dll": {} + }, + "resources": { + "cs/illink.resources.dll": { + "locale": "cs" + }, + "de/illink.resources.dll": { + "locale": "de" + }, + "es/illink.resources.dll": { + "locale": "es" + }, + "fr/illink.resources.dll": { + "locale": "fr" + }, + "it/illink.resources.dll": { + "locale": "it" + }, + "ja/illink.resources.dll": { + "locale": "ja" + }, + "ko/illink.resources.dll": { + "locale": "ko" + }, + "pl/illink.resources.dll": { + "locale": "pl" + }, + "pt-BR/illink.resources.dll": { + "locale": "pt-BR" + }, + "ru/illink.resources.dll": { + "locale": "ru" + }, + "tr/illink.resources.dll": { + "locale": "tr" + }, + "zh-Hans/illink.resources.dll": { + "locale": "zh-Hans" + }, + "zh-Hant/illink.resources.dll": { + "locale": "zh-Hant" + } + } + }, + "Microsoft.Build.Tasks.Git/1.1.0-beta-20206-02": {}, + "Microsoft.CodeAnalysis.CSharp.CodeStyle/3.10.0-2.21203.6": {}, + "Microsoft.DotNet.CodeAnalysis/6.0.0-beta.21105.12": {}, + "Microsoft.Net.Compilers.Toolset/3.10.0-2.21203.6": {}, + "Microsoft.SourceLink.AzureRepos.Git/1.1.0-beta-20206-02": { + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.Common": "1.1.0-beta-20206-02" + } + }, + "Microsoft.SourceLink.Common/1.1.0-beta-20206-02": {}, + "Microsoft.SourceLink.GitHub/1.1.0-beta-20206-02": { + "dependencies": { + "Microsoft.Build.Tasks.Git": "1.1.0-beta-20206-02", + "Microsoft.SourceLink.Common": "1.1.0-beta-20206-02" + } + }, + "XliffTasks/1.0.0-beta.21224.1": {}, + "Mono.Cecil/0.11.3": { + "runtime": { + "Mono.Cecil.dll": {} + } + }, + "Mono.Cecil.Pdb/0.11.3": { + "dependencies": { + "Mono.Cecil": "0.11.3" + }, + "runtime": { + "Mono.Cecil.Pdb.dll": {} + } + } + } + }, + "libraries": { + "illink/6.0.100-dev": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.Build.Tasks.Git/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-hZ9leS9Yd9MHpqvviMftSJFDcLYu2h1DrapW1TDm1s1fgOy71c8HvArNMd3fseVkXmp3VTfGnkgcw0FR+TI6xw==", + "path": "microsoft.build.tasks.git/1.1.0-beta-20206-02", + "hashPath": "microsoft.build.tasks.git.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.CodeAnalysis.CSharp.CodeStyle/3.10.0-2.21203.6": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fsT4Pbkk07Cp+vxjpYviX4vh/XlOrNjsU8Lh9q9SfRDqpoS4EuwoMqBdPBM0s5uJ2EOjNPg8PwTa/8NDlU0/eQ==", + "path": "microsoft.codeanalysis.csharp.codestyle/3.10.0-2.21203.6", + "hashPath": "microsoft.codeanalysis.csharp.codestyle.3.10.0-2.21203.6.nupkg.sha512" + }, + "Microsoft.DotNet.CodeAnalysis/6.0.0-beta.21105.12": { + "type": "package", + "serviceable": true, + "sha512": "sha512-dmMVnb1XyFucbZZSOyBVzEYX3DIU6Y/JL/hzy3r4EPdQPxwTcaMOYYZ0RvPwSostgR9af0IIVXi0VFosN7RV5w==", + "path": "microsoft.dotnet.codeanalysis/6.0.0-beta.21105.12", + "hashPath": "microsoft.dotnet.codeanalysis.6.0.0-beta.21105.12.nupkg.sha512" + }, + "Microsoft.Net.Compilers.Toolset/3.10.0-2.21203.6": { + "type": "package", + "serviceable": true, + "sha512": "sha512-EEbyz3ENhbmyInnvTcSghFnqvS8+QQFjE0QActzdA3pM1PnOAECqsgPTiEji6Oz3Ij4uB2tUzu8HJPREFNHHeQ==", + "path": "microsoft.net.compilers.toolset/3.10.0-2.21203.6", + "hashPath": "microsoft.net.compilers.toolset.3.10.0-2.21203.6.nupkg.sha512" + }, + "Microsoft.SourceLink.AzureRepos.Git/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-vVYhSds9TfraTQkGHHMDMVWnr3kCkTZ7vmqUmrXQBDJFXiWTuMoP5RRa9s1M/KmgB4szi5TOb7sOaHWKDT9qDA==", + "path": "microsoft.sourcelink.azurerepos.git/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.azurerepos.git.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.SourceLink.Common/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-aek0RTQ+4Bf11WvqaXajwYoaBWkX2edBjAr5XJOvhAsHX6/9vPOb7IpHAiE/NyCse7IcpGWslJZHNkv4UBEFqw==", + "path": "microsoft.sourcelink.common/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.common.1.1.0-beta-20206-02.nupkg.sha512" + }, + "Microsoft.SourceLink.GitHub/1.1.0-beta-20206-02": { + "type": "package", + "serviceable": true, + "sha512": "sha512-7A7P0EwL+lypaI/CEvG4IcpAlQeAt04uPPw1SO6Q9Jwz2nE9309pQXJ4TfP/RLL8IOObACidN66+gVR+bJDZHw==", + "path": "microsoft.sourcelink.github/1.1.0-beta-20206-02", + "hashPath": "microsoft.sourcelink.github.1.1.0-beta-20206-02.nupkg.sha512" + }, + "XliffTasks/1.0.0-beta.21224.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-LDhQk+1vOGEZcuGOv7flrDVzc44pzFA57mhqrnxMbYexDU7rTzuCZ251aLz8V6x3hNDFb6jiLOdskB7pNOfm3g==", + "path": "xlifftasks/1.0.0-beta.21224.1", + "hashPath": "xlifftasks.1.0.0-beta.21224.1.nupkg.sha512" + }, + "Mono.Cecil/0.11.3": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Mono.Cecil.Pdb/0.11.3": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.dll b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.dll new file mode 100644 index 000000000000..1049a676c7a5 Binary files /dev/null and b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.dll differ diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.exe b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.exe new file mode 100644 index 000000000000..60519c669f37 Binary files /dev/null and b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.exe differ diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.runtimeconfig.dev.json b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.runtimeconfig.dev.json new file mode 100644 index 000000000000..f3298c1176f5 --- /dev/null +++ b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.runtimeconfig.dev.json @@ -0,0 +1,11 @@ +{ + "runtimeOptions": { + "additionalProbingPaths": [ + "C:\\Users\\jerome.laban\\.dotnet\\store\\|arch|\\|tfm|", + "C:\\Users\\jerome.laban\\.nuget\\packages", + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages", + "C:\\Program Files (x86)\\Microsoft\\Xamarin\\NuGet", + "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder" + ] + } +} \ No newline at end of file diff --git a/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.runtimeconfig.json b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.runtimeconfig.json new file mode 100644 index 000000000000..b3f87bcc6794 --- /dev/null +++ b/src/SourceGenerators/Uno.UI.Tasks/external/linker/illink.runtimeconfig.json @@ -0,0 +1,13 @@ +{ + "runtimeOptions": { + "tfm": "net5.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "5.0.0" + }, + "rollForwardOnNoCandidateFx": 2, + "configProperties": { + "System.GC.Server": true + } + } +} \ No newline at end of file diff --git a/src/Uno.UI.FluentTheme/Uno.UI.FluentTheme.Wasm.csproj b/src/Uno.UI.FluentTheme/Uno.UI.FluentTheme.Wasm.csproj index 5d579b4cadf9..0cd5677460c4 100644 --- a/src/Uno.UI.FluentTheme/Uno.UI.FluentTheme.Wasm.csproj +++ b/src/Uno.UI.FluentTheme/Uno.UI.FluentTheme.Wasm.csproj @@ -14,7 +14,8 @@ false true - + + true WebAssembly true @@ -33,4 +34,6 @@ + + diff --git a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Wasm.csproj b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Wasm.csproj index 8d394cc4f7d2..b325371743ba 100644 --- a/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Wasm.csproj +++ b/src/Uno.UI.Toolkit/Uno.UI.Toolkit.Wasm.csproj @@ -14,7 +14,9 @@ false true - + + true + WebAssembly .\ diff --git a/src/Uno.UI/AssemblyInfo.cs b/src/Uno.UI/AssemblyInfo.cs index 9681ccd177cb..b5547b7e02f1 100644 --- a/src/Uno.UI/AssemblyInfo.cs +++ b/src/Uno.UI/AssemblyInfo.cs @@ -1,4 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Reflection; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Uno.UI.DualScreen")] [assembly: InternalsVisibleTo("Uno.UI.Tests")] @@ -15,3 +16,5 @@ [assembly: InternalsVisibleTo("SamplesApp.Wasm")] [assembly: InternalsVisibleTo("SamplesApp.Skia")] [assembly: InternalsVisibleTo("Uno.UI.FluentTheme")] + +[assembly: AssemblyMetadata("IsTrimmable", "True")] diff --git a/src/Uno.UI/UI/Xaml/ResourceDictionary.cs b/src/Uno.UI/UI/Xaml/ResourceDictionary.cs index 832c4bd2a430..ffa17f8dc24e 100644 --- a/src/Uno.UI/UI/Xaml/ResourceDictionary.cs +++ b/src/Uno.UI/UI/Xaml/ResourceDictionary.cs @@ -83,8 +83,17 @@ internal object Lookup(string key) /// and can be removed as breaking change later. public bool Insert(object key, object value) { - Set(new ResourceKey(key), value, throwIfPresent: false); - return true; + if (key is { }) + { + Set(new ResourceKey(key), value, throwIfPresent: false); + return true; + } + else + { + // This case is present to support XAML resources trimming + // https://github.com/unoplatform/uno/issues/6564 + return false; + } } public bool Remove(object key) => _values.Remove(new ResourceKey(key)); @@ -178,7 +187,13 @@ public object this[object key] return value; } - set => Set(new ResourceKey(key), value, throwIfPresent: false); + set + { + if(!(key is null)) + { + Set(new ResourceKey(key), value, throwIfPresent: false); + } + } } private void Set(in ResourceKey resourceKey, object value, bool throwIfPresent) diff --git a/src/Uno.UI/Uno.UI.Wasm.csproj b/src/Uno.UI/Uno.UI.Wasm.csproj index 8181f7f83304..f84b67c6a3a7 100644 --- a/src/Uno.UI/Uno.UI.Wasm.csproj +++ b/src/Uno.UI/Uno.UI.Wasm.csproj @@ -21,6 +21,7 @@ Uno.WinUI + true WebAssembly .\ @@ -47,7 +48,7 @@ - $(AssemblyName).xml + $(AssemblyName).xml