Skip to content

Commit b67b545

Browse files
[.NET 5] NuGet workarounds for Xamarin.Forms projects (#4663)
Context: https://github.com/xamarin/net5-samples/blob/d525d8a2d60700f52f86372c47e83d646d800aa4/Directory.Android.targets Currently, .NET 5 will not restore *existing* Xamarin.Android NuGet packages. It does not know to map `netcoreapp5.0` to `MonoAndroid10.0`, `MonoAndroid9` etc. We'll need to work around this until this lands: * NuGet/NuGet.Client#3339 To get a Xamarin.Forms project building & running *at all* in `xamarin/net5-samples`, I had to: 1. Change `$(AssetTargetFallback)` 2. Write an MSBuild target to replace any instances of `netstandard2.0\Xamarin.Forms.Platform.dll` with `MonoAndroid10.0\Xamarin.Forms.Platform.dll`. 3. Add additional platform-specific assemblies in `$(PkgXamarin_Forms)\lib\MonoAndroid10.0\` There are still some issues with this: 1. You get a lot of `NU1701` warnings due to `$(AssetTargetFallback)`. 2. Due to: https://github.com/NuGet/docs.microsoft.com-nuget/issues/1955 You have to manually list *every* transitive dependency, which totals around ~36 packages for AndroidX. These drawbacks make it completely impractical to run some subset of our existing MSBuild tests on .NET 5. We would have to list the entire tree of `<PackageReference/>` for every test. Since we might be waiting a bit of time for this, I have been able to come up with some workarounds to solve these problems for now: 1. Add a `Microsoft.Android.Sdk.NuGet.targets` with workarounds that we will completely remove down the road (I hope!). 2. Use `$(PackageTargetFallback)` instead of `$(AssetTargetFallback)`. It does not emit `NU1701` warnings, and transitive dependencies work! It is a deprecated MSBuild property, but should be fine as a workaround. 3. Write a `_FixupNuGetReferences` MSBuild target that runs after `ResolvePackageAssets`. A `<FixupNuGetReferences/>` MSBuild task will remove any `netstandard2.0` assemblies where platform-specific ones exist. It also needs to add platform-specific assemblies that are missing. With this in place, a Xamarin.Forms `.csproj` with no hacks works! <Project Sdk="Microsoft.Android.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp5.0</TargetFramework> <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> <PackageReference Include="Xamarin.Forms" Version="4.5.0.617" /> </ItemGroup> </Project> I created a new `XamarinFormsXASdkProject` class for use in our existing .NET 5 MSBuild tests. Other changes: * `$(XamarinAndroidVersion)` needs to be filled out for the AndroidX MSBuild targets: https://github.com/xamarin/XamarinAndroidXMigration/blob/ea130ca0d0e9b3e20edccec02364f36da11ada6b/source/Xamarin.AndroidX.Migration/BuildTasks/Xamarin.AndroidX.Migration.targets#L107 * `$(RuntimeIdentifier)` should have a default value for application projects. * Some tweaks to make `XASdkProject` more flexible. * `Xamarin.ProjectTools.DotNetStandard` projects now support `<Import/>`. * Added AndroidX-compatible `Tabbar.xml` and `Toolbar.xml`
1 parent 51176c6 commit b67b545

File tree

13 files changed

+293
-23
lines changed

13 files changed

+293
-23
lines changed

build-tools/create-packs/Microsoft.Android.Sdk.proj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and
102102
<Project>
103103
<PropertyGroup>
104104
<AndroidNETSdkVersion>$(AndroidPackVersionLong)</AndroidNETSdkVersion>
105+
<XamarinAndroidVersion>$(AndroidPackVersionLong)</XamarinAndroidVersion>
105106
</PropertyGroup>
106107
<ItemGroup>
107108
<KnownFrameworkReference Include="Microsoft.Android"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!--
2+
***********************************************************************************************
3+
Microsoft.Android.Sdk.NuGet.targets
4+
5+
This file contains *temporary* workarounds for NuGet in .NET 5.
6+
7+
***********************************************************************************************
8+
-->
9+
10+
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
11+
12+
<UsingTask TaskName="Xamarin.Android.Tasks.FixupNuGetReferences" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
13+
14+
<PropertyGroup>
15+
<!-- Clear $(AssetTargetFallback), so only $(PackageTargetFallback) is used. -->
16+
<AssetTargetFallback></AssetTargetFallback>
17+
<!--
18+
Use $(PackageTargetFallback), even though it is deprecated.
19+
It doesn't suffer from: https://github.com/NuGet/docs.microsoft.com-nuget/issues/1955
20+
-->
21+
<PackageTargetFallback>
22+
monoandroid10.0;
23+
monoandroid90;
24+
monoandroid81;
25+
monoandroid80;
26+
monoandroid70;
27+
monoandroid60;
28+
monoandroid50;
29+
$(PackageTargetFallback);
30+
</PackageTargetFallback>
31+
</PropertyGroup>
32+
33+
<Target Name="_FixupNuGetReferences" AfterTargets="ResolvePackageAssets">
34+
<FixupNuGetReferences
35+
PackageTargetFallback="$(PackageTargetFallback)"
36+
CopyLocalItems="@(RuntimeCopyLocalItems)">
37+
<Output TaskParameter="AssembliesToRemove" ItemName="_AssembliesToRemove" />
38+
<Output TaskParameter="AssembliesToAdd" ItemName="Reference" />
39+
</FixupNuGetReferences>
40+
<ItemGroup>
41+
<RuntimeCopyLocalItems Remove="@(_AssembliesToRemove)" />
42+
<ResolvedCompileFileDefinitions Remove="@(_AssembliesToRemove)" />
43+
</ItemGroup>
44+
</Target>
45+
46+
</Project>

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.targets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<AndroidApplication Condition=" '$(AndroidApplication)' == '' And '$(OutputType)' == 'Exe' ">true</AndroidApplication>
1010
<AndroidApplication Condition=" '$(AndroidApplication)' == '' ">false</AndroidApplication>
1111
<SelfContained Condition=" '$(SelfContained)' == '' And '$(AndroidApplication)' == 'true' ">true</SelfContained>
12+
<RuntimeIdentifier Condition=" '$(RuntimeIdentifier)' == '' And '$(AndroidApplication)' == 'true' ">android.21-x86</RuntimeIdentifier>
1213
<AndroidManifest Condition=" '$(AndroidManifest)' == '' And '$(AndroidApplication)' == 'true' ">Properties\AndroidManifest.xml</AndroidManifest>
1314
<!-- We don't ever need a `static void Main` method, so switch to Library here-->
1415
<OutputType Condition=" '$(OutputType)' == 'Exe' ">Library</OutputType>
@@ -20,6 +21,7 @@
2021
<Import Project="$(MSBuildThisFileDirectory)..\tools\Xamarin.Android.Common.targets" />
2122
<Import Project="$(MSBuildThisFileDirectory)Microsoft.Android.Sdk.AssemblyResolution.targets" />
2223
<Import Project="$(MSBuildThisFileDirectory)Microsoft.Android.Sdk.BuildOrder.targets" />
24+
<Import Project="$(MSBuildThisFileDirectory)Microsoft.Android.Sdk.NuGet.targets" />
2325
<Import Project="$(MSBuildThisFileDirectory)Microsoft.Android.Sdk.Tooling.targets" />
2426

2527
<!-- Automatically supply project capabilities for IDE use -->
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using Microsoft.Build.Framework;
6+
7+
namespace Xamarin.Android.Tasks
8+
{
9+
/// <summary>
10+
/// This task contains *temporary* workarounds for NuGet in .NET 5.
11+
/// </summary>
12+
public class FixupNuGetReferences : AndroidTask
13+
{
14+
public override string TaskPrefix => "FNR";
15+
16+
[Required]
17+
public string [] PackageTargetFallback { get; set; }
18+
19+
public ITaskItem [] CopyLocalItems { get; set; }
20+
21+
[Output]
22+
public string [] AssembliesToAdd { get; set; }
23+
24+
[Output]
25+
public ITaskItem [] AssembliesToRemove { get; set; }
26+
27+
public override bool RunTask ()
28+
{
29+
if (CopyLocalItems == null || CopyLocalItems.Length == 0)
30+
return true;
31+
32+
var assembliesToAdd = new Dictionary<string, string> ();
33+
var assembliesToRemove = new List<ITaskItem> ();
34+
var fallbackDirectories = new HashSet<string> ();
35+
36+
foreach (var item in CopyLocalItems) {
37+
var directory = Path.GetDirectoryName (item.ItemSpec);
38+
var directoryName = Path.GetFileName (directory);
39+
Log.LogDebugMessage ($"{directoryName} -> {item.ItemSpec}");
40+
if (directoryName == "netstandard2.0") {
41+
var parent = Directory.GetParent (directory);
42+
foreach (var nugetDirectory in parent.EnumerateDirectories ()) {
43+
var name = Path.GetFileName (nugetDirectory.Name);
44+
foreach (var fallback in PackageTargetFallback) {
45+
if (!string.Equals (name, fallback, StringComparison.OrdinalIgnoreCase))
46+
continue;
47+
var fallbackDirectory = Path.Combine (parent.FullName, fallback);
48+
fallbackDirectories.Add (fallbackDirectory);
49+
50+
// Remove the netstandard assembly, if there is a platform-specific one
51+
var path = Path.Combine (fallbackDirectory, Path.GetFileName (item.ItemSpec));
52+
if (File.Exists (path)) {
53+
Log.LogDebugMessage ($"Removing: {item.ItemSpec}");
54+
assembliesToRemove.Add (item);
55+
}
56+
}
57+
}
58+
}
59+
}
60+
61+
// Look for any platform-specific assemblies
62+
foreach (var directory in fallbackDirectories) {
63+
foreach (var assembly in Directory.GetFiles (directory, "*.dll")) {
64+
var assemblyName = Path.GetFileName (assembly);
65+
if (!assembliesToAdd.ContainsKey (assemblyName)) {
66+
Log.LogDebugMessage ($"Adding: {assembly}");
67+
assembliesToAdd.Add (assemblyName, assembly);
68+
}
69+
}
70+
}
71+
72+
AssembliesToAdd = assembliesToAdd.Values.ToArray ();
73+
AssembliesToRemove = assembliesToRemove.ToArray ();
74+
75+
return !Log.HasLoggedErrors;
76+
}
77+
}
78+
}

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ public void DotNetBuild (string runtimeIdentifier, bool isRelease)
8989
}
9090
}
9191

