diff --git a/Documentation/guides/OneDotNetSingleProject.md b/Documentation/guides/OneDotNetSingleProject.md index a534a877bd0..babdac788af 100644 --- a/Documentation/guides/OneDotNetSingleProject.md +++ b/Documentation/guides/OneDotNetSingleProject.md @@ -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 @@ -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 @@ -67,16 +69,22 @@ The full list of defaults might be something like: ```xml - 1.0 - - 1 - - $(ApplicationVersion) - $(ApplicationVersion) - $(ApplicationVersion) + 1 + $(ApplicationDisplayVersion) + $(Version) ``` +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: @@ -88,8 +96,8 @@ The default Android project template would include: net6.0-android @string/application_title com.companyname.myapp - 1.0 - 1 + 1 + 1.0 @@ -119,14 +127,12 @@ The default iOS project template would include: net6.0-ios MyApp com.companyname.myapp - 1.0 + 1 + 1.0 ``` -`$(AppleShortVersion)` can default to `$(ApplicationVersion)` when -blank. - Removed from `Info.plist` in the project template: * `CFBundleDisplayName` @@ -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 Hello! com.companyname.hello - 1.0.0 - 1 + 1 + 1.0 ``` 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 - - - net6.0-android;net6.0-ios - Hello! - com.companyname.hello - 1.0.0 - 1 - - -``` +and `$(ApplicationDisplayVersion)` for each public release. ## Localization @@ -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: diff --git a/src/Microsoft.Android.Templates/android/AndroidApp1.csproj b/src/Microsoft.Android.Templates/android/AndroidApp1.csproj index 906e1314de1..16f9ea6f3e5 100644 --- a/src/Microsoft.Android.Templates/android/AndroidApp1.csproj +++ b/src/Microsoft.Android.Templates/android/AndroidApp1.csproj @@ -6,5 +6,8 @@ Exe enable enable + com.companyname.AndroidApp1 + 1 + 1.0 \ No newline at end of file diff --git a/src/Microsoft.Android.Templates/android/AndroidManifest.xml b/src/Microsoft.Android.Templates/android/AndroidManifest.xml index c669bdfaa58..d9115795753 100644 --- a/src/Microsoft.Android.Templates/android/AndroidManifest.xml +++ b/src/Microsoft.Android.Templates/android/AndroidManifest.xml @@ -1,8 +1,5 @@ - + \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets index ffef701783d..ccacac7e0fb 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets @@ -101,10 +101,10 @@ True - 1.0 - 1 - $(ApplicationVersion) - $(ApplicationVersion) + + 1 + $(ApplicationDisplayVersion) + $(Version) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/SingleProjectTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/SingleProjectTest.cs index 294278bb59f..52bcb932d8b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/SingleProjectTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/SingleProjectTest.cs @@ -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; @@ -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}\"", "") @@ -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); @@ -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); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 61d9b1ef053..5566a3cb460 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -503,8 +503,8 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidPackage>$(ApplicationId) <_ApplicationLabel>$(ApplicationTitle) - <_AndroidVersionName>$(ApplicationVersion) - <_AndroidVersionCode Condition=" '$(AndroidCreatePackagePerAbi)' != 'true' ">$(AndroidVersionCode) + <_AndroidVersionName>$(ApplicationDisplayVersion) + <_AndroidVersionCode Condition=" '$(AndroidCreatePackagePerAbi)' != 'true' ">$(ApplicationVersion)