From c1d8e33cc7f50f2ff9ddcc28a4d17ff94573dadd Mon Sep 17 00:00:00 2001 From: Dean Ellis Date: Tue, 27 Jun 2017 10:08:58 +0100 Subject: [PATCH] [Xamarin.Android.Build.Tasks] Implement a new process for defining versionCode. Context https://bugzilla.xamarin.com/show_bug.cgi?id=51620 Context https://bugzilla.xamarin.com/show_bug.cgi?id=51618 --- Documentation/build_process.md | 52 ++++++++++ src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs | 24 ++++- .../ManifestTest.cs | 94 ++++++++++++++++++- .../Utilities/ManifestDocument.cs | 48 +++++++++- .../Xamarin.Android.Common.targets | 3 + 5 files changed, 216 insertions(+), 5 deletions(-) diff --git a/Documentation/build_process.md b/Documentation/build_process.md index 48a26dc6a72..ef3f3b71d08 100644 --- a/Documentation/build_process.md +++ b/Documentation/build_process.md @@ -501,6 +501,58 @@ when packaing Release applications. Added in Xamarin.Android 7.1. +- **AndroidVersionCodePattern** &ndsh; A string property which allows + the developer to customize the `versionCode` in the manifest when splitting + up the apk by abi. + See [Creating_the_Version_Code_for_the_APK](https://developer.xamarin.com/guides/android/advanced_topics/build-abi-specific-apks/#Creating_the_Version_Code_for_the_APK) + for information on deciding a `versionCode`. + + Some examples, if `abi` is `armeabi` and `versionCode` in the manifest + is `123` + + `{abi}{versionCode}` + + will prodice a versionCode of `1123`. + If `abi` is `x86_64` and `versionCode` in the manifest + is `44`. This will produce `544`. + + If we include some left padding semantics + + `{abi}{versionCode:0000}` + + it would produde `50044` because we are left padding the `versionCode` + with `0`. + + Pre defined key items + + - **abi** &ndsh; Inserts the targetted abi for the app + - 1 – `armeabi` + - 2 – `armeabi-v7a` + - 3 – `x86` + - 4 – `arm64-v8a` + - 5 – `x86_64` + + - **minSDK** &ndsh; Inserts the minimum supported Sdk + value from the `AndroidManifest.xml` or `11` if none is + defined. + + - **versionCode** &ndsh; Uses the version code direrctly from + `Properties\AndroidManifest.xml`. + + You can define custom items using the [AndroidVersionCodeProperties] (#AndroidVersionCodeProperties) + property. + + Added in Xamarin.Android 7.2. +- **AndroidVersionCodeProperties** &ndsh; A string property which allows + the developer to define custom items to use with the [AndroidVersionCodePattern] (#AndroidVersionCodePattern). + They are in the form of a `key=value` pair. All items in the `value` should + be integer values. + + `screen=23;target=$(_SupportedApiLevel)` + + + + Added in Xamarin.Android 7.2. ## Binding Project Build Properties The following MSBuild properties are used with diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs index de9ff551bd8..b3623786ecc 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs @@ -76,6 +76,19 @@ public class Aapt : AsyncTask public bool ExplicitCrunch { get; set; } + // pattern to use for the version code. Used in CreatePackagePerAbi + // eg. {abi:00}{dd}{version} + // known keyworks + // {abi} the value for the current abi + // {version} the version code from the manifest. + public string VersionCodePattern { get; set; } + + // Name=Value pair seperated by ';' + // e.g screen=21;abi=11 + public string VersionCodeProperties { get; set; } + + public string AndroidSdkPlatform { get; set; } + Dictionary resource_name_case_map = new Dictionary (); bool ManifestIsUpToDate (string manifestFile) @@ -190,6 +203,8 @@ public override bool Execute () Log.LogDebugMessage (" ExtraArgs: {0}", ExtraArgs); Log.LogDebugMessage (" CreatePackagePerAbi: {0}", CreatePackagePerAbi); Log.LogDebugMessage (" ResourceNameCaseMap: {0}", ResourceNameCaseMap); + Log.LogDebugMessage (" VersionCodePattern: {0}", VersionCodePattern); + Log.LogDebugMessage (" VersionCodeProperties: {0}", VersionCodeProperties); if (CreatePackagePerAbi) Log.LogDebugMessage (" SupportedAbis: {0}", SupportedAbis); @@ -244,8 +259,13 @@ protected string GenerateCommandLineCommands (string ManifestFile, string curren Directory.CreateDirectory (manifestDir); manifestFile = Path.Combine (manifestDir, Path.GetFileName (ManifestFile)); ManifestDocument manifest = new ManifestDocument (ManifestFile, this.Log); - if (currentAbi != null) - manifest.SetAbi (currentAbi); + manifest.SdkVersion = AndroidSdkPlatform; + if (currentAbi != null) { + if (!string.IsNullOrEmpty (VersionCodePattern)) + manifest.CalculateVersionCode (currentAbi, VersionCodePattern, VersionCodeProperties); + else + manifest.SetAbi (currentAbi); + } manifest.ApplicationName = ApplicationName; manifest.Save (manifestFile); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs index ea6a91dd4e7..dd0d2a9354f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NUnit.Framework; using Xamarin.ProjectTools; @@ -264,6 +264,98 @@ public void DirectBootAwareAttribute () } } + static object [] VersionCodeTestSource = new object [] { + new object[] { + /* seperateApk */ false, + /* abis */ "armeabi-v7a", + /* versionCode */ "123", + /* pattern */ null, + /* props */ null, + /* expected */ "123", + }, + new object[] { + /* seperateApk */ false, + /* abis */ "armeabi-v7a", + /* versionCode */ "123", + /* pattern */ "{abi}{versionCode}", + /* props */ null, + /* expected */ "123", + }, + new object[] { + /* seperateApk */ false, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "123", + /* pattern */ "{abi}{versionCode}", + /* props */ null, + /* expected */ "123", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "123", + /* pattern */ null, + /* props */ null, + /* expected */ "131195;196731", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "123", + /* pattern */ "{abi}{versionCode}", + /* props */ null, + /* expected */ "2123;3123", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "12", + /* pattern */ "{abi}{minSDK:00}{versionCode:000}", + /* props */ null, + /* expected */ "211012;311012", + }, + }; + + [Test] + [TestCaseSource("VersionCodeTestSource")] + public void VersionCodeTests (bool seperateApk, string abis, string versionCode, string versionCodePattern, string versionCodeProperties, string expected) + { + var proj = new XamarinAndroidApplicationProject () { + IsRelease = true, + }; + proj.SetProperty (proj.ReleaseProperties, KnownProperties.AndroidCreatePackagePerAbi, seperateApk); + if (!string.IsNullOrEmpty (abis)) + proj.SetProperty (proj.ReleaseProperties, KnownProperties.AndroidSupportedAbis, abis); + if (!string.IsNullOrEmpty (versionCodePattern)) + proj.SetProperty (proj.ReleaseProperties, "AndroidVersionCodePattern", versionCodePattern); + else + proj.RemoveProperty (proj.ReleaseProperties, "AndroidVersionCodePattern"); + if (!string.IsNullOrEmpty (versionCodeProperties)) + proj.SetProperty (proj.ReleaseProperties, "AndroidVersionCodeProperties", versionCodeProperties); + else + proj.RemoveProperty (proj.ReleaseProperties, "AndroidVersionCodeProperties"); + proj.AndroidManifest = proj.AndroidManifest.Replace ("android:versionCode=\"1\"", $"android:versionCode=\"{versionCode}\""); + using (var builder = CreateApkBuilder (Path.Combine ("temp", "VersionCodeTests"), false, false)) { + builder.Build (proj); + var abiItems = seperateApk ? abis.Split (';') : new string[1]; + var expectedItems = expected.Split (';'); + XNamespace aNS = "http://schemas.android.com/apk/res/android"; + Assert.AreEqual (abiItems.Length, expectedItems.Length, "abis parameter should have matching elements for expected"); + for (int i = 0; i < abiItems.Length; i++) { + var path = seperateApk ? Path.Combine ("android", abiItems[i], "AndroidManifest.xml") : Path.Combine ("android", "AndroidManifest.xml"); + var manifest = builder.Output.GetIntermediaryAsText (Root, path); + var doc = XDocument.Parse (manifest); + var nsResolver = new XmlNamespaceManager (new NameTable ()); + nsResolver.AddNamespace ("android", "http://schemas.android.com/apk/res/android"); + var m = doc.XPathSelectElement ("/manifest") as XElement; + Assert.IsNotNull (m, "no manifest element found"); + var vc = m.Attribute (aNS + "versionCode"); + Assert.IsNotNull (vc, "no versionCode attribute found"); + StringAssert.AreEqualIgnoringCase (expectedItems[i], vc.Value, + $"Version Code is incorrect. Found {vc.Value} expect {expectedItems[i]}"); + } + } + } + [Test] public void ManifestPlaceholders () { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs index 7bac3b1e53e..174f8abea06 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs @@ -29,6 +29,8 @@ internal class ManifestDocument { public static XNamespace AndroidXmlNamespace = "http://schemas.android.com/apk/res/android"; + const int maxVersionCode = 2100000000; + static XNamespace androidNs = AndroidXmlNamespace; XDocument doc; @@ -68,6 +70,19 @@ public string VersionCode { doc.Root.SetAttributeValue (androidNs + "versionCode", value); } } + public string MinimumSdk { + get { + var uses = doc.Root.Element ("uses-sdk"); + if (uses?.Attribute (androidNs + "minSdkVersion") == null) { + int minSdkVersion; + if (!int.TryParse (SdkVersionName, out minSdkVersion)) + minSdkVersion = 11; + return Math.Min (minSdkVersion, 11).ToString (); + } else { + return uses.Attribute (androidNs + "minSdkVersion").Value; + } + } + } TaskLoggingHelper log; public ManifestDocument (string templateFilename, TaskLoggingHelper log) : base () @@ -839,11 +854,40 @@ public void SetAbi (string abi) int code = 1; if (!string.IsNullOrEmpty (VersionCode)) { code = Convert.ToInt32 (VersionCode); - if (code > 0xffff || code < 0) - throw new ArgumentOutOfRangeException ("VersionCode", "VersionCode is outside 0, 65535 interval"); + if (code > maxVersionCode || code < 0) + throw new ArgumentOutOfRangeException ("VersionCode", $"VersionCode is outside 0, {maxVersionCode} interval"); } code |= GetAbiCode (abi) << 16; VersionCode = code.ToString (); } + + public void CalculateVersionCode (string currentAbi, string versionCodePattern, string versionCodeProperties) + { + var regex = new Regex ("\\{(?([A-Za-z]*)):?0*[\\}]"); + var kvp = new Dictionary (); + foreach (var item in versionCodeProperties?.Split (new char [] { ';' }) ?? new string [0]) { + var a = item.Split (new char [] { '=' }); + kvp.Add (a [0], int.Parse (a [1])); + } + if (!kvp.ContainsKey ("abi")) + kvp.Add ("abi", GetAbiCode (currentAbi)); + if (!kvp.ContainsKey ("versionCode")) + kvp.Add ("versionCode", int.Parse (VersionCode)); + if (!kvp.ContainsKey ("minSDK")) { + kvp.Add ("minSDK", int.Parse (MinimumSdk)); + } + var versionCode = String.Empty; + foreach (Match match in regex.Matches (versionCodePattern)) { + var key = match.Groups ["key"].Value; + var format = match.Value.Replace (key, "0"); + if (!kvp.ContainsKey (key)) + continue; + versionCode += string.Format (format, kvp [key]); + } + var code = Convert.ToInt32 (versionCode); + if (code > maxVersionCode || code < 0) + throw new ArgumentOutOfRangeException ("VersionCode", $"VersionCode {code} is outside 0, {maxVersionCode} interval"); + VersionCode = versionCode; + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index d6c956a4d48..875f376a1ea 100755 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1807,6 +1807,9 @@ because xbuild doesn't support framework reference assemblies. CreatePackagePerAbi="$(AndroidCreatePackagePerAbi)" YieldDuringToolExecution="$(YieldDuringToolExecution)" ExplicitCrunch="$(AndroidExplicitCrunch)" + VersionCodePattern="$(AndroidVersionCodePattern)" + VersionCodeProperties="$(AndroidVersionCodeProperties)" + AndroidSdkPlatform="$(_AndroidApiLevel)" />