Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] better ResolveAssemblies errors (#1551)
Browse files Browse the repository at this point in the history
Fixes: https://bugzilla.xamarin.com/show_bug.cgi?id=7505
Fixes: #1532

In discussion around this issue, we realized the error message given
by the `<ResolveAssemblies/>` MSBuild task is confusing.

Currently users are currently getting an *unhandled exception* error:

	Exception while loading assemblies: System.IO.FileNotFoundException: Could not load assembly 'A'.
	Perhaps it doesn't exist in the Mono for Android profile?
	File name: 'A.dll'


This message makes sense for `System.*` or framework assemblies.

We were thinking in some cases we could give an error such as:

	Could not find assembly A, referenced by B. Please add a NuGet package or assembly reference for A, or remove the reference to B.

Additionally we should list an assembly "resolution path", so we can
easily figure out where the missing assembly came from.

In order to make this work:

  - Keep a `List<string>` to store the name of each assembly
  - Catch `FileNotFoundException` specifically
  - Check `MonoAndroidHelper.IsFrameworkAssembly()` to decide if we
    show the old message, or the new/improved message
  - We have to track the `resolutionPath` variable as we go, passing
    it through recursively as well as removing items as we "un-indent"

The full error message will now read:

	error XA2002: Can not resolve reference `Microsoft.Azure.EventHubs`, referenced by `MyLibrary`.
	Please add a NuGet package or assembly reference for `Microsoft.Azure.EventHubs`, or remove the reference to `MyLibrary`.

If there are multiple assemblies involved you should see something like:

	error XA2002: Can not resolve reference `Microsoft.Azure.Amqp`, referenced by `MyLibrary` > `Microsoft.Azure.EventHubs`.
	Please add a NuGet package or assembly reference for `Microsoft.Azure.Amqp`, or remove the reference to `MyLibrary`.

Here, `MyLibrary.dll` references `Microsoft.Azure.EventHubs.dll`,
which references `Microsoft.Azure.Amqp.dll`, which cannot be found.

Other changes:

  - Added a unit test specifically for the NuGet used in #1532.
    Tested the scenario where a library project references the broken
    NuGet, add the `@(ProjectReference)` to the app, then get a
    different error message.
  - `StringAssertEx` should give some reasonable default assertion
    message if `message` is not supplied
  - `@(PackageReference)` elements had this odd `xmlns=""` attribute,
    so I included the msbuild XML namespace to fix this  
  - Found an issue in `xabuild.exe` where the NuGet targets were not
    setup. Most cases of `/t:Restore;Build` seems like they would
    fail on macOS.
  • Loading branch information
jonathanpeppers authored and jonpryor committed Apr 16, 2018
1 parent 1ab9c5e commit 38c365b
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 5 deletions.
38 changes: 34 additions & 4 deletions src/Xamarin.Android.Build.Tasks/Tasks/ResolveAssemblies.cs
Expand Up @@ -9,6 +9,7 @@
using Mono.Cecil;
using MonoDroid.Tuner;
using System.IO;
using System.Text;
using Xamarin.Android.Tools;
using NuGet.Common;
using NuGet.Frameworks;
Expand Down Expand Up @@ -120,7 +121,7 @@ void Execute (DirectoryAssemblyResolver resolver)
}
try {
foreach (var assembly in topAssemblyReferences)
AddAssemblyReferences (resolver, assemblies, assembly, true);
AddAssemblyReferences (resolver, assemblies, assembly, null);
} catch (Exception ex) {
LogError ("Exception while loading assemblies: {0}", ex);
return;
Expand Down Expand Up @@ -184,14 +185,18 @@ AssemblyDefinition ResolveRuntimeAssemblyForReferenceAssembly (LockFile lockFile
return null;
}

void AddAssemblyReferences (DirectoryAssemblyResolver resolver, ICollection<string> assemblies, AssemblyDefinition assembly, bool topLevel)
void AddAssemblyReferences (DirectoryAssemblyResolver resolver, ICollection<string> assemblies, AssemblyDefinition assembly, List<string> resolutionPath)
{
var fqname = assembly.MainModule.FullyQualifiedName;
var fullPath = Path.GetFullPath (fqname);

// Don't repeat assemblies we've already done
bool topLevel = resolutionPath == null;
if (!topLevel && assemblies.Contains (fullPath))
return;

if (resolutionPath == null)
resolutionPath = new List<string>();

foreach (var att in assembly.CustomAttributes.Where (a => a.AttributeType.FullName == "Java.Interop.DoNotPackageAttribute")) {
string file = (string) att.ConstructorArguments.First ().Value;
Expand All @@ -201,17 +206,42 @@ void AddAssemblyReferences (DirectoryAssemblyResolver resolver, ICollection<stri
}

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

// Add this assembly
if (!topLevel && assemblies.All (a => new AssemblyNameDefinition (a, null).Name != assembly.Name.Name))
assemblies.Add (fullPath);

// Recurse into each referenced assembly
foreach (AssemblyNameReference reference in assembly.MainModule.AssemblyReferences) {
var reference_assembly = resolver.Resolve (reference);
AddAssemblyReferences (resolver, assemblies, reference_assembly, false);
AssemblyDefinition reference_assembly;
try {
reference_assembly = resolver.Resolve (reference);
} catch (FileNotFoundException ex) {
var references = new StringBuilder ();
for (int i = 0; i < resolutionPath.Count; i++) {
if (i != 0)
references.Append (" > ");
references.Append ('`');
references.Append (resolutionPath [i]);
references.Append ('`');
}

string missingAssembly = Path.GetFileNameWithoutExtension (ex.FileName);
string message = $"Can not resolve reference: `{missingAssembly}`, referenced by {references}.";
if (MonoAndroidHelper.IsFrameworkAssembly (ex.FileName)) {
LogError ("XA2002", $"{message} Perhaps it doesn't exist in the Mono for Android profile?");
} else {
LogError ("XA2002", $"{message} Please add a NuGet package or assembly reference for `{missingAssembly}`, or remove the reference to `{resolutionPath [0]}`.");
}
return;
}
AddAssemblyReferences (resolver, assemblies, reference_assembly, resolutionPath);
}

indent -= 2;
resolutionPath.RemoveAt (resolutionPath.Count - 1);
}

static LinkModes ParseLinkMode (string linkmode)
Expand Down
Expand Up @@ -1855,6 +1855,81 @@ public void BuildReleaseApplicationWithNugetPackages ()
}
}

