Skip to content

Commit

Permalink
[.NET 5] use $(OutputType) Exe for application vs library
Browse files Browse the repository at this point in the history
In .NET 5, Xamarin.iOS is already using `<OutputType>Exe<OutputType>`
to distinguish an iOS application vs an iOS class library.

Xamarin.Android, however, is checking the existence of
`Properties/AndroidManifest.xml`.

This brings up some issues:

* When using a multi-targeted project in .NET, we should be using the
  same convention to distinguish library vs applications on both iOS
  and Android.
* Xamarin.iOS apps have a `static void Main` method. `OutputType=Exe`
  makes sense, as it tells Roslyn to expect a `Main` method.

To consolidate this, I propose:

* Use `$(OutputType)` of `Exe` to set `$(AndroidApplication)` to
  `true`. No longer look for `AndroidManifest.xml` for this default.
* Immediately set `$(OutputType)` to `Library`, since we *never* want a
  `Main` method on Android.

To make this work, I had to move these defaults from
`Xamarin.Android.Sdk.props` to `Xamarin.Android.Sdk.targets`. This
allows these properties to be evaluated *after* the values set in the
user's project.

I added a test that builds a library, checking that the
`__AndroidLibraryProjects__.zip` `EmbeddedResource` has the correct
contents.
  • Loading branch information
jonathanpeppers committed Apr 22, 2020
1 parent 20d5b6b commit 248e71f
Show file tree
Hide file tree
Showing 8 changed files with 50 additions and 11 deletions.
7 changes: 6 additions & 1 deletion Documentation/guides/DotNet5.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
_NOTE: this document is very likely to change, as the requirements for
.NET 5 are better understood._

A .NET 5 project for Xamarin.Android will look something like:
A .NET 5 project for a Xamarin.Android application will look something
like:

```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0-android</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
</Project>
```

For a "library" project, you would omit the `$(OutputType)` property
completely or specify `Library`.

See the [Target Framework Names in .NET 5][net5spec] spec for details.

[net5spec]: https://github.com/dotnet/designs/blob/5e921a9dc8ecce33b3195dcdb6f10ef56ef8b9d7/accepted/2020/net5/net5.md
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.IO;
using System.Linq;
using System.Reflection;
using Mono.Cecil;
using NUnit.Framework;
using Xamarin.Android.Tasks;
using Xamarin.ProjectTools;
using Xamarin.Tools.Zip;

namespace Xamarin.Android.Build.Tests
{
Expand All @@ -29,6 +31,29 @@ public void DotNetBuild ([Values (false, true)] bool isRelease)
Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed");
}

[Test]
[Category ("SmokeTests")]
public void DotNetBuildLibrary ([Values (false, true)] bool isRelease)
{
var proj = new XASdkProject (SdkVersion, outputType: "Library") {
IsRelease = isRelease
};
var dotnet = CreateDotNetBuilder (proj);
Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed");

var assemblyPath = Path.Combine (Root, dotnet.ProjectDirectory, proj.OutputPath, "UnnamedProject.dll");
FileAssert.Exists (assemblyPath);
using (var assembly = AssemblyDefinition.ReadAssembly (assemblyPath)) {
var resourceName = "__AndroidLibraryProjects__.zip";
var resource = assembly.MainModule.Resources.OfType<EmbeddedResource> ().FirstOrDefault (r => r.Name == resourceName);
Assert.IsNotNull (resource, $"{assemblyPath} should contain a {resourceName} EmbeddedResource");
using (var zip = ZipArchive.Open (resource.GetResourceStream ())) {
var entry = "library_project_imports/res/values/strings.xml";
Assert.IsTrue (zip.ContainsEntry (entry), $"{resourceName} should contain {entry}");
}
}
}

static readonly object [] DotNetPublishSource = new object [] {
new object [] {
/* runtimeIdentifier */ "android.21-arm",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static class KnownProperties

public const string OutputPath = "OutputPath";
public const string IntermediateOutputPath = "IntermediateOutputPath";
public const string OutputType = "OutputType";
public const string AndroidFastDeploymentType = "AndroidFastDeploymentType";
public const string AndroidClassParser = "AndroidClassParser";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class XASdkProject : DotNetStandard
public string PackageName { get; set; }
public string JavaPackageName { get; set; }

public XASdkProject (string sdkVersion = "")
public XASdkProject (string sdkVersion = "", string outputType = "Exe")
{
Sdk = string.IsNullOrEmpty (sdkVersion) ? "Xamarin.Android.Sdk" : $"Xamarin.Android.Sdk/{sdkVersion}";
TargetFramework = "netcoreapp5.0";
Expand All @@ -33,6 +33,7 @@ public XASdkProject (string sdkVersion = "")
JavaPackageName = JavaPackageName ?? PackageName.ToLowerInvariant ();
ExtraNuGetConfigSources = new List<string> { Path.Combine (XABuildPaths.BuildOutputDirectory, "nupkgs") };
GlobalPackagesFolder = Path.Combine (XABuildPaths.TopDirectory, "packages");
SetProperty (KnownProperties.OutputType, outputType);

using (var sr = new StreamReader (typeof (XamarinAndroidApplicationProject).Assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.AndroidManifest.xml")))
default_android_manifest = sr.ReadToEnd ();
Expand All @@ -46,9 +47,11 @@ public XASdkProject (string sdkVersion = "")
}

// Add relevant Android content to our project without writing it to the .csproj file
Sources.Add (new BuildItem.Source ("Properties\\AndroidManifest.xml") {
TextContent = () => default_android_manifest.Replace ("${PROJECT_NAME}", ProjectName).Replace ("${PACKAGENAME}", string.Format ("{0}.{0}", ProjectName))
});
if (outputType == "Exe") {
Sources.Add (new BuildItem.Source ("Properties\\AndroidManifest.xml") {
TextContent = () => default_android_manifest.Replace ("${PROJECT_NAME}", ProjectName).Replace ("${PACKAGENAME}", string.Format ("{0}.{0}", ProjectName))
});
}
Sources.Add (new BuildItem.Source ($"MainActivity{Language.DefaultExtension}") { TextContent = () => ProcessSourceTemplate (default_main_activity_cs) });
Sources.Add (new BuildItem.Source ("Resources\\layout\\Main.axml") { TextContent = () => default_layout_main });
Sources.Add (new BuildItem.Source ("Resources\\values\\Strings.xml") { TextContent = () => default_strings_xml.Replace ("${PROJECT_NAME}", ProjectName) });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protected XamarinAndroidProject (string debugConfigurationName = "Debug", string
SetProperty ("ProjectTypeGuids", () => "{" + Language.ProjectTypeGuid + "};{" + ProjectTypeGuid + "}");
SetProperty ("ProjectGuid", () => "{" + ProjectGuid + "}");

SetProperty ("OutputType", "Library");
SetProperty (KnownProperties.OutputType, "Library");
SetProperty ("MonoAndroidAssetsPrefix", "Assets");
SetProperty ("MonoAndroidResourcePrefix", "Resources");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public XamarinPCLProject ()
SetProperty ("ProjectGuid", () => "{" + ProjectGuid + "}");
SetProperty ("TargetFrameworkProfile", "Profile78");
SetProperty ("TargetFrameworkVersion", "v4.5");
SetProperty ("OutputType", "Library");
SetProperty (KnownProperties.OutputType, "Library");
}

public override string ProjectTypeGuid {
Expand Down
4 changes: 0 additions & 4 deletions src/Xamarin.Android.Sdk/targets/Xamarin.Android.Sdk.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@
<MonoAndroidAssetsPrefix Condition=" '$(MonoAndroidAssetsPrefix)' == '' ">Assets</MonoAndroidAssetsPrefix>
<AndroidResgenClass Condition=" '$(AndroidResgenClass)' == '' ">Resource</AndroidResgenClass>
<AndroidResgenFile Condition=" '$(AndroidResgenFile)' == '' ">$(MonoAndroidResourcePrefix)\$(_AndroidResourceDesigner)</AndroidResgenFile>
<AndroidManifest Condition=" '$(AndroidManifest)' == '' ">Properties\AndroidManifest.xml</AndroidManifest>
<AndroidApplication Condition=" '$(AndroidApplication)' == '' And Exists('$(AndroidManifest)') ">true</AndroidApplication>
<AndroidApplication Condition=" '$(AndroidApplication)' == '' ">false</AndroidApplication>
<SelfContained Condition=" '$(SelfContained)' == '' And '$(AndroidApplication)' == 'true' ">true</SelfContained>
<AndroidEnableSGenConcurrent Condition=" '$(AndroidEnableSGenConcurrent)' == '' ">true</AndroidEnableSGenConcurrent>
<AndroidHttpClientHandlerType Condition=" '$(AndroidHttpClientHandlerType)' == '' ">Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<AndroidUseIntermediateDesignerFile Condition=" '$(AndroidUseIntermediateDesignerFile)' == '' ">true</AndroidUseIntermediateDesignerFile>
Expand Down
9 changes: 9 additions & 0 deletions src/Xamarin.Android.Sdk/targets/Xamarin.Android.Sdk.targets
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
<_XamarinAndroidBuildTasksAssembly>$(MSBuildThisFileDirectory)..\tools\Xamarin.Android.Build.Tasks.dll</_XamarinAndroidBuildTasksAssembly>
</PropertyGroup>

<PropertyGroup>
<AndroidApplication Condition=" '$(AndroidApplication)' == '' And '$(OutputType)' == 'Exe' ">true</AndroidApplication>
<AndroidApplication Condition=" '$(AndroidApplication)' == '' ">false</AndroidApplication>
<SelfContained Condition=" '$(SelfContained)' == '' And '$(AndroidApplication)' == 'true' ">true</SelfContained>
<AndroidManifest Condition=" '$(AndroidManifest)' == '' And '$(AndroidApplication)' == 'true' ">Properties\AndroidManifest.xml</AndroidManifest>
<!-- We don't ever need a `static void Main` method, so switch to Library here-->
<OutputType Condition=" '$(OutputType)' == 'Exe' ">Library</OutputType>
</PropertyGroup>

<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" />

<Import Project="$(MSBuildThisFileDirectory)..\tools\Xamarin.Android.DefaultOutputPaths.targets" Condition="'$(EnableDefaultOutputPaths)' == 'true'" />
Expand Down

0 comments on commit 248e71f

Please sign in to comment.