Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Xamarin.Android.Build.Tasks] use System.Reflection.Metadata in <ResolveAssemblies/> #2612

Merged
merged 2 commits into from
Jan 16, 2019
Merged
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
119 changes: 60 additions & 59 deletions src/Xamarin.Android.Build.Tasks/Tasks/ResolveAssemblies.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
// Copyright (C) 2011, Xamarin Inc.
// Copyright (C) 2010, Novell Inc.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Mono.Cecil;
using MonoDroid.Tuner;
using NuGet.Frameworks;
using NuGet.ProjectModel;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using Xamarin.Android.Tools;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.ProjectModel;

using Java.Interop.Tools.Cecil;

namespace Xamarin.Android.Tasks
{
Expand Down Expand Up @@ -62,7 +59,7 @@ public override bool Execute ()
Yield ();
try {
System.Threading.Tasks.Task.Run (() => {
using (var resolver = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: false)) {
using (var resolver = new MetadataResolver ()) {
Execute (resolver);
}
}, Token).ContinueWith (Complete);
Expand All @@ -72,14 +69,13 @@ public override bool Execute ()
}
}

void Execute (DirectoryAssemblyResolver resolver)
void Execute (MetadataResolver resolver)
{
foreach (var dir in ReferenceAssembliesDirectory.Split (new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
resolver.SearchDirectories.Add (dir);

var assemblies = new Dictionary<string, ITaskItem> ();
foreach (var dir in ReferenceAssembliesDirectory.Split (new char [] { ';' }, StringSplitOptions.RemoveEmptyEntries))
resolver.AddSearchDirectory (dir);

var topAssemblyReferences = new List<AssemblyDefinition> ();
var assemblies = new Dictionary<string, ITaskItem> (Assemblies.Length);
var topAssemblyReferences = new List<string> (Assemblies.Length);
var logger = new NuGetLogger((s) => {
LogDebugMessage ("{0}", s);
});
Expand All @@ -92,32 +88,28 @@ void Execute (DirectoryAssemblyResolver resolver)
try {
foreach (var assembly in Assemblies) {
var assembly_path = Path.GetDirectoryName (assembly.ItemSpec);

if (!resolver.SearchDirectories.Contains (assembly_path))
resolver.SearchDirectories.Add (assembly_path);
resolver.AddSearchDirectory (assembly_path);

// Add each user assembly and all referenced assemblies (recursive)
var assemblyDef = resolver.Load (assembly.ItemSpec);
if (assemblyDef == null)
throw new InvalidOperationException ("Failed to load assembly " + assembly.ItemSpec);
if (MonoAndroidHelper.IsReferenceAssembly (assemblyDef)) {
string resolved_assembly = resolver.Resolve (assembly.ItemSpec);
if (MonoAndroidHelper.IsReferenceAssembly (resolved_assembly)) {
// Resolve "runtime" library
var asmFullPath = Path.GetFullPath (assembly.ItemSpec);
if (lockFile != null)
assemblyDef = ResolveRuntimeAssemblyForReferenceAssembly (lockFile, resolver, asmFullPath);
if (lockFile == null || assemblyDef == null) {
LogCodedWarning ("XA0107", asmFullPath, 0, "Ignoring {0} as it is a Reference Assembly", asmFullPath);
resolved_assembly = ResolveRuntimeAssemblyForReferenceAssembly (lockFile, assembly.ItemSpec);
if (lockFile == null || resolved_assembly == null) {
LogCodedWarning ("XA0107", resolved_assembly, 0, "Ignoring {0} as it is a Reference Assembly", resolved_assembly);
continue;
}
}
topAssemblyReferences.Add (assemblyDef);
topAssemblyReferences.Add (resolved_assembly);
var taskItem = new TaskItem (assembly) {
ItemSpec = Path.GetFullPath (assemblyDef.MainModule.FileName),
ItemSpec = Path.GetFullPath (resolved_assembly),
};
if (string.IsNullOrEmpty (taskItem.GetMetadata ("ReferenceAssembly"))) {
taskItem.SetMetadata ("ReferenceAssembly", taskItem.ItemSpec);
}
assemblies [assemblyDef.Name.Name] = taskItem;
string assemblyName = Path.GetFileNameWithoutExtension (resolved_assembly);
assemblies [assemblyName] = taskItem;
}
} catch (Exception ex) {
LogError ("Exception while loading assemblies: {0}", ex);
Expand Down Expand Up @@ -171,7 +163,7 @@ void Execute (DirectoryAssemblyResolver resolver)
readonly Dictionary<string, int> api_levels = new Dictionary<string, int> ();
int indent = 2;

AssemblyDefinition ResolveRuntimeAssemblyForReferenceAssembly (LockFile lockFile, DirectoryAssemblyResolver resolver, string assemblyPath)
string ResolveRuntimeAssemblyForReferenceAssembly (LockFile lockFile, string assemblyPath)
{
if (string.IsNullOrEmpty(TargetMoniker))
return null;
Expand Down Expand Up @@ -200,16 +192,16 @@ AssemblyDefinition ResolveRuntimeAssemblyForReferenceAssembly (LockFile lockFile
path = Path.Combine (folder.Path, libraryPath.Path, runtime.Path).Replace('/', Path.DirectorySeparatorChar);
if (!File.Exists (path))
continue;
LogDebugMessage ($"Attempting to load {path}");
return resolver.Load (path, forceLoad: true);
return path;
}
return null;
}

void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<string, ITaskItem> assemblies, AssemblyDefinition assembly, List<string> resolutionPath)
void AddAssemblyReferences (MetadataResolver resolver, Dictionary<string, ITaskItem> assemblies, string assemblyPath, List<string> resolutionPath)
{
var assemblyName = assembly.Name.Name;
var fullPath = Path.GetFullPath (assembly.MainModule.FileName);
var reader = resolver.GetAssemblyReader (assemblyPath);
var assembly = reader.GetAssemblyDefinition ();
var assemblyName = reader.GetString (assembly.Name);

// Don't repeat assemblies we've already done
bool topLevel = resolutionPath == null;
Expand All @@ -219,22 +211,23 @@ void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<strin
if (resolutionPath == null)
resolutionPath = new List<string>();

CheckAssemblyAttributes (assembly);
CheckAssemblyAttributes (assembly, reader);

LogMessage ("{0}Adding assembly reference for {1}, recursively...", new string (' ', indent), assembly.Name);
resolutionPath.Add (assembly.Name.Name);
LogMessage ("{0}Adding assembly reference for {1}, recursively...", new string (' ', indent), assemblyName);
resolutionPath.Add (assemblyName);
indent += 2;

// Add this assembly
if (!topLevel) {
assemblies [assemblyName] = CreateAssemblyTaskItem (fullPath);
assemblies [assemblyName] = CreateAssemblyTaskItem (Path.GetFullPath (assemblyPath));
}

// Recurse into each referenced assembly
foreach (AssemblyNameReference reference in assembly.MainModule.AssemblyReferences) {
AssemblyDefinition reference_assembly;
foreach (var handle in reader.AssemblyReferences) {
var reference = reader.GetAssemblyReference (handle);
string reference_assembly;
try {
reference_assembly = resolver.Resolve (reference);
reference_assembly = resolver.Resolve (reader.GetString (reference.Name));
} catch (FileNotFoundException ex) {
var references = new StringBuilder ();
for (int i = 0; i < resolutionPath.Count; i++) {
Expand All @@ -245,7 +238,10 @@ void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<strin
references.Append ('`');
}

string missingAssembly = Path.GetFileNameWithoutExtension (ex.FileName);
string missingAssembly = ex.FileName;
if (missingAssembly.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) {
missingAssembly = Path.GetFileNameWithoutExtension (missingAssembly);
}
string message = $"Can not resolve reference: `{missingAssembly}`, referenced by {references}.";
if (MonoAndroidHelper.IsFrameworkAssembly (ex.FileName)) {
LogCodedError ("XA2002", $"{message} Perhaps it doesn't exist in the Mono for Android profile?");
Expand All @@ -261,25 +257,30 @@ void AddAssemblyReferences (DirectoryAssemblyResolver resolver, Dictionary<strin
resolutionPath.RemoveAt (resolutionPath.Count - 1);
}

void CheckAssemblyAttributes (AssemblyDefinition assembly)
void CheckAssemblyAttributes (AssemblyDefinition assembly, MetadataReader reader)
{
foreach (var att in assembly.CustomAttributes) {
switch (att.AttributeType.FullName) {
foreach (var handle in assembly.GetCustomAttributes ()) {
var attribute = reader.GetCustomAttribute (handle);
switch (reader.GetCustomAttributeFullName (attribute)) {
case "Java.Interop.DoNotPackageAttribute": {
string file = (string)att.ConstructorArguments.First ().Value;
if (string.IsNullOrWhiteSpace (file))
LogError ("In referenced assembly {0}, Java.Interop.DoNotPackageAttribute requires non-null file name.", assembly.FullName);
do_not_package_atts.Add (Path.GetFileName (file));
var arguments = attribute.GetCustomAttributeArguments ();
if (arguments.FixedArguments.Length > 0) {
string file = arguments.FixedArguments [0].Value?.ToString ();
if (string.IsNullOrWhiteSpace (file))
LogError ("In referenced assembly {0}, Java.Interop.DoNotPackageAttribute requires non-null file name.", assembly.GetAssemblyName ().FullName);
do_not_package_atts.Add (Path.GetFileName (file));
}
}
break;
case "System.Runtime.Versioning.TargetFrameworkAttribute": {
foreach (var p in att.ConstructorArguments) {
var value = p.Value.ToString ();
if (value.StartsWith ("MonoAndroid")) {
var arguments = attribute.GetCustomAttributeArguments ();
foreach (var p in arguments.FixedArguments) {
var value = p.Value?.ToString ();
if (value != null && value.StartsWith ("MonoAndroid", StringComparison.Ordinal)) {
var values = value.Split ('=');
var apiLevel = MonoAndroidHelper.SupportedVersions.GetApiLevelFromFrameworkVersion (values [1]);
if (apiLevel != null) {
var assemblyName = assembly.Name.Name;
var assemblyName = reader.GetString (assembly.Name);
Log.LogDebugMessage ("{0}={1}", assemblyName, apiLevel);
api_levels [assemblyName] = apiLevel.Value;
}
Expand All @@ -305,7 +306,7 @@ static LinkModes ParseLinkMode (string linkmode)
return mode;
}

void AddI18nAssemblies (DirectoryAssemblyResolver resolver, Dictionary<string, ITaskItem> assemblies)
void AddI18nAssemblies (MetadataResolver resolver, Dictionary<string, ITaskItem> assemblies)
{
var i18n = Linker.ParseI18nAssemblies (I18nAssemblies);
var link = ParseLinkMode (LinkMode);
Expand All @@ -332,10 +333,10 @@ void AddI18nAssemblies (DirectoryAssemblyResolver resolver, Dictionary<string, I
ResolveI18nAssembly (resolver, "I18N.West", assemblies);
}

void ResolveI18nAssembly (DirectoryAssemblyResolver resolver, string name, Dictionary<string, ITaskItem> assemblies)
void ResolveI18nAssembly (MetadataResolver resolver, string name, Dictionary<string, ITaskItem> assemblies)
{
var assembly = resolver.Resolve (AssemblyNameReference.Parse (name));
var assemblyFullPath = Path.GetFullPath (assembly.MainModule.FileName);
var assembly = resolver.Resolve (name);
var assemblyFullPath = Path.GetFullPath (assembly);
assemblies [name] = CreateAssemblyTaskItem (assemblyFullPath);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Reflection.Metadata;

namespace Xamarin.Android.Tasks
{
/// <summary>
/// A helper type for System.Reflection.Metadata. Getting the value of custom attribute arguments is a bit convoluted, if you merely want the values.
///
/// This interface allows usage such as:
/// CustomAttribute attribute = reader.GetCustomAttribute (handle);
/// CustomAttributeValue<object> decoded = attribute.DecodeValue (DummyCustomAttributeProvider.Instance);
/// Or better yet, used via the extension method:
/// CustomAttributeValue<object> decoded = attribute.GetCustomAttributeArguments ();
/// </summary>
public class DummyCustomAttributeProvider : ICustomAttributeTypeProvider<object>
{
public static readonly DummyCustomAttributeProvider Instance = new DummyCustomAttributeProvider ();

public object GetPrimitiveType (PrimitiveTypeCode typeCode) => null;

public object GetSystemType () => null;

public object GetSZArrayType (object elementType) => null;

public object GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => null;

public object GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => null;

public object GetTypeFromSerializedName (string name) => null;

public PrimitiveTypeCode GetUnderlyingEnumType (object type) => default (PrimitiveTypeCode);

public bool IsSystemType (object type) => false;
}
}
26 changes: 26 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Utilities/MetadataExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Reflection.Metadata;

namespace Xamarin.Android.Tasks
{
public static class MetadataExtensions
{
public static string GetCustomAttributeFullName (this MetadataReader reader, CustomAttribute attribute)
{
if (attribute.Constructor.Kind == HandleKind.MemberReference) {
var ctor = reader.GetMemberReference ((MemberReferenceHandle)attribute.Constructor);
var type = reader.GetTypeReference ((TypeReferenceHandle)ctor.Parent);
return reader.GetString (type.Namespace) + "." + reader.GetString (type.Name);
} else if (attribute.Constructor.Kind == HandleKind.MethodDefinition) {
var ctor = reader.GetMethodDefinition ((MethodDefinitionHandle)attribute.Constructor);
var type = reader.GetTypeDefinition (ctor.GetDeclaringType ());
return reader.GetString (type.Namespace) + "." + reader.GetString (type.Name);
}
return null;
}

public static CustomAttributeValue<object> GetCustomAttributeArguments (this CustomAttribute attribute)
{
return attribute.DecodeValue (DummyCustomAttributeProvider.Instance);
}
}
}
60 changes: 60 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Utilities/MetadataResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

namespace Xamarin.Android.Tasks
{
/// <summary>
/// A replacement for DirectoryAssemblyResolver, using System.Reflection.Metadata
/// </summary>
public class MetadataResolver : IDisposable
{
readonly Dictionary<string, PEReader> cache = new Dictionary<string, PEReader> ();
readonly List<string> searchDirectories = new List<string> ();

public MetadataReader GetAssemblyReader (string assemblyName)
{
var key = Path.GetFileNameWithoutExtension (assemblyName);
if (!cache.TryGetValue (key, out PEReader reader)) {
var assemblyPath = Resolve (assemblyName);
cache.Add (key, reader = new PEReader (File.OpenRead (assemblyPath)));
}
return reader.GetMetadataReader ();
}

public void AddSearchDirectory (string directory)
{
directory = Path.GetFullPath (directory);
if (!searchDirectories.Contains (directory))
searchDirectories.Add (directory);
}

public string Resolve (string assemblyName)
{
string assemblyPath = assemblyName;
if (!assemblyPath.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) {
assemblyPath += ".dll";
Copy link
Member

Choose a reason for hiding this comment

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

Should this also support .exe suffixes?

Copy link
Member Author

Choose a reason for hiding this comment

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

Since e390702 we dropped support for referencing exe's at the MSBuild level. I didn't add support for it here, but if there is some scenario you think we'll hit, I can add it.

Offhand I can't think of a case where a .dll would reference a random .exe?

}
if (File.Exists (assemblyPath)) {
return assemblyPath;
}
foreach (var dir in searchDirectories) {
var path = Path.Combine (dir, assemblyPath);
if (File.Exists (path))
return path;
}

throw new FileNotFoundException ($"Could not load assembly '{assemblyName}'.", assemblyName);
}

public void Dispose ()
{
foreach (var provider in cache.Values) {
provider.Dispose ();
}
cache.Clear ();
}
}
}
Loading