92+
[Test]
93+
[Category ("SmokeTests")]
94+
public void DotNetBuildXamarinForms ()
95+
{
96+
var proj = new XamarinFormsXASdkProject (SdkVersion);
97+
var dotnet = CreateDotNetBuilder (proj);
98+
Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed");
99+
Assert.IsTrue (StringAssertEx.ContainsText (dotnet.LastBuildOutput, " 0 Warning(s)"), "Should have no MSBuild warnings.");
100+
}
101+
92102
[Test]
93103
public void BuildWithLiteSdk ()
94104
{

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownPackages.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ public static class KnownPackages
208208
},
209209
}
210210
};
211+
public static Package XamarinForms_4_5_0_617 = new Package {
212+
Id = "Xamarin.Forms",
213+
Version = "4.5.0.617",
214+
TargetFramework = "MonoAndroid10.0",
215+
};
211216
public static Package XamarinFormsMaps_4_0_0_425677 = new Package {
212217
Id = "Xamarin.Forms.Maps",
213218
Version = "4.0.0.425677",

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XASdkProject.cs

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
using System.Linq;
5-
using System.Text;
6-
using System.Threading.Tasks;
74

85
namespace Xamarin.ProjectTools
96
{
@@ -16,10 +13,25 @@ public class XASdkProject : DotNetStandard
1613
</resources>
1714
";
1815

19-
readonly string default_layout_main;
20-
readonly string default_main_activity_cs;
21-
readonly string default_android_manifest;
22-
readonly byte [] icon_binary_mdpi;
16+
static readonly string default_layout_main;
17+
static readonly string default_main_activity_cs;
18+
static readonly string default_android_manifest;
19+
static readonly byte [] icon_binary_mdpi;
20+
21+
static XASdkProject ()
22+
{
23+
var assembly = typeof (XASdkProject).Assembly;
24+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.AndroidManifest.xml")))
25+
default_android_manifest = sr.ReadToEnd ();
26+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.MainActivity.cs")))
27+
default_main_activity_cs = sr.ReadToEnd ();
28+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.LayoutMain.axml")))
29+
default_layout_main = sr.ReadToEnd ();
30+
using (var stream = assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.Icon.png")) {
31+
icon_binary_mdpi = new byte [stream.Length];
32+
stream.Read (icon_binary_mdpi, 0, (int) stream.Length);
33+
}
34+
}
2335

2436
public string PackageName { get; set; }
2537
public string JavaPackageName { get; set; }
@@ -35,24 +47,13 @@ public XASdkProject (string sdkVersion = "", string outputType = "Exe")
3547
GlobalPackagesFolder = Path.Combine (XABuildPaths.TopDirectory, "packages");
3648
SetProperty (KnownProperties.OutputType, outputType);
3749

38-
using (var sr = new StreamReader (typeof (XamarinAndroidApplicationProject).Assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.AndroidManifest.xml")))
39-
default_android_manifest = sr.ReadToEnd ();
40-
using (var sr = new StreamReader (typeof (XamarinAndroidApplicationProject).Assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.MainActivity.cs")))
41-
default_main_activity_cs = sr.ReadToEnd ();
42-
using (var sr = new StreamReader (typeof (XamarinAndroidApplicationProject).Assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.LayoutMain.axml")))
43-
default_layout_main = sr.ReadToEnd ();
44-
using (var stream = typeof (XamarinAndroidCommonProject).Assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.Icon.png")) {
45-
icon_binary_mdpi = new byte [stream.Length];
46-
stream.Read (icon_binary_mdpi, 0, (int) stream.Length);
47-
}
48-
4950
// Add relevant Android content to our project without writing it to the .csproj file
5051
if (outputType == "Exe") {
5152
Sources.Add (new BuildItem.Source ("Properties\\AndroidManifest.xml") {
5253
TextContent = () => default_android_manifest.Replace ("${PROJECT_NAME}", ProjectName).Replace ("${PACKAGENAME}", string.Format ("{0}.{0}", ProjectName))
5354
});
5455
}
55-
Sources.Add (new BuildItem.Source ($"MainActivity{Language.DefaultExtension}") { TextContent = () => ProcessSourceTemplate (default_main_activity_cs) });
56+
Sources.Add (new BuildItem.Source ($"MainActivity{Language.DefaultExtension}") { TextContent = () => ProcessSourceTemplate (MainActivity ?? DefaultMainActivity) });
5657
Sources.Add (new BuildItem.Source ("Resources\\layout\\Main.axml") { TextContent = () => default_layout_main });
5758
Sources.Add (new BuildItem.Source ("Resources\\values\\Strings.xml") { TextContent = () => default_strings_xml.Replace ("${PROJECT_NAME}", ProjectName) });
5859
Sources.Add (new BuildItem.Source ("Resources\\drawable-mdpi\\Icon.png") { BinaryContent = () => icon_binary_mdpi });
@@ -65,6 +66,10 @@ public XASdkProject (string sdkVersion = "", string outputType = "Exe")
6566

6667
public string IntermediateOutputPath => Path.Combine ("obj", Configuration, TargetFramework.ToLowerInvariant ());
6768

69+
public string DefaultMainActivity => default_main_activity_cs;
70+
71+
public string MainActivity { get; set; }
72+
6873
public override string ProcessSourceTemplate (string source)
6974
{
7075
return source.Replace ("${ROOT_NAMESPACE}", RootNamespace ?? ProjectName)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
6+
namespace Xamarin.ProjectTools
7+
{
8+
public class XamarinFormsXASdkProject : XASdkProject
9+
{
10+
static readonly string default_main_activity_cs;
11+
static readonly string colors_xml;
12+
static readonly string styles_xml;
13+
static readonly string Tabbar_xml;
14+
static readonly string Toolbar_xml;
15+
static readonly string MainPage_xaml;
16+
static readonly string MainPage_xaml_cs;
17+
static readonly string App_xaml;
18+
static readonly string App_xaml_cs;
19+
20+
static XamarinFormsXASdkProject ()
21+
{
22+
var assembly = typeof (XamarinFormsXASdkProject).Assembly;
23+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Forms.MainActivity.cs")))
24+
default_main_activity_cs = sr.ReadToEnd ();
25+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Forms.colors.xml")))
26+
colors_xml = sr.ReadToEnd ();
27+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Forms.styles.xml")))
28+
styles_xml = sr.ReadToEnd ();
29+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.AndroidX.Tabbar.xml")))
30+
Tabbar_xml = sr.ReadToEnd ();
31+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.AndroidX.Toolbar.xml")))
32+
Toolbar_xml = sr.ReadToEnd ();
33+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Forms.MainPage.xaml")))
34+
MainPage_xaml = sr.ReadToEnd ();
35+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Forms.MainPage.xaml.cs")))
36+
MainPage_xaml_cs = sr.ReadToEnd ();
37+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Forms.App.xaml")))
38+
App_xaml = sr.ReadToEnd ();
39+
using (var sr = new StreamReader (assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Forms.App.xaml.cs")))
40+
App_xaml_cs = sr.ReadToEnd ();
41+
}
42+
43+
44+
public XamarinFormsXASdkProject (string sdkVersion = "", string outputType = "Exe")
45+
: base (sdkVersion, outputType)
46+
{
47+
PackageReferences.Add (KnownPackages.XamarinForms_4_5_0_617);
48+
49+
// Workaround for AndroidX, see: https://github.com/xamarin/AndroidSupportComponents/pull/239
50+
Imports.Add (new Import (() => "Directory.Build.targets") {
51+
TextContent = () =>
52+
@"<Project>
53+
<PropertyGroup>
54+
<VectorDrawableCheckBuildToolsVersionTaskBeforeTargets />
55+
</PropertyGroup>
56+
</Project>"
57+
});
58+
59+
Sources.Add (new AndroidItem.AndroidResource ("Resources\\values\\colors.xml") {
60+
TextContent = () => colors_xml,
61+
});
62+
Sources.Add (new AndroidItem.AndroidResource ("Resources\\values\\styles.xml") {
63+
TextContent = () => styles_xml,
64+
});
65+
Sources.Add (new AndroidItem.AndroidResource ("Resources\\layout\\Tabbar.xml") {
66+
TextContent = () => Tabbar_xml,
67+
});
68+
Sources.Add (new AndroidItem.AndroidResource ("Resources\\layout\\Toolbar.xml") {
69+
TextContent = () => Toolbar_xml,
70+
});
71+
Sources.Add (new BuildItem ("EmbeddedResource", "MainPage.xaml") {
72+
TextContent = MainPageXaml,
73+
});
74+
Sources.Add (new BuildItem.Source ("MainPage.xaml.cs") {
75+
TextContent = () => ProcessSourceTemplate (MainPage_xaml_cs),
76+
});
77+
Sources.Add (new BuildItem ("EmbeddedResource", "App.xaml") {
78+
TextContent = () => ProcessSourceTemplate (App_xaml),
79+
});
80+
Sources.Add (new BuildItem.Source ("App.xaml.cs") {
81+
TextContent = () => ProcessSourceTemplate (App_xaml_cs),
82+
});
83+
84+
MainActivity = default_main_activity_cs;
85+
}
86+
87+
protected virtual string MainPageXaml () => ProcessSourceTemplate (MainPage_xaml);
88+
}
89+
}

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetStandard.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ public override string SaveProject ()
8282
}
8383
sb.AppendLine ("\t</ItemGroup>");
8484
}
85+
foreach (var import in Imports) {
86+
var project = import.Project ();
87+
if (project != "Directory.Build.props" && project != "Directory.Build.targets")
88+
sb.AppendLine ($"\t<Import Project=\"{project}\" />");
89+
}
8590
return $"<Project Sdk=\"{Sdk}\">\r\n{sb.ToString ()}\r\n</Project>";
8691
}
8792

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<androidx.design.widget.TabLayout
2+
xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
android:id="@+id/sliding_tabs"
5+
android:layout_width="match_parent"
6+
android:layout_height="wrap_content"
7+
android:background="?attr/colorPrimary"
8+
app:tabIndicatorColor="@android:color/white"
9+
app:tabGravity="fill"
10+
app:tabMode="fixed" />

0 commit comments

Comments
 (0)