Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] <FilterAssemblies/> support missing TFI
Browse files Browse the repository at this point in the history
Context: https://www.nuget.org/packages/Refractored.Controls.CircleImageView/
Context: https://github.com/Azure-Samples/MyDriving

The MyDriving sample app currently fails to build on master with:

    Resources/layout/fragment_profile.axml(2): error APT0000: attribute civ_border_width (aka com.microsoft.mydriving:civ_border_width) not found.

The failure happens with both `aapt` and `aapt2`.

This layout is using a custom view such as:

    <?xml version="1.0" encoding="utf-8"?>
    <ScrollView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:local="http://schemas.android.com/apk/res-auto">
        ...
        <refractored.controls.CircleImageView local:civ_border_width="0dp" />
        ...
    </ScrollView>

This comes from the `Refractored.Controls.CircleImageView` NuGet
package.

In 5ec3e3a, I added a `<FilterAssemblies/>` MSBuild task that appears
to be to blame. It is not returning
`Refractored.Controls.CircleImageView.dll`, but it needs to!

`Refractored.Controls.CircleImageView.dll` has no `[assembly:
System.Runtime.Versioning.TargetFrameworkAttribute]`...

    // C:\src\des\MyDriving\packages\Refractored.Controls.CircleImageView.1.0.1\lib\MonoAndroid10\Refractored.Controls.CircleImageView.dll
    // Refractored.Controls.CircleImageView, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    // Global type: <Module>
    // Architecture: x86
    // Runtime: v4.0.30319
    // Hash algorithm: SHA1

    using Android.Runtime;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Security;
    using System.Security.Permissions;

    [assembly: AssemblyTitle("Refractored.Controls.CircleImageView")]
    [assembly: AssemblyDescription("")]
    [assembly: AssemblyConfiguration("")]
    [assembly: AssemblyCompany("")]
    [assembly: AssemblyProduct("")]
    [assembly: AssemblyCopyright("2015 Refractored LLC/James Montemagno")]
    [assembly: AssemblyTrademark("")]
    [assembly: NamespaceMapping(Java = "de.hdodenhof.circleimageview", Managed = "Refractored.Controls")]
    [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
    [assembly: SecurityPermission(8, SkipVerification = true)]
    [assembly: AssemblyVersion("1.0.0.0")]
    [module: UnverifiableCode]

It is indeed a `MonoAndroid` assembly, since it references
`Mono.Android.dll`. It is weird, though...

~~ What should we do? ~~

One idea is to look for `Mono.Android.dll` references in each
assembly *instead* of `TargetFrameworkIdentifier`.

Looking at the assemblies in Xamarin.Forms, there is a complication to
this:

* `Xamarin.Forms.Core.dll` (a NetStandard library) references
  * `Xamarin.Forms.Platform.dll` (a MonoAndroid library?) references
    * `Xamarin.Forms.Platform.Android.dll`

But `Xamarin.Forms.Platform.dll` does not reference
`Mono.Android.dll`?

So then should we "recursively" look for any reference to
`Mono.Android.dll` in a given dependency tree?

I don't think so? This would include more assemblies than we want...
In the above example `Xamarin.Forms.Core.dll` would get counted as a
"MonoAndroid" assembly.

~~ Conclusion ~~

I think we should stick with `TargetFrameworkIdentifier`, and in the
rare case it is missing look for a `Mono.Android.dll` reference for
the single assembly. This seems like it is going to cover all cases to
me, and we still will get good performance.

So we will cover:

* `Xamarin.Forms.Platform.dll` counted as a "MonoAndroid" assembly
  since it has a `TargetFrameworkIdentifier` (but no reference).
* `Refractored.Controls.CircleImageView.dll` counted as a
  "MonoAndroid" assembly. It has no `TargetFrameworkIdentifier, but
  has a reference to `Mono.Android.dll`.

Changes include:

* `<FilterAssemblies/>` needs to also check for an assembly reference
  to `Mono.Android` as a fallback.
* `<ResolveAssemblies/>` now adds a `%(MonoAndroidReference)=True`
  item metadata.
* When creating the `@(_ResolvedUserMonoAndroidAssemblies)` item
  group, we also check for `%(MonoAndroidReference)=True`.

I added a few tests to verify these scenarios.
  • Loading branch information
jonathanpeppers committed Apr 12, 2019
1 parent 28e1f93 commit 71623cc
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 8 deletions.
26 changes: 24 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Tasks/FilterAssemblies.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;
Expand All @@ -8,13 +9,21 @@
namespace Xamarin.Android.Tasks
{
/// <summary>
/// Filters a set of assemblies based on a given TargetFrameworkIdentifier
/// Filters a set of assemblies based on a given TargetFrameworkIdentifier or FallbackReference
/// </summary>
public class FilterAssemblies : Task
{
/// <summary>
/// The MonoAndroid portion of [assembly: System.Runtime.Versioning.TargetFramework("MonoAndroid,v9.0")]
/// </summary>
[Required]
public string TargetFrameworkIdentifier { get; set; }

/// <summary>
/// If TargetFrameworkIdentifier is missing, we can look for Mono.Android.dll references instead
/// </summary>
public string FallbackReference { get; set; }

[Required]
public bool DesignTimeBuild { get; set; }

Expand All @@ -38,8 +47,21 @@ public override bool Execute ()
var reader = pe.GetMetadataReader ();
var assemblyDefinition = reader.GetAssemblyDefinition ();
var targetFrameworkIdentifier = assemblyDefinition.GetTargetFrameworkIdentifier (reader);
if (targetFrameworkIdentifier == TargetFrameworkIdentifier) {
if (string.Compare (targetFrameworkIdentifier, TargetFrameworkIdentifier, StringComparison.OrdinalIgnoreCase) == 0) {
output.Add (assemblyItem);
continue;
}
// Fallback to looking at references
if (string.IsNullOrEmpty (targetFrameworkIdentifier) && !string.IsNullOrEmpty (FallbackReference)) {
Log.LogDebugMessage ($"Checking references for: {assemblyItem.ItemSpec}");
foreach (var handle in reader.AssemblyReferences) {
var reference = reader.GetAssemblyReference (handle);
var name = reader.GetString (reference.Name);
if (FallbackReference == name) {
output.Add (assemblyItem);
break;
}
}
}
}
}
Expand Down
16 changes: 11 additions & 5 deletions src/Xamarin.Android.Build.Tasks/Tasks/ResolveAssemblies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,22 +223,28 @@ void AddAssemblyReferences (MetadataResolver resolver, Dictionary<string, ITaskI
indent += 2;

// Add this assembly
ITaskItem assemblyItem = null;
if (topLevel) {
if (!string.IsNullOrEmpty (targetFrameworkIdentifier) && assemblies.TryGetValue (assemblyName, out ITaskItem taskItem)) {
if (string.IsNullOrEmpty (taskItem.GetMetadata ("TargetFrameworkIdentifier"))) {
taskItem.SetMetadata ("TargetFrameworkIdentifier", targetFrameworkIdentifier);
if (assemblies.TryGetValue (assemblyName, out assemblyItem)) {
if (!string.IsNullOrEmpty (targetFrameworkIdentifier) && string.IsNullOrEmpty (assemblyItem.GetMetadata ("TargetFrameworkIdentifier"))) {
assemblyItem.SetMetadata ("TargetFrameworkIdentifier", targetFrameworkIdentifier);
}
}
} else {
assemblies [assemblyName] = CreateAssemblyTaskItem (assemblyPath, targetFrameworkIdentifier);
assemblies [assemblyName] =
assemblyItem = CreateAssemblyTaskItem (assemblyPath, targetFrameworkIdentifier);
}

// Recurse into each referenced assembly
foreach (var handle in reader.AssemblyReferences) {
var reference = reader.GetAssemblyReference (handle);
string reference_assembly;
try {
reference_assembly = resolver.Resolve (reader.GetString (reference.Name));
var referenceName = reader.GetString (reference.Name);
if (assemblyItem != null && referenceName == "Mono.Android") {
assemblyItem.SetMetadata ("HasMonoAndroidReference", "True");
}
reference_assembly = resolver.Resolve (referenceName);
} catch (FileNotFoundException ex) {
var references = new StringBuilder ();
for (int i = 0; i < resolutionPath.Count; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3636,6 +3636,36 @@ public void CompilerErrorShouldNotRunLinkAssemblies ()
Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, "The \"LinkAssemblies\" task failed unexpectedly"), "The LinkAssemblies MSBuild task should not run!");
}
}

/// <summary>
/// This assembly weirdly has no [assembly: System.Runtime.Versioning.TargetFrameworkAttribute()], at all...
/// </summary>
[Test]
public void AssemblyWithMissingTargetFramework ()
{
var proj = new XamarinFormsAndroidApplicationProject {
AndroidResources = {
new AndroidItem.AndroidResource ("Resources\\layout\\test.axml") {
TextContent = () =>
@"<?xml version=""1.0"" encoding=""utf-8""?>
<ScrollView
xmlns:android=""http://schemas.android.com/apk/res/android""
xmlns:local=""http://schemas.android.com/apk/res-auto"">
<refractored.controls.CircleImageView local:civ_border_width=""0dp"" />
</ScrollView>"
}
}
};
proj.PackageReferences.Add (KnownPackages.CircleImageView);
using (var b = CreateApkBuilder (Path.Combine ("temp", TestName))) {
Assert.IsTrue (b.Build (proj), "build should have succeeded.");

// We should have a java stub
var intermediate = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath);
var javaStub = Path.Combine (intermediate, "android", "src", "md54908d67eb9afef4acc92753cc61471e9", "CircleImageView.java");
FileAssert.Exists (javaStub);
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Xamarin.Android.Tasks;
using Xamarin.Tools.Zip;
using TaskItem = Microsoft.Build.Utilities.TaskItem;

namespace Xamarin.Android.Build.Tests
{
[TestFixture]
public class FilterAssembliesTests : BaseTest
{
HttpClient httpClient = new HttpClient ();
string tempDirectory;

[SetUp]
public void Setup ()
{
tempDirectory = Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ());
Directory.CreateDirectory (tempDirectory);
}

[TearDown]
public void TearDown ()
{
Directory.Delete (tempDirectory, recursive: true);
}

async Task<string> DownloadFromNuGet (string url)
{
var response = await httpClient.GetAsync (url);
response.EnsureSuccessStatusCode ();
var temp = Path.Combine (tempDirectory, Path.GetRandomFileName ());
using (var httpStream = await response.Content.ReadAsStreamAsync ())
using (var fileStream = File.Create (temp)) {
await httpStream.CopyToAsync (fileStream);
}
return temp;
}

async Task<string []> GetAssembliesFromNuGet (string url, string path)
{
var assemblies = new List<string> ();
var nuget = await DownloadFromNuGet (url);
using (var zip = ZipArchive.Open (nuget, FileMode.Open)) {
foreach (var entry in zip) {
if (entry.FullName.StartsWith (path, StringComparison.OrdinalIgnoreCase) &&
entry.FullName.EndsWith (".dll", StringComparison.OrdinalIgnoreCase)) {
var temp = Path.Combine (tempDirectory, Path.GetFileName (entry.NativeFullName));
assemblies.Add (temp);
using (var fileStream = File.Create (temp)) {
entry.Extract (fileStream);
}
}
}
}
return assemblies.ToArray ();
}

string [] Run (params string [] assemblies)
{
var task = new FilterAssemblies {
BuildEngine = new MockBuildEngine (TestContext.Out),
TargetFrameworkIdentifier = "MonoAndroid",
FallbackReference = "Mono.Android",
InputAssemblies = assemblies.Select (a => new TaskItem (a)).ToArray (),
};
Assert.IsTrue (task.Execute (), "task.Execute() should have succeeded.");
return task.OutputAssemblies.Select (a => Path.GetFileName (a.ItemSpec)).ToArray ();
}

[Test]
public async Task CircleImageView ()
{
var assemblies = await GetAssembliesFromNuGet (
"https://www.nuget.org/api/v2/package/Refractored.Controls.CircleImageView/1.0.1",
"lib/MonoAndroid10/");
var actual = Run (assemblies);
var expected = new [] { "Refractored.Controls.CircleImageView.dll" };
CollectionAssert.AreEqual (expected, actual);
}

[Test]
public async Task XamarinForms ()
{
var assemblies = await GetAssembliesFromNuGet (
"https://www.nuget.org/api/v2/package/Xamarin.Forms/3.6.0.220655",
"lib/MonoAndroid90/");
var actual = Run (assemblies);
var expected = new [] {
"FormsViewGroup.dll",
"Xamarin.Forms.Platform.Android.dll",
"Xamarin.Forms.Platform.dll",
};
CollectionAssert.AreEqual (expected, actual);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="nunit.framework">
Expand Down Expand Up @@ -97,6 +98,7 @@
<Compile Include="ManagedResourceParserTests.cs" />
<Compile Include="Tasks\BundleToolTests.cs" />
<Compile Include="Tasks\CopyResourceTests.cs" />
<Compile Include="Tasks\FilterAssembliesTests.cs" />
<Compile Include="Tasks\GenerateLibraryResourcesTests.cs" />
<Compile Include="Tasks\KeyToolTests.cs" />
<Compile Include="Aapt2Tests.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,16 @@ public static class KnownPackages
}
},
};
public static Package CircleImageView = new Package {
Id = "Refractored.Controls.CircleImageView",
Version = "1.0.1",
TargetFramework = "MonoAndroid10",
References = {
new BuildItem.Reference ("Refractored.Controls.CircleImageView") {
MetadataValues = "HintPath=..\\packages\\Refractored.Controls.CircleImageView.1.0.1\\lib\\MonoAndroid10\\Refractored.Controls.CircleImageView.dll"
}
},
};
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
<FilterAssemblies
DesignTimeBuild="$(DesignTimeBuild)"
TargetFrameworkIdentifier="MonoAndroid"
FallbackReference="Mono.Android"
InputAssemblies="@(_ReferencePath);@(_ReferenceDependencyPaths)">
<Output TaskParameter="OutputAssemblies" ItemName="_MonoAndroidReferencePath" />
</FilterAssemblies>
Expand Down Expand Up @@ -2213,7 +2214,7 @@ because xbuild doesn't support framework reference assemblies.

<CreateItem
Include="@(_ResolvedUserAssemblies)"
Condition="'%(_ResolvedUserAssemblies.TargetFrameworkIdentifier)' == 'MonoAndroid'">
Condition="'%(_ResolvedUserAssemblies.TargetFrameworkIdentifier)' == 'MonoAndroid' Or '%(_ResolvedUserAssemblies.HasMonoAndroidReference)' == 'True'">
<Output TaskParameter="Include" ItemName="_ResolvedUserMonoAndroidAssemblies" />
</CreateItem>
</Target>
Expand Down

0 comments on commit 71623cc

Please sign in to comment.