[Test]
public void BuildWithResolveAssembliesFailure ([Values (true, false)] bool usePackageReference)
{
var path = Path.Combine ("temp", TestContext.CurrentContext.Test.Name);
var app = new XamarinAndroidApplicationProject {
ProjectName = "MyApp",
Sources = {
new BuildItem.Source ("Foo.cs") {
TextContent = () => "public class Foo : Bar { }"
},
}
};
var lib = new XamarinAndroidLibraryProject {
ProjectName = "MyLibrary",
Sources = {
new BuildItem.Source ("Bar.cs") {
TextContent = () => "public class Bar { void EventHubs () { Microsoft.Azure.EventHubs.EventHubClient c; } }"
},
}
};
if (usePackageReference)
lib.PackageReferences.Add (KnownPackages.Microsoft_Azure_EventHubs);
else
lib.Packages.Add (KnownPackages.Microsoft_Azure_EventHubs);
app.References.Add (new BuildItem.ProjectReference ($"..\\{lib.ProjectName}\\{lib.ProjectName}.csproj", lib.ProjectName, lib.ProjectGuid));

using (var libBuilder = CreateDllBuilder (Path.Combine (path, lib.ProjectName), false))
using (var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName))) {
if (usePackageReference) {
//NOTE: <PackageReference /> not working under xbuild
if (!libBuilder.RunningMSBuild)
Assert.Ignore ("This test requires MSBuild.");

libBuilder.Target = "Restore";
Assert.IsTrue (libBuilder.Build (lib), "Restore should have succeeded.");
libBuilder.Target = "Build";
}
Assert.IsTrue (libBuilder.Build (lib), "Build should have succeeded.");

appBuilder.ThrowOnBuildFailure = false;
Assert.IsFalse (appBuilder.Build (app), "Build should have failed.");

const string error = "error XA2002: Can not resolve reference:";

//NOTE: we get a different message when using <PackageReference /> due to automatically getting the Microsoft.Azure.Amqp (and many other) transient dependencies
if (usePackageReference) {
Assert.IsTrue (appBuilder.LastBuildOutput.ContainsText ($"{error} `Microsoft.Azure.EventHubs`, referenced by `MyLibrary`. Please add a NuGet package or assembly reference for `Microsoft.Azure.EventHubs`, or remove the reference to `MyLibrary`."),
$"Should recieve '{error}' regarding `Microsoft.Azure.EventHubs`!");
} else {
Assert.IsTrue (appBuilder.LastBuildOutput.ContainsText ($"{error} `Microsoft.Azure.Amqp`, referenced by `MyLibrary` > `Microsoft.Azure.EventHubs`. Please add a NuGet package or assembly reference for `Microsoft.Azure.Amqp`, or remove the reference to `MyLibrary`."),
$"Should recieve '{error}' regarding `Microsoft.Azure.Amqp`!");
}

//Now add the PackageReference to the app to see a different error message
if (usePackageReference) {
app.PackageReferences.Add (KnownPackages.Microsoft_Azure_EventHubs);
appBuilder.Target = "Restore";
Assert.IsTrue (appBuilder.Build (app), "Restore should have succeeded.");
appBuilder.Target = "Build";
} else {
app.Packages.Add (KnownPackages.Microsoft_Azure_EventHubs);
}
Assert.IsFalse (appBuilder.Build (app), "Build should have failed.");

//NOTE: we get a different message when using <PackageReference /> due to automatically getting the Microsoft.Azure.Amqp (and many other) transient dependencies
if (usePackageReference) {
Assert.IsTrue (appBuilder.LastBuildOutput.ContainsText ($"{error} `Microsoft.Azure.Services.AppAuthentication`, referenced by `Microsoft.Azure.EventHubs`. Please add a NuGet package or assembly reference for `Microsoft.Azure.Services.AppAuthentication`, or remove the reference to `Microsoft.Azure.EventHubs`."),
$"Should recieve '{error}' regarding `Microsoft.Azure.Services.AppAuthentication`!");
} else {
Assert.IsTrue (appBuilder.LastBuildOutput.ContainsText ($"{error} `Microsoft.Azure.Amqp`, referenced by `Microsoft.Azure.EventHubs`. Please add a NuGet package or assembly reference for `Microsoft.Azure.Amqp`, or remove the reference to `Microsoft.Azure.EventHubs`."),
$"Should recieve '{error}' regarding `Microsoft.Azure.Services.Amqp`!");
}
}
}

static object [] TlsProviderTestCases =
{
// androidTlsProvider, isRelease, extpected
Expand Down
Expand Up @@ -49,7 +49,7 @@ public static void Contains (string text, IEnumerable<string> collection, string
return;
}
}
Assert.Fail (message);
Assert.Fail (message ?? $"String did not contain '{text}'!");
}

public static bool ContainsText (this IEnumerable<string> collection, string expected) {
Expand Down
Expand Up @@ -385,6 +385,16 @@ public static class KnownPackages
}
}
};
public static Package Microsoft_Azure_EventHubs = new Package {
Id = "Microsoft.Azure.EventHubs",
Version = "2.0.0",
TargetFramework = "netstandard2.0",
References = {
new BuildItem.Reference("Microsoft.Azure.EventHubs") {
MetadataValues = "HintPath=..\\packages\\Microsoft.Azure.EventHubs.2.0.0\\lib\\netstandard2.0\\Microsoft.Azure.EventHubs.dll"
}
}
};
}
}

Expand Up @@ -135,7 +135,10 @@ public override string SaveProject ()
var referenceGroup = p.Elements ().FirstOrDefault (x => x.Name.LocalName == "ItemGroup" && x.HasElements && x.Elements ().Any (e => e.Name.LocalName == "Reference"));
if (referenceGroup != null) {
foreach (var pr in PackageReferences) {
//NOTE: without the namespace it puts xmlns=""
XNamespace ns = "http://schemas.microsoft.com/developer/msbuild/2003";
var e = XElement.Parse ($"<PackageReference Include=\"{pr.Id}\" Version=\"{pr.Version}\"/>");
e.Name = ns + e.Name.LocalName;
referenceGroup.Add (e);
}
sw = new StringWriter ();
Expand Down
6 changes: 6 additions & 0 deletions tools/xabuild/XABuildPaths.cs
Expand Up @@ -148,6 +148,12 @@ public XABuildPaths ()
ProjectImportSearchPaths = new [] { MSBuildPath, Path.Combine (mono, "xbuild"), Path.Combine (monoExternal, "xbuild") };
SystemProfiles = Path.Combine (mono, "xbuild-frameworks");
SearchPathsOS = IsMacOS ? "osx" : "unix";

string nuget = Path.Combine(mono, "xbuild", "Microsoft", "NuGet");
if (Directory.Exists(nuget)) {
NuGetTargets = Path.Combine (nuget, "Microsoft.NuGet.targets");
NuGetProps = Path.Combine (nuget, "Microsoft.NuGet.props");
}
if (!string.IsNullOrEmpty (DotNetSdkPath)) {
NuGetRestoreTargets = Path.Combine (DotNetSdkPath, "..", "NuGet.targets");
}
Expand Down

0 comments on commit 38c365b

Please sign in to comment.