Skip to content

Commit

Permalink
[One .NET] rework $(ApplicationVersion) and $(ApplicationDisplayVersi…
Browse files Browse the repository at this point in the history
…on) (#6139)

Context: dotnet/maui#1662

In commit 4f74bab we were mapping MSBuild properties to values in
`AndroidManifest.xml`:

  * `$(AndroidVersionCode)` -> `android:versionCode`
  * `$(ApplicationVersion)` -> `android:versionName`

To be more consistent with other platforms, we're reworking this to be:

  * `$(ApplicationVersion)` -> `android:versionCode`
  * `$(ApplicationDisplayVersion)` -> `android:versionName`

If we force `$(ApplicationVersion)` to be an integer, you can use the
same values on all platforms dotnet/maui supports.  We no longer need
an Android-specific `$(AndroidVersionCode)`.

We also need to consider the `$(Version)` property that influences
`$(AssemblyVersion)`, `$(FileVersion)`, and `$(InformationalVersion)`.
We can simply set `$(Version)` when `$(ApplicationDisplayVersion)` is
set to influence *all* the version values.

Unfortunately, if `$(Version)` is not parsable by
[`Version.Parse()`][0], [NuGet will error out][1].  We can use
@jonpryor's clever hack to detect when
`$(ApplicationDisplayVersion)` is not a valid `System.Version`,
setting `$(Version)` only when `$(ApplicationDisplayVersion)` is valid:

	<Version Condition=" $([System.Version]::TryParse (
	    '$(ApplicationDisplayVersion)',
	    $([System.Version]::Parse('1.0')))) "
	>$(ApplicationDisplayVersion)</Version>

I updated tests and documentation around this scenario.  I also
updated the `dotnet new android` project template.

There will need to be future changes in xamarin/xamarin-macios for
Apple platforms and dotnet/maui for WinUI.

[0]: https://docs.microsoft.com/en-us/dotnet/api/system.version.parse?view=net-5.0
[1]: NuGet/Home#11230
  • Loading branch information
jonathanpeppers committed Sep 13, 2021
1 parent 58c8d5e commit da5b904
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 71 deletions.
94 changes: 40 additions & 54 deletions Documentation/guides/OneDotNetSingleProject.md
Expand Up @@ -19,25 +19,27 @@ Xamarin.Android and Xamarin.iOS/Mac SDKs:

* `$(ApplicationId)` maps to `/manifest/@package` and
`CFBundleIdentifier`
* `$(ApplicationVersion)` maps to `android:versionName` or
`CFBundleVersion`. This is a version string that must be incremented
for each iOS App Store or TestFlight submission.
* `$(AndroidVersionCode)` maps to `android:versionCode` (_Android
only)_. This is unfortunately an integer and must be incremented for
each Google Play submission.
* `$(AppleShortVersion)` maps to `CFBundleShortVersionString` (_iOS
only)_. This can default to `$(ApplicationVersion)` when blank.
* `$(ApplicationVersion)` maps to `android:versionCode` or
[`CFBundleVersion`][CFBundleVersion]. This is required to be an integer on Android and
less than 10000 on iOS. This value must be incremented for each
Google Play or App Store / TestFlight submission.
* `$(ApplicationDisplayVersion)` maps to `android:versionName` or
[`CFBundleShortVersionString`][CFBundleShortVersionString]. This can
default to `$(ApplicationVersion)` when blank.
* `$(ApplicationTitle)` maps to `/application/@android:title` or
`CFBundleDisplayName`

[CFBundleVersion]: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102364
[CFBundleShortVersionString]: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-111349

The final value that is generated in the `Info.plist` or
`AndroidManifest.xml` can be overridden at different times. The final
source of truth is determined in order of:

1. `Info.plist` or `AndroidManifest.xml` in the iOS/Android head project.
2. iOS/Android head `.csproj` defines the MSBuild properties
3. _(To be implemented in MAUI/Forms)_ set in a shared `.csproj`.
4. The properties set by MSBuild via other means such as
1. `Info.plist` or `AndroidManifest.xml` in the iOS/Android project.
2. iOS/Android `.csproj` defines the MSBuild properties. This could
also be done in a .NET MAUI "Single Project".
3. The properties set by MSBuild via other means such as
`Directory.Build.props`, etc.

