From 06e4fe42329a5bfd55bc6d2d55d1895aacd81971 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Fri, 5 Apr 2019 09:17:16 -0500 Subject: [PATCH] [Xamarin.Android.Build.Tasks] filter @(ReferencePath) for MonoAndroid assemblies We have a couple MSBuild targets that need to only operate on `MonoAndroid` assemblies: * `_BuildAdditionalResourcesCache` is the precursor to Xamarin.Build.Download * `_ResolveLibraryProjectImports` unzips `__AndroidLibrariesProjects__.zip`, .jar/.aar files, etc. Both of these targets all looking at *all* assemblies, so we could make a new item group `@(_MonoAndroidReferencePath)` and use this instead. This would both allow the tasks inside these targets to operate on less assemblies. It would also allow them to skip for changes in NetStandard projects. I added a new `` MSBuild task to filter based on the presence of this attribute in an assembly: [assembly: System.Runtime.Versioning.TargetFrameworkAttribute ("MonoAndroid,Version=v8.1")] MSBuild/Roslyn populate this attribute based on the `$(TargetFrameworkIdentifier)` of the project. One complication is that during design-time builds, some assemblies may not exist. I made `` just skip files in this case--using the same logic that `` uses. (I also fixed some wording/misspelling) ~~ Results ~~ I tested the Xamarin.Forms project in this repo. Initial build: Before: 78 ms _BuildAdditionalResourcesCache 1 calls 1678 ms _ResolveLibraryProjectImports 1 calls After: 47 ms FilterAssemblies 1 calls 23 ms _BuildAdditionalResourcesCache 1 calls 1120 ms _ResolveLibraryProjectImports 1 calls Incremental build with XAML change: Before: 62 ms _BuildAdditionalResourcesCache 1 calls 300 ms _ResolveLibraryProjectImports 1 calls After: 62 ms FilterAssemblies 1 calls 0 ms _BuildAdditionalResourcesCache 1 calls 16 ms _ResolveLibraryProjectImports 1 calls Note that during the incremental build, since only a NetStandard assembly was updated the following targets are skipped: _BuildAdditionalResourcesCache: Skipping target "_BuildAdditionalResourcesCache" because all output files are up-to-date with respect to the input files. ... _ResolveLibraryProjectImports: Skipping target "_ResolveLibraryProjectImports" because all output files are up-to-date with respect to the input files. Overall I would say this saves ~500ms on initial build, and ~250ms on incremental builds. --- .../Tasks/FilterAssemblies.cs | 51 +++++++++++++++++++ .../Tasks/ResolveLibraryProjectImports.cs | 2 +- .../Utilities/MetadataExtensions.cs | 29 ++++++++++- .../Xamarin.Android.Build.Tasks.csproj | 1 + .../Xamarin.Android.Common.targets | 15 ++++-- 5 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/FilterAssemblies.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/FilterAssemblies.cs b/src/Xamarin.Android.Build.Tasks/Tasks/FilterAssemblies.cs new file mode 100644 index 00000000000..af76d46970f --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/FilterAssemblies.cs @@ -0,0 +1,51 @@ +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; + +namespace Xamarin.Android.Tasks +{ + /// + /// Filters a set of assemblies based on a given TargetFrameworkIdentifier + /// + public class FilterAssemblies : Task + { + [Required] + public string TargetFrameworkIdentifier { get; set; } + + [Required] + public bool DesignTimeBuild { get; set; } + + public ITaskItem [] InputAssemblies { get; set; } + + [Output] + public ITaskItem [] OutputAssemblies { get; set; } + + public override bool Execute () + { + if (InputAssemblies == null) + return true; + + var output = new List (InputAssemblies.Length); + foreach (var assemblyItem in InputAssemblies) { + if (DesignTimeBuild && !File.Exists (assemblyItem.ItemSpec)) { + Log.LogDebugMessage ($"Skipping non-existent dependency '{assemblyItem.ItemSpec}' during a design-time build."); + continue; + } + using (var pe = new PEReader (File.OpenRead (assemblyItem.ItemSpec))) { + var reader = pe.GetMetadataReader (); + var assemblyDefinition = reader.GetAssemblyDefinition (); + var targetFrameworkIdentifier = assemblyDefinition.GetTargetFrameworkIdentifier (reader); + if (targetFrameworkIdentifier == TargetFrameworkIdentifier) { + output.Add (assemblyItem); + } + } + } + OutputAssemblies = output.ToArray (); + + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs index 73d1112d1f2..09641d85870 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs @@ -200,7 +200,7 @@ void Extract ( .Where (a => a != null) .Distinct ()) { if (DesignTimeBuild && !File.Exists (assemblyPath)) { - Log.LogDebugMessage ("Skipping non existant dependancy '{0}' due to design time build.", assemblyPath); + Log.LogDebugMessage ($"Skipping non-existent dependency '{assemblyPath}' during a design-time build."); continue; } string assemblyFileName = Path.GetFileNameWithoutExtension (assemblyPath); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MetadataExtensions.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MetadataExtensions.cs index 7af06da33b8..bd259dc528c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MetadataExtensions.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MetadataExtensions.cs @@ -1,4 +1,5 @@ -using System.Reflection.Metadata; +using System; +using System.Reflection.Metadata; namespace Xamarin.Android.Tasks { @@ -22,5 +23,31 @@ public static CustomAttributeValue GetCustomAttributeArguments (this Cus { return attribute.DecodeValue (DummyCustomAttributeProvider.Instance); } + + /// + /// Returns the TargetFrameworkIdentifier of an assembly, or null if not found + /// + public static string GetTargetFrameworkIdentifier (this AssemblyDefinition assembly, MetadataReader reader) + { + foreach (var handle in assembly.GetCustomAttributes ()) { + var attribute = reader.GetCustomAttribute (handle); + var name = reader.GetCustomAttributeFullName (attribute); + if (name == "System.Runtime.Versioning.TargetFrameworkAttribute") { + var arguments = attribute.GetCustomAttributeArguments (); + foreach (var p in arguments.FixedArguments) { + // Of the form "MonoAndroid,Version=v8.1" + var value = p.Value?.ToString (); + if (!string.IsNullOrEmpty (value)) { + int commaIndex = value.IndexOf (",", StringComparison.Ordinal); + if (commaIndex != -1) { + return value.Substring (0, commaIndex); + } + } + } + return null; + } + } + return null; + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index 920a725d09c..19aae31f7cc 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -128,6 +128,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 7746e11c624..ac6952c0d2c 100755 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -58,6 +58,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + @@ -494,6 +495,12 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_ReferenceDependencyPaths Include="@(ReferenceDependencyPaths)" Condition="'$(AndroidApplication)' == '' Or !$(AndroidApplication)"/> + + + @@ -503,13 +510,13 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.