diff --git a/Documentation/build_process.md b/Documentation/build_process.md index 48a26dc6a72..f038b9e3e84 100644 --- a/Documentation/build_process.md +++ b/Documentation/build_process.md @@ -10,10 +10,10 @@ dateupdated: 2017-06-22 The Xamarin.Android build process is responsible for gluing everything together: -[generating `Resource.designer.cs`](/guides/android/advanced_topics/api_design#Resources), +[generating `Resource.designer.cs`](https://developer.xamarin.com/guides/android/advanced_topics/api_design#Resources), supporting the `AndroidAsset`, `AndroidResource`, and other [build actions](#Build_Actions), generating -[Android-callable wrappers](/guides/android/advanced_topics/java_integration_overview/android_callable_wrappers), +[Android-callable wrappers](https://developer.xamarin.com/guides/android/advanced_topics/java_integration_overview/android_callable_wrappers), and generating a `.apk` for execution on Android devices. @@ -247,7 +247,7 @@ when packaing Release applications. Added in Xamarin.Android 6.1. - **AndroidHttpClientHandlerType** – Allow setting the value of the - [`XA_HTTP_CLIENT_HANDLER_TYPE` environment variable](/guides/android/advanced_topics/environment/#XA_HTTP_CLIENT_HANDLER_TYPE). + [`XA_HTTP_CLIENT_HANDLER_TYPE` environment variable](https://developer.xamarin.com/guides/android/advanced_topics/environment/#XA_HTTP_CLIENT_HANDLER_TYPE). This value will not override an explicitly specified `XA_HTTP_CLIENT_HANDLER_TYPE` value. An `XA_HTTP_CLIENT_HANDLER_TYPE` environment variable value specified @@ -278,7 +278,7 @@ when packaing Release applications. **Experimental**. Added in Xamarin.Android 7.1. - **AndroidLinkMode** – Specifies which type of - [linking](/guides/android/advanced_topics/linking/) should be + [linking](https://developer.xamarin.com/guides/android/advanced_topics/linking/) should be performed on assemblies contained within the Android package. Only used in Android Application projects. The default value is *SdkOnly*. Valid values are: @@ -310,7 +310,7 @@ when packaing Release applications. - **AndroidManifest** – Specifies a filename to use as the template for the app's - [`AndroidManifest.xml`](/guides/android/advanced_topics/working_with_androidmanifest.xml/). + [`AndroidManifest.xml`](https://developer.xamarin.com/guides/android/advanced_topics/working_with_androidmanifest.xml/). During the build, any other necessary values will be merged into to produce the actual `AndroidManifest.xml`. The `$(AndroidManifest)` must contain the package name in the `/manifest/@package` attribute. @@ -501,6 +501,71 @@ when packaing Release applications. Added in Xamarin.Android 7.1. + +- **AndroidVersionCodePattern** – A string property which allows + the developer to customize the `versionCode` in the manifest. + 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 produce a versionCode of `1123` when `$(AndroidCreatePackagePerAbi)` + is True, otherwise will produce a value of 123. + If `abi` is `x86_64` and `versionCode` in the manifest + is `44`. This will produce `544` when `$(AndroidCreatePackagePerAbi)` + is True, otherwise will produce a value of `44`. + + If we include a left padding format string + + {abi}{versionCode:0000} + + it would produde `50044` because we are left padding the `versionCode` + with `0`. Alternatively you can use the decimal padding such as + + {abi}{versionCode:D4} + + which does the same as the previous example. + + Only '0' and 'Dx' padding format strings are supported since the value + MUST be an integer. + + Pre defined key items + + - **abi** – Inserts the targetted abi for the app + - 1 – `armeabi` + - 2 – `armeabi-v7a` + - 3 – `x86` + - 4 – `arm64-v8a` + - 5 – `x86_64` + + - **minSDK** – Inserts the minimum supported Sdk + value from the `AndroidManifest.xml` or `11` if none is + defined. + + - **versionCode** – 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** – 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) + + As you can see you can make use of existing or custom MSBuild properties + in the string. + + Added in Xamarin.Android 7.2. + ## Binding Project Build Properties The following MSBuild properties are used with @@ -675,7 +740,7 @@ within the project and control how the file is processed. ## AndroidEnvironment Files with a Build action of `AndroidEnvironment` are used -to [initialize environment variables and system properties during process startup](/guides/android/advanced_topics/environment/). +to [initialize environment variables and system properties during process startup](https://developer.xamarin.com/guides/android/advanced_topics/environment/). The `AndroidEnvironment` Build action may be applied to multiple files, and they will be evaluated in no particular order (so don't specify the same environment variable or system property in multiple @@ -748,7 +813,7 @@ example: ## AndroidNativeLibrary -[Native libraries](/guides/android/advanced_topics/cpu_architecture/#Android_Native_Library_Installation) +[Native libraries](https://developer.xamarin.com/guides/android/advanced_topics/cpu_architecture/#Android_Native_Library_Installation) are added to the build by setting their Build action to `AndroidNativeLibrary`. @@ -787,7 +852,7 @@ Build action will result in a `XA0101` warning. ## LinkDescription Files with a *LinkDescription* build action are used to -[control linker behavior](/guides/cross-platform/advanced/custom_linking/). +[control linker behavior](https://developer.xamarin.com/guides/cross-platform/advanced/custom_linking/). @@ -797,7 +862,7 @@ Files with a *LinkDescription* build action are used to Files with a *ProguardConfiguration* build action contain options which are used to control `proguard` behavior. For more information about this build action, see -[ProGuard](/guides/android/deployment,_testing,_and_metrics/proguard/). +[ProGuard](https://developer.xamarin.com/guides/android/deployment,_testing,_and_metrics/proguard/). These files are ignored unless the `$(EnableProguard)` MSBuild property is `True`. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs index de9ff551bd8..54215c603c7 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,15 @@ 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); + } else if (!string.IsNullOrEmpty (VersionCodePattern)) { + manifest.CalculateVersionCode (null, VersionCodePattern, VersionCodeProperties); + } 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..99e5003c49d 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,144 @@ public void DirectBootAwareAttribute () } } + static object [] VersionCodeTestSource = new object [] { + new object[] { + /* seperateApk */ false, + /* abis */ "armeabi-v7a", + /* versionCode */ "123", + /* pattern */ null, + /* props */ null, + /* shouldBuild */ true, + /* expected */ "123", + }, + new object[] { + /* seperateApk */ false, + /* abis */ "armeabi-v7a", + /* versionCode */ "123", + /* pattern */ "{abi}{versionCode}", + /* props */ null, + /* shouldBuild */ true, + /* expected */ "123", + }, + new object[] { + /* seperateApk */ false, + /* abis */ "armeabi-v7a", + /* versionCode */ "1", + /* pattern */ "{abi}{versionCode}", + /* props */ "versionCode=123", + /* shouldBuild */ true, + /* expected */ "123", + }, + new object[] { + /* seperateApk */ false, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "123", + /* pattern */ "{abi}{versionCode}", + /* props */ null, + /* shouldBuild */ true, + /* expected */ "123", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "123", + /* pattern */ null, + /* props */ null, + /* shouldBuild */ true, + /* expected */ "131195;196731", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "123", + /* pattern */ "{abi}{versionCode}", + /* props */ null, + /* shouldBuild */ true, + /* expected */ "2123;3123", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "12", + /* pattern */ "{abi}{minSDK:00}{versionCode:000}", + /* props */ null, + /* shouldBuild */ true, + /* expected */ "211012;311012", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "12", + /* pattern */ "{abi}{minSDK:00}{screen}{versionCode:000}", + /* props */ "screen=24", + /* shouldBuild */ true, + /* expected */ "21124012;31124012", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "12", + /* pattern */ "{abi}{minSDK:00}{screen}{foo:0}{versionCode:000}", + /* props */ "screen=24;foo=$(Foo)", + /* shouldBuild */ true, + /* expected */ "211241012;311241012", + }, + new object[] { + /* seperateApk */ true, + /* abis */ "armeabi-v7a;x86", + /* versionCode */ "12", + /* pattern */ "{abi}{minSDK:00}{screen}{foo:00}{versionCode:000}", + /* props */ "screen=24;foo=$(Foo)", + /* shouldBuild */ false, + /* expected */ "2112401012;3112401012", + }, + }; + + [Test] + [TestCaseSource("VersionCodeTestSource")] + public void VersionCodeTests (bool seperateApk, string abis, string versionCode, string versionCodePattern, string versionCodeProperties, bool shouldBuild, string expectedVersionCode) + { + var proj = new XamarinAndroidApplicationProject () { + IsRelease = true, + }; + proj.SetProperty ("Foo", "1"); + 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.ThrowOnBuildFailure = false; + Assert.AreEqual (shouldBuild, builder.Build (proj), shouldBuild ? "Build should have succeeded." : "Build should have failed."); + if (!shouldBuild) + return; + var abiItems = seperateApk ? abis.Split (';') : new string[1]; + var expectedItems = expectedVersionCode.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", "manifest", "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..b05e022ebf5 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,17 @@ public string VersionCode { doc.Root.SetAttributeValue (androidNs + "versionCode", value); } } + public string GetMinimumSdk () { + var minAttr = doc.Root.Element ("uses-sdk")?.Attribute (androidNs + "minSdkVersion"); + if (minAttr == null) { + int minSdkVersion; + if (!int.TryParse (SdkVersionName, out minSdkVersion)) + minSdkVersion = 11; + return Math.Min (minSdkVersion, 11).ToString (); + } + return minAttr.Value; + } + TaskLoggingHelper log; public ManifestDocument (string templateFilename, TaskLoggingHelper log) : base () @@ -839,11 +852,45 @@ 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]+)):?[D0-9]*[\\}]"); + var kvp = new Dictionary (); + foreach (var item in versionCodeProperties?.Split (new char [] { ';', ':' }) ?? new string [0]) { + var keyValue = item.Split (new char [] { '=' }); + int val; + if (!int.TryParse (keyValue [1], out val)) + continue; + kvp.Add (keyValue [0], val); + } + if (!kvp.ContainsKey ("abi") && !string.IsNullOrEmpty (currentAbi)) + kvp.Add ("abi", GetAbiCode (currentAbi)); + if (!kvp.ContainsKey ("versionCode")) + kvp.Add ("versionCode", int.Parse (VersionCode)); + if (!kvp.ContainsKey ("minSDK")) { + kvp.Add ("minSDK", int.Parse (GetMinimumSdk ())); + } + 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]); + } + int code; + if (!int.TryParse (versionCode, out code)) + throw new ArgumentOutOfRangeException ("VersionCode", $"VersionCode {versionCode} is invalid. It must be an integer value."); + 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 68050ff759f..78db70caaa3 100755 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1810,6 +1810,9 @@ because xbuild doesn't support framework reference assemblies. CreatePackagePerAbi="$(AndroidCreatePackagePerAbi)" YieldDuringToolExecution="$(YieldDuringToolExecution)" ExplicitCrunch="$(AndroidExplicitCrunch)" + VersionCodePattern="$(AndroidVersionCodePattern)" + VersionCodeProperties="$(AndroidVersionCodeProperties)" + AndroidSdkPlatform="$(_AndroidApiLevel)" />