Even if we did not complete the goal of complete removal of
Expand All @@ -57,7 +59,7 @@ property to disable the behavior.
In most cases, developers would only use `$(GenerateApplicationManifest)`
if they want to try the new features in "legacy" Xamarin.

## AssemblyVersion and FileVersion
## Version, AssemblyVersion, FileVersion, and InformationalVersion

Since we are adding *more* version properties, we should consider
adding defaults to consolidate the assembly-level attributes when
Expand All @@ -67,16 +69,22 @@ The full list of defaults might be something like:

```xml
<PropertyGroup>
<ApplicationVersion Condition=" '$(ApplicationVersion)' == '' ">1.0</ApplicationVersion>
<!-- Android only -->
<AndroidVersionCode Condition=" '$(AndroidVersionCode)' == '' ">1</AndroidVersionCode>
<!-- Apple platforms only -->
<AppleShortVersion Condition=" '$(AppleShortVersion)' == '' ">$(ApplicationVersion)</ApplicationVersion>
<AssemblyVersion Condition=" '$(AssemblyVersion)' == '' ">$(ApplicationVersion)</AssemblyVersion>
<FileVersion Condition=" '$(FileVersion)' == '' ">$(ApplicationVersion)</FileVersion>
<ApplicationVersion Condition=" '$(ApplicationVersion)' == '' ">1</ApplicationVersion>
<Version Condition=" '$(ApplicationDisplayVersion)' != '' ">$(ApplicationDisplayVersion)</Version>
<ApplicationDisplayVersion Condition=" '$(ApplicationDisplayVersion)' == '' ">$(Version)</ApplicationDisplayVersion>
</PropertyGroup>
```

The dotnet/sdk defaults `$(Version)` to 1.0.0 and uses it to set:

* `$(AssemblyVersion)`
* `$(FileVersion)`
* `$(InformationalVersion)`

If we expect users to set `$(ApplicationVersion)` and
`$(ApplicationDisplayVersion)` in mobile apps, we can use the value of
`$(ApplicationDisplayVersion)` for `$(Version)` as well.

## Android Template

The default Android project template would include:
Expand All @@ -88,8 +96,8 @@ The default Android project template would include:
<TargetFramework>net6.0-android</TargetFramework>
<ApplicationTitle>@string/application_title</ApplicationTitle>
<ApplicationId>com.companyname.myapp</ApplicationId>
<ApplicationVersion>1.0</ApplicationVersion>
<AndroidVersionCode>1</AndroidVersionCode>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
</PropertyGroup>
</Project>

Expand Down Expand Up @@ -119,14 +127,12 @@ The default iOS project template would include:
<TargetFramework>net6.0-ios</TargetFramework>
<ApplicationTitle>MyApp</ApplicationTitle>
<ApplicationId>com.companyname.myapp</ApplicationId>
<ApplicationVersion>1.0</ApplicationVersion>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
</PropertyGroup>
</Project>
```

`$(AppleShortVersion)` can default to `$(ApplicationVersion)` when
blank.

Removed from `Info.plist` in the project template:

* `CFBundleDisplayName`
Expand All @@ -139,45 +145,26 @@ MSBuild properties.

## Example

You could setup a cross-platform solution in .NET 6 with:
The .NET MAUI project template (`dotnet new maui`):

* `Hello/Hello.csproj` - `net6.0` shared code
* `HelloAndroid/HelloAndroid.csproj` - `net6.0-android`
* `HelloiOS/HelloiOS.csproj` - `net6.0-ios`
* `Hello.sln`
* `Directory.Build.props`
* `HelloMaui/HelloMaui.csproj` - multi-targeted for `net6.0-android`,
`net6.0-ios`, `net6.0-maccatalyst`, etc.

Where `Directory.Build.props` can be setup for both platforms at once
with:
Where the versions can be setup for both platforms at once with:

```xml
<Project>
<PropertyGroup>
<ApplicationTitle>Hello!</ApplicationTitle>
<ApplicationId>com.companyname.hello</ApplicationId>
<ApplicationVersion>1.0.0</ApplicationVersion>
<AndroidVersionCode>1</AndroidVersionCode>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
</PropertyGroup>
</Project>
```

In this project, a developer would increment `$(ApplicationVersion)`
and `$(AndroidVersionCode)` for each public release.

For our long-term vision, we could one day have a single project that
multi-targets:

```xml
<Project>
<PropertyGroup>
<TargetFrameworks>net6.0-android;net6.0-ios</TargetFrameworks>
<ApplicationTitle>Hello!</ApplicationTitle>
<ApplicationId>com.companyname.hello</ApplicationId>
<ApplicationVersion>1.0.0</ApplicationVersion>
<AndroidVersionCode>1</AndroidVersionCode>
</PropertyGroup>
</Project>
```
and `$(ApplicationDisplayVersion)` for each public release.

## Localization

Expand All @@ -203,8 +190,7 @@ and Android. This is a consideration for the future.

In future iterations, we can consider additional MSBuild properties
beyond `$(ApplicationTitle)`, `$(ApplicationId)`,
`$(ApplicationVersion)`, `$(AndroidVersionCode)`, and
`$(AppleShortVersion)`.
`$(ApplicationVersion)`, and `$(ApplicationDisplayVersion)`.

This is a list of additional properties that cover most of the
property pages in Visual Studio:
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.Android.Templates/android/AndroidApp1.csproj
Expand Up @@ -6,5 +6,8 @@
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationId>com.companyname.AndroidApp1</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
</PropertyGroup>
</Project>
5 changes: 1 addition & 4 deletions src/Microsoft.Android.Templates/android/AndroidManifest.xml
@@ -1,8 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
package="com.companyname.AndroidApp1">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true">
</application>
</manifest>
Expand Up @@ -101,10 +101,10 @@
<AndroidNeedsInternetPermission Condition=" '$(AndroidEnableProfiler)' == 'true' ">True</AndroidNeedsInternetPermission>
</PropertyGroup>
<PropertyGroup Condition=" '$(AndroidApplication)' == 'true' and '$(GenerateApplicationManifest)' == 'true' ">
<ApplicationVersion Condition=" '$(ApplicationVersion)' == '' ">1.0</ApplicationVersion>
<AndroidVersionCode Condition=" '$(AndroidVersionCode)' == '' ">1</AndroidVersionCode>
<AssemblyVersion Condition=" '$(AssemblyVersion)' == '' ">$(ApplicationVersion)</AssemblyVersion>
<FileVersion Condition=" '$(FileVersion)' == '' ">$(ApplicationVersion)</FileVersion>
<!-- Default to 1, if blank -->
<ApplicationVersion Condition=" '$(ApplicationVersion)' == '' ">1</ApplicationVersion>
<Version Condition=" $([System.Version]::TryParse ('$(ApplicationDisplayVersion)', $([System.Version]::Parse('1.0')))) ">$(ApplicationDisplayVersion)</Version>
<ApplicationDisplayVersion Condition=" '$(ApplicationDisplayVersion)' == '' ">$(Version)</ApplicationDisplayVersion>
</PropertyGroup>

</Project>
@@ -1,7 +1,9 @@
using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Mono.Cecil;
using NUnit.Framework;
using Xamarin.Android.Tasks;
using Xamarin.Android.Tools;
using Xamarin.ProjectTools;

Expand All @@ -11,13 +13,35 @@ namespace Xamarin.Android.Build.Tests
[Parallelizable (ParallelScope.Children)]
public partial class SingleProjectTest : BaseTest
{
static readonly object [] AndroidManifestPropertiesSource = new object [] {
new object [] {
/* versionName */ "2.1",
/* versionCode */ "42",
/* errorMessage */ "",
},
new object [] {
/* versionName */ "1.0.0",
/* versionCode */ "1.0.0",
/* errorMessage */ "XA0003",
},
new object [] {
/* versionName */ "3.1.3a1",
/* versionCode */ "42",
/* errorMessage */ "",
},
new object [] {
/* versionName */ "6.0-preview.7",
/* versionCode */ "42",
/* errorMessage */ "",
},
};

[Test]
public void AndroidManifestProperties ()
[TestCaseSource (nameof (AndroidManifestPropertiesSource))]
public void AndroidManifestProperties (string versionName, string versionCode, string errorMessage)
{
var packageName = "com.xamarin.singleproject";
var applicationLabel = "My Sweet App";
var versionName = "2.1";
var versionCode = "42";
var proj = new XamarinAndroidApplicationProject ();
proj.AndroidManifest = proj.AndroidManifest
.Replace ("package=\"${PACKAGENAME}\"", "")
Expand All @@ -29,12 +53,18 @@ public void AndroidManifestProperties ()
}
proj.SetProperty ("ApplicationId", packageName);
proj.SetProperty ("ApplicationTitle", applicationLabel);
proj.SetProperty ("ApplicationVersion", versionName);
proj.SetProperty ("AndroidVersionCode", versionCode);
proj.SetProperty ("ApplicationVersion", versionCode);
proj.SetProperty ("ApplicationDisplayVersion", versionName);

using (var b = CreateApkBuilder ()) {
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
if (!string.IsNullOrEmpty (errorMessage)) {
b.ThrowOnBuildFailure = false;
Assert.IsFalse (b.Build (proj), "Build should have failed.");
StringAssertEx.Contains (errorMessage, b.LastBuildOutput, $"Build should fail with message '{errorMessage}'");
return;
}

Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
var manifest = b.Output.GetIntermediaryPath ("android/AndroidManifest.xml");
FileAssert.Exists (manifest);

Expand All @@ -48,6 +78,34 @@ public void AndroidManifestProperties ()

var apk = b.Output.GetIntermediaryPath ($"android/bin/{packageName}.apk");
FileAssert.Exists (apk);

// NOTE: the $(Version) setting is only implemented in .NET 6
if (!Builder.UseDotNet)
return;
// If not valid version, skip
if (!Version.TryParse (versionName, out _))
return;

int index = versionName.IndexOf ('-');
var versionNumber = index == -1 ?
$"{versionName}.0.0" :
$"{versionName.Substring (0, index)}.0.0";
var assemblyPath = b.Output.GetIntermediaryPath ($"android/assets/{proj.ProjectName}.dll");
FileAssert.Exists (assemblyPath);
using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath);

// System.Reflection.AssemblyVersion
Assert.AreEqual (versionNumber, assembly.Name.Version.ToString ());

// System.Reflection.AssemblyFileVersion
var assemblyInfoVersion = assembly.CustomAttributes.FirstOrDefault (a => a.AttributeType.FullName == "System.Reflection.AssemblyInformationalVersionAttribute");
Assert.IsNotNull (assemblyInfoVersion, "Should find AssemblyInformationalVersionAttribute!");
Assert.AreEqual (versionName, assemblyInfoVersion.ConstructorArguments [0].Value);

// System.Reflection.AssemblyInformationalVersion
var assemblyFileVersion = assembly.CustomAttributes.FirstOrDefault (a => a.AttributeType.FullName == "System.Reflection.AssemblyFileVersionAttribute");
Assert.IsNotNull (assemblyFileVersion, "Should find AssemblyFileVersionAttribute!");
Assert.AreEqual (versionNumber, assemblyFileVersion.ConstructorArguments [0].Value);
}
}
}
Expand Down
Expand Up @@ -503,8 +503,8 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
<PropertyGroup Condition=" '$(GenerateApplicationManifest)' == 'true' ">
<_AndroidPackage>$(ApplicationId)</_AndroidPackage>
<_ApplicationLabel>$(ApplicationTitle)</_ApplicationLabel>
<_AndroidVersionName>$(ApplicationVersion)</_AndroidVersionName>
<_AndroidVersionCode Condition=" '$(AndroidCreatePackagePerAbi)' != 'true' ">$(AndroidVersionCode)</_AndroidVersionCode>
<_AndroidVersionName>$(ApplicationDisplayVersion)</_AndroidVersionName>
<_AndroidVersionCode Condition=" '$(AndroidCreatePackagePerAbi)' != 'true' ">$(ApplicationVersion)</_AndroidVersionCode>
</PropertyGroup>
<AndroidError Code="XA1018"
ResourceName="XA1018"
Expand Down

0 comments on commit da5b904

Please sign in to comment.