Skip to content

Commit

Permalink
[One .NET] dotnet new project and item templates (dotnet#5348)
Browse files Browse the repository at this point in the history
Context: https://docs.microsoft.com/dotnet/core/tutorials/cli-templates-create-template-pack
Context: https://github.com/dotnet/templating/wiki
Context: dotnet/designs#120

This implements basic Android templates that are contained in a
`Microsoft.Android.Templates.nupkg` file.

We can define a template pack in `WorkloadManifest.json`:

	"Microsoft.Android.Templates": {
	  "kind": "template",
	  "version": "@TEMPLATE_PACK_VERSION@"
	}

This allows the workload to locate the `*.nupkg` file in:

	C:\Program Files\dotnet\template-packs
	/usr/local/share/dotnet/template-packs

Our .NET 6 preview installers simply have to place the `*.nupkg`
files in the right place for them to be picked up by `dotnet new`.

Some example project templates:

	dotnet new android            --output MyAndroidApp     --packageName com.mycompany.myandroidapp
	dotnet new androidlib         --output MyAndroidLibrary
	dotnet new android-bindinglib --output MyJavaBinding

And item templates:

	dotnet new android-activity --name LoginActivity --namespace MyAndroidApp
	dotnet new android-layout   --name MyLayout      --output Resources/layout

Note that the `android-bindinglib` template is not a special project
type.  It has additional help files for writing bindings as we have
in the current Xamarin.Android templates.

I also updated the `XASdkTests` to `dotnet new` each template and
`dotnet build` the resulting output.

Finally, the `<GenerateWixFile/>` task (68be8d8) will now add
`<Directory/>` entries such as:

	<Directory Id="packs" Name="packs" FileSource="C:\Users\myuser\android-toolchain\dotnet\packs">

and:

	<Directory Id="templatepacks" Name="template-packs" FileSource="C:\Users\myuser\android-toolchain\dotnet\template-packs">
	  <Component Id="SC983E605827BDDA589CA4DFF082E29CDFEC2CD059B4AF3C01421CC95568A8D74">
	    <File Id="SC983E605827BDDA589CA4DFF082E29CDFEC2CD059B4AF3C01421CC95568A8D74" Name="Microsoft.Android.Templates.11.0.100-ci.dotnet-new.259.nupkg" KeyPath="yes" />
	  </Component>
	</Directory>

In the latter case, MSI has weird rules around the `Id` value.
Consequently, file hashes were used.
  • Loading branch information
jonathanpeppers authored Dec 4, 2020
1 parent a74fe4a commit e346f84
Show file tree
Hide file tree
Showing 48 changed files with 512 additions and 20 deletions.
47 changes: 44 additions & 3 deletions Documentation/guides/OneDotNet.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,47 @@ It is recommended to migrate to the new linker settings, as
There are currently a few "verbs" we are aiming to get working in
Xamarin.Android:

dotnet new
dotnet build
dotnet publish
dotnet run

Currently in .NET 5 console apps, `dotnet publish` is where all the
work to produce a self-contained "app" happens:
### dotnet new

To support `dotnet new`, we created a few basic project and item
templates for Android that are named following the patterns and naming
of existing .NET templates:

Templates Short Name Language Tags
-------------------------------------------- ------------------- ---------- ----------------------
Android Activity template android-activity [C#] Android
Android Java Library Binding android-bindinglib [C#] Android
Android Layout template android-layout [C#] Android
Android Class library androidlib [C#] Android
Android Application android [C#] Android
Console Application console [C#],F#,VB Common/Console
Class library classlib [C#],F#,VB Common/Library
WPF Application wpf [C#],VB Common/WPF
WPF Class library wpflib [C#],VB Common/WPF
NUnit 3 Test Project nunit [C#],F#,VB Test/NUnit
NUnit 3 Test Item nunit-test [C#],F#,VB Test/NUnit

To create different types of Android projects:

dotnet new android --output MyAndroidApp --packageName com.mycompany.myandroidapp
dotnet new androidlib --output MyAndroidLibrary
dotnet new android-bindinglib --output MyJavaBinding

Once the projects are created, some basic item templates can also be
used such as:

dotnet new android-activity --name LoginActivity --namespace MyAndroidApp
dotnet new android-layout --name MyLayout --output Resources/layout

### dotnet build & publish

Currently in .NET console apps, `dotnet publish` is where all the work
to produce a self-contained "app" happens:

* The linker via the `<IlLink/>` MSBuild task
* .NET Core's version of AOT, named "ReadyToRun"
Expand Down Expand Up @@ -229,12 +264,18 @@ Play, ad-hoc distribution, etc. It could be able to sign the `.apk` or
`.aab` with different keys. As a starting point, this will currently
copy the output to a `publish` directory on disk.

[illink]: https://github.com/mono/linker/blob/master/src/linker/README.md

### dotnet run

`dotnet run` can be used to launch applications on a
device or emulator via the `--project` switch:

dotnet run --project HelloAndroid.csproj

[illink]: https://github.com/mono/linker/blob/master/src/linker/README.md
Alternatively, you could use the `Run` MSBuild target such as:

dotnet build HelloAndroid.csproj -t:Run

### Preview testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,22 @@ public override bool Execute ()
packWriter.WriteStartElement ("Directory");
packWriter.WriteAttributeString ("Id", "packs");
packWriter.WriteAttributeString ("Name", "packs");
packWriter.WriteAttributeString ("FileSource", packs_dir);
foreach (var directory in Directory.EnumerateDirectories (packs_dir, "Microsoft.Android.*")) {
RecurseDirectory (packs_dir, packWriter, componentWriter, directory);
}
packWriter.WriteEndElement (); // </Directory> packs

// template-packs
var templates_dir = Path.Combine (DotNetPath, "template-packs");
packWriter.WriteStartElement ("Directory");
packWriter.WriteAttributeString ("Id", "templatepacks");
packWriter.WriteAttributeString ("Name", "template-packs");
packWriter.WriteAttributeString ("FileSource", templates_dir);
foreach (var file in Directory.EnumerateFiles (templates_dir, "Microsoft.Android.Templates.*.nupkg")) {
AddFile (templates_dir, packWriter, componentWriter, file);
}
packWriter.WriteEndElement (); // </Directory> template-packs

packWriter.WriteEndDocument (); // </Directory>
componentWriter.WriteEndDocument (); // </ComponentGroup>
Expand Down Expand Up @@ -145,22 +158,27 @@ static void RecurseDirectory (string top_dir, XmlWriter packWriter, XmlWriter co
var fileName = Path.GetFileName (file);
if (fileName.StartsWith (".") || fileName.StartsWith ("_"))
continue;
var componentId = GetId (top_dir, file);
packWriter.WriteStartElement ("Component");
packWriter.WriteAttributeString ("Id", componentId);
packWriter.WriteStartElement ("File");
packWriter.WriteAttributeString ("Id", componentId);
packWriter.WriteAttributeString ("Name", Path.GetFileName (file));
packWriter.WriteAttributeString ("KeyPath", "yes");
packWriter.WriteEndElement (); // </File>
packWriter.WriteEndElement (); // </Component>
componentWriter.WriteStartElement ("ComponentRef");
componentWriter.WriteAttributeString ("Id", componentId);
componentWriter.WriteEndElement (); // </ComponentRef>
AddFile (top_dir, packWriter, componentWriter, file);
}
packWriter.WriteEndElement (); // </Directory>
}

static void AddFile (string top_dir, XmlWriter packWriter, XmlWriter componentWriter, string file)
{
string componentId = GetId (top_dir, file);
packWriter.WriteStartElement ("Component");
packWriter.WriteAttributeString ("Id", componentId);
packWriter.WriteStartElement ("File");
packWriter.WriteAttributeString ("Id", componentId);
packWriter.WriteAttributeString ("Name", Path.GetFileName (file));
packWriter.WriteAttributeString ("KeyPath", "yes");
packWriter.WriteEndElement (); // </File>
packWriter.WriteEndElement (); // </Component>
componentWriter.WriteStartElement ("ComponentRef");
componentWriter.WriteAttributeString ("Id", componentId);
componentWriter.WriteEndElement (); // </ComponentRef>
}

static string GetId (string top_dir, string path)
{
if (string.IsNullOrEmpty (path))
Expand Down
1 change: 1 addition & 0 deletions build-tools/create-dotnet-pkg/create-dotnet-pkg.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<_FilesToCopy Include="$(DotNetPreviewPath)\sdk-manifests\$(DotNetPreviewVersionBand)\Microsoft.NET.Workload.Android\**\*" />
<_FilesToCopy Include="$(DotNetPreviewPath)\packs\Microsoft.Android.Ref\**\*" />
<_FilesToCopy Include="$(DotNetPreviewPath)\packs\Microsoft.Android.Sdk.osx-x64\**\*" />
<_FilesToCopy Include="$(DotNetPreviewPath)\template-packs\Microsoft.Android.Templates.*.nupkg" />
</ItemGroup>
<MakeDir Directories="$(PkgOutputPath);$(PayloadDir);$(PkgResourcesPath);$(LicenseDestination)"/>
<ReplaceFileContents
Expand Down
5 changes: 5 additions & 0 deletions build-tools/create-packs/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
<Exec Command="$(DotNetPreviewTool) pack @(_GlobalProperties, ' ') -p:AndroidHostRID=osx-x64 &quot;$(MSBuildThisFileDirectory)Microsoft.Android.Sdk.proj&quot;" Condition=" '$(HostOS)' == 'Darwin' " />
<Exec Command="$(DotNetPreviewTool) pack @(_GlobalProperties, ' ') -p:AndroidHostRID=win-x64 &quot;$(MSBuildThisFileDirectory)Microsoft.Android.Sdk.proj&quot;" Condition=" '$(HostOS)' != 'Linux' " /> <!-- Windows pack should be built both Windows and macOS -->
<Exec Command="$(DotNetPreviewTool) pack @(_GlobalProperties, ' ') &quot;$(MSBuildThisFileDirectory)Microsoft.NET.Workload.Android.proj&quot;" />
<Exec Command="$(DotNetPreviewTool) pack @(_GlobalProperties, ' ') &quot;$(XamarinAndroidSourcePath)src\Microsoft.Android.Templates\Microsoft.Android.Templates.csproj&quot;" />
</Target>

<Target Name="ExtractWorkloadPacks"
Expand All @@ -92,6 +93,7 @@
<_WLPacks Include="$(XamarinAndroidSourcePath)bin\Build$(Configuration)\nupkgs\Microsoft.Android.Sdk.osx-x64.*.nupkg" Condition=" '$(HostOS)' == 'Darwin' " />
<_WLPacks Include="$(XamarinAndroidSourcePath)bin\Build$(Configuration)\nupkgs\Microsoft.Android.Sdk.win-x64.*.nupkg" Condition=" '$(HostOS)' == 'Windows' " />
<_WLPacks Include="$(XamarinAndroidSourcePath)bin\Build$(Configuration)\nupkgs\Microsoft.Android.Ref.*.nupkg" />
<_WLTemplates Include="$(XamarinAndroidSourcePath)bin\Build$(Configuration)\nupkgs\Microsoft.Android.Templates.*.nupkg" />
<!-- Runtime packs are not yet supported by workloads -->
<!-- <_WLPacks Include="$(XamarinAndroidSourcePath)bin\Build$(Configuration)\nupkgs\Microsoft.Android.Runtime.*.nupkg" /> -->
</ItemGroup>
Expand All @@ -106,6 +108,8 @@
SourceFiles="@(_WLPacks)"
DestinationFolder="$(DotNetPreviewPath)packs\$([System.String]::Copy('%(_WLPacks.Filename)').Replace('.$(_WLPackVersion)', ''))\$(_WLPackVersion)"
/>
<MakeDir Directories="$(DotNetPreviewPath)template-packs" />
<Copy SourceFiles="@(_WLTemplates)" DestinationFolder="$(DotNetPreviewPath)template-packs" />
<ItemGroup>
<_UnixExecutables Include="$(DotNetPreviewPath)packs\Microsoft.Android.Sdk.*\*\tools\$(HostOS)\**\*.*" />
<_FilesToTouch Include="$(DotNetPreviewPath)sdk-manifests\$(DotNetPreviewVersionBand)\Microsoft.NET.Workload.Android\**" />
Expand All @@ -129,6 +133,7 @@
<_PackFilesToDelete Include="$(DotNetPreviewPath)sdk-manifests\$(DotNetPreviewVersionBand)\Microsoft.Android.Workload\**\*.*" />
<_PackFilesToDelete Include="$(DotNetPreviewPath)sdk-manifests\$(DotNetPreviewVersionBand)\Microsoft.NET.Workload.Android\**\*.*" />
<_PackFilesToDelete Include="$(DotNetPreviewPath)packs\Microsoft.Android*\**\*.*" />
<_PackFilesToDelete Include="$(DotNetPreviewPath)template-packs\Microsoft.Android.Templates.*.nupkg" />
</ItemGroup>
<RemoveDir Directories="%(_PackFilesToDelete.RootDir)%(_PackFilesToDelete.Directory)" />
<Delete Files="$(_WorkloadResolverFlagFile)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ workload manifest pack containing information about the various Microsoft.Androi
<ReplaceFileContents
SourceFile="$(XamarinAndroidSourcePath)src\Xamarin.Android.Build.Tasks\Microsoft.NET.Workload.Android\WorkloadManifest.in.json"
DestinationFile="$(WorkloadManifestJsonPath)"
Replacements="@SDK_PACK_VERSION@=$(AndroidPackVersionLong);@REF_PACK_VERSION@=$(AndroidPackVersionLong)">
Replacements="@SDK_PACK_VERSION@=$(AndroidPackVersionLong);@REF_PACK_VERSION@=$(AndroidPackVersionLong);@TEMPLATE_PACK_VERSION@=$(AndroidPackVersionLong);">
</ReplaceFileContents>

<ItemGroup>
Expand Down
15 changes: 15 additions & 0 deletions src/Microsoft.Android.Templates/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project>
<Import Project="..\..\build-tools\scripts\XAVersionInfo.targets" />
<PropertyGroup>
<BeforePack>
_GetDefaultPackageVersion;
$(BeforePack);
</BeforePack>
</PropertyGroup>
<Target Name="_GetDefaultPackageVersion"
DependsOnTargets="GetXAVersionInfo" >
<PropertyGroup>
<PackageVersion>$(AndroidPackVersionLong)+sha.$(XAVersionHash)</PackageVersion>
</PropertyGroup>
</Target>
</Project>
23 changes: 23 additions & 0 deletions src/Microsoft.Android.Templates/Microsoft.Android.Templates.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageType>Template</PackageType>
<PackageId>Microsoft.Android.Templates</PackageId>
<Title>.NET Android Templates</Title>
<Authors>Microsoft</Authors>
<Description>Templates for Android platforms.</Description>
<IncludeContentInPack>true</IncludeContentInPack>
<IncludeBuildOutput>false</IncludeBuildOutput>
<ContentTargetFolders>content</ContentTargetFolders>
<OutputPath>..\..\bin\Build$(Configuration)\nupkgs\</OutputPath>
</PropertyGroup>

<Import Project="..\..\Configuration.props" />

<ItemGroup>
<Content Include="**\*" Exclude="**\bin\**;**\obj\**" />
<Compile Remove="**\*" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Microsoft",
"classifications": [ "Android" ],
"name": "Android Activity template",
"description": "An Android Activity class",
"tags": {
"language": "C#",
"type": "item"
},
"identity": "Microsoft.Android.AndroidActivity",
"shortName": "android-activity",
"sourceName": "Activity1",
"primaryOutputs": [
{ "path": "Activity1.cs" }
],
"defaultName": "Activity1",
"symbols": {
"namespace": {
"description": "namespace for the generated code",
"replaces": "AndroidApp1",
"type": "parameter"
}
}
}
18 changes: 18 additions & 0 deletions src/Microsoft.Android.Templates/android-activity/Activity1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Android.App;
using Android.OS;
using Android.Runtime;
using Android.Widget;

namespace AndroidApp1
{
[Activity(Label = "@string/app_name", MainLauncher = true)]
public class Activity1 : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);

// Create your application here
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Microsoft",
"classifications": [ "Android" ],
"identity": "Microsoft.Android.AndroidBinding",
"name": "Android Java Library Binding",
"description": "A project for creating an Android class library that binds to a native Java library",
"shortName": "android-bindinglib",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "AndroidBinding1",
"preferNameDirectory": true,
"primaryOutputs": [
{ "path": "AndroidBinding1.csproj" }
],
"defaultName": "AndroidBinding1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Additions allow you to add arbitrary C# to the generated classes
before they are compiled. This can be helpful for providing convenience
methods or adding pure C# classes.

== Adding Methods to Generated Classes ==

Let's say the library being bound has a Rectangle class with a constructor
that takes an x and y position, and a width and length size. It will look like
this:

public partial class Rectangle
{
public Rectangle (int x, int y, int width, int height)
{
// JNI bindings
}
}

Imagine we want to add a constructor to this class that takes a Point and
Size structure instead of 4 ints. We can add a new file called Rectangle.cs
with a partial class containing our new method:

public partial class Rectangle
{
public Rectangle (Point location, Size size) :
this (location.X, location.Y, size.Width, size.Height)
{
}
}

At compile time, the additions class will be added to the generated class
and the final assembly will a Rectangle class with both constructors.


== Adding C# Classes ==

Another thing that can be done is adding fully C# managed classes to the
generated library. In the above example, let's assume that there isn't a
Point class available in Java or our library. The one we create doesn't need
to interact with Java, so we'll create it like a normal class in C#.

By adding a Point.cs file with this class, it will end up in the binding library:

public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-android</TargetFramework>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">AndroidBinding1</RootNamespace>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<enum-field-mappings>
<!--
This example converts the constants Fragment_id, Fragment_name,
and Fragment_tag from android.support.v4.app.FragmentActivity.FragmentTag
to an enum called Android.Support.V4.App.FragmentTagType with values
Id, Name, and Tag.
<mapping jni-class="android/support/v4/app/FragmentActivity$FragmentTag" clr-enum-type="Android.Support.V4.App.FragmentTagType">
<field jni-name="Fragment_name" clr-name="Name" value="0" />
<field jni-name="Fragment_id" clr-name="Id" value="1" />
<field jni-name="Fragment_tag" clr-name="Tag" value="2" />
</mapping>
-->
</enum-field-mappings>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<enum-method-mappings>
<!--
This example changes the Java method:
android.support.v4.app.Fragment.SavedState.writeToParcel (int flags)
to be:
android.support.v4.app.Fragment.SavedState.writeToParcel (Android.OS.ParcelableWriteFlags flags)
when bound in C#.
<mapping jni-class="android/support/v4/app/Fragment.SavedState">
<method jni-name="writeToParcel" parameter="flags" clr-enum-type="Android.OS.ParcelableWriteFlags" />
</mapping>
-->
</enum-method-mappings>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<metadata>
<!--
This sample removes the class: android.support.v4.content.AsyncTaskLoader.LoadTask:
<remove-node path="/api/package[@name='android.support.v4.content']/class[@name='AsyncTaskLoader.LoadTask']" />
This sample removes the method: android.support.v4.content.CursorLoader.loadInBackground:
<remove-node path="/api/package[@name='android.support.v4.content']/class[@name='CursorLoader']/method[@name='loadInBackground']" />
-->
</metadata>
Loading

0 comments on commit e346f84

Please sign in to comment.