Skip to content

Commit b07d47f

Browse files
committed
[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 Context https://bugzilla.xamarin.com/show_bug.cgi?id=51145 Our current version system for multiple apk's for each Abi is a bit broken [1]. If a user for example has a versionCode set which is 123 the final version code for an x86_64 build ends up as 327803. This is completely transparent to the user and also does not follow the guidance in the documentation at [1] and [2]. So we need a new system :) but as usual we have to support the old system. So we are introducing a new system which is more flexible. This will only apply when the `$(AndroidCreatePackagePerAbi)` is set to `True`. The new system has two new properties <AndroidVersionCodePattern/> <AndroidVersionCodeProperties/> The first allows the developer to define the Pattern to be used for the versonCode. The pattern will be made up of a format string which will contain keys. These keys will be replaced with values form one of the known keys or a custom user defined one. We define a few known key values - abi : The current target abi converted to an int where - 'armeabi' = 1, - 'armeabi-v7a' = 2, - 'x86' = 3, - 'arm64-v8a' = 4, - 'x86_64' = 5, - minSDK : The minSDK value from the manifest or 11 if not present. - versionCode : The versionCode from the manifest. With these keys the user can define a pattern of {abi}{minSDK}{versionCode} or if they way to include zero padding they can use {abi}{minSDK}{versionCode:D4} similar to the left padding formats used in string.Format (). Users can also use the `$(AndroidVersionCodeProperties)` property to define new custom keys. This string will be in the form of a semi-colon delimited key=value pairs. For example foo=12;bar=$(SomeBuildProperty) when can then be used in the pattern. {abi}{foo}{bar}{versionCode} Lets work through an example. The user defines a version code of '123' in the manifest and enables `$(AndroidCreatePackagePerAbi)`. They define a `$(AndroidVersionCodePattern)` of `{abi}{versionCode:D5}`. This will result in the following version code being produced for the 'x86' build. 300123 The first 3 is the `{abi}` value. The rest is the left zero padded versionCode. A slightly more complex pattern would be `{abi}{minSDK:D2}{versionCode:D4}` which would produce 3140123 if the minimumSdk value was set to API 14. A more real life example mgiht be as follows. A user wants to use the `Build` value from the AssemblyInfo.cs . They define the following target ```xml <Target Name="_GetBuild" AfterTargets="Compile"> <GetAssemblyIdentity AssemblyFiles="Foo.dll"> <Output TaskParameter="Assemblies" ItemName="MasterVersion"/> </GetAssemblyIdentity> <PropertyGroup> <BuildVersion>$([System.Version]::Parse(%(MasterVersion.Version)).Build)</BuildVersion> </PropertyGroup> </Target> ``` This extracts the build version from the built assembly. They can then define a pattern of {abi}{minSDK}{build:D4} and set the properties to build=$(BuildVersion) Given similar properties from the previous example e.g abi=x86 and minSDk=14, this will result in the follwing output (assuming the `Build` value was 3421). 3143421 [1] https://developer.xamarin.com/guides/android/advanced_topics/build-abi-specific-apks/ [2] https://developer.android.com/google/play/publishing/multiple-apks.html#Rules
1 parent 42ec1af commit b07d47f

File tree

5 files changed

+279
-5
lines changed

5 files changed

+279
-5
lines changed

Documentation/build_process.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,68 @@ when packaing Release applications.
501501

502502
Added in Xamarin.Android 7.1.
503503

504+
- **AndroidVersionCodePattern** &ndash; A string property which allows
505+
the developer to customize the `versionCode` in the manifest when splitting
506+
up the apk by abi.
507+
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)
508+
for information on deciding a `versionCode`.
509+
510+
Some examples, if `abi` is `armeabi` and `versionCode` in the manifest
511+
is `123`
512+
513+
{abi}{versionCode}
514+
515+
will prodice a versionCode of `1123`.
516+
If `abi` is `x86_64` and `versionCode` in the manifest
517+
is `44`. This will produce `544`.
518+
519+
If we include a left padding format string
520+
521+
{abi}{versionCode:0000}
522+
523+
it would produde `50044` because we are left padding the `versionCode`
524+
with `0`. Alternatively you can use the decimal padding such as
525+
526+
{abi}{versionCode:D4}
527+
528+
which does the same as the previous example.
529+
530+
Only '0' and 'Dx' padding format strings are supported since the value
531+
MUST be an integer.
532+
533+
Pre defined key items
534+
535+
- **abi** &ndash; Inserts the targetted abi for the app
536+
- 1 &ndash; `armeabi`
537+
- 2 &ndash; `armeabi-v7a`
538+
- 3 &ndash; `x86`
539+
- 4 &ndash; `arm64-v8a`
540+
- 5 &ndash; `x86_64`
541+
542+
- **minSDK** &ndash; Inserts the minimum supported Sdk
543+
value from the `AndroidManifest.xml` or `11` if none is
544+
defined.
545+
546+
- **versionCode** &ndash; Uses the version code direrctly from
547+
`Properties\AndroidManifest.xml`.
548+
549+
You can define custom items using the [AndroidVersionCodeProperties](#AndroidVersionCodeProperties)
550+
property.
551+
552+
Added in Xamarin.Android 7.2.
553+
554+
- **AndroidVersionCodeProperties** &ndash; A string property which allows
555+
the developer to define custom items to use with the [AndroidVersionCodePattern](#AndroidVersionCodePattern).
556+
They are in the form of a `key=value` pair. All items in the `value` should
557+
be integer values.
558+
559+
screen=23;target=$(_SupportedApiLevel)
560+
561+
As you can see you can make use of existing or custom MSBuild properties
562+
in the string.
563+
564+
Added in Xamarin.Android 7.2.
565+
504566
## Binding Project Build Properties
505567

506568
The following MSBuild properties are used with

src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,19 @@ public class Aapt : AsyncTask
7676

7777
public bool ExplicitCrunch { get; set; }
7878

79+
// pattern to use for the version code. Used in CreatePackagePerAbi
80+
// eg. {abi:00}{dd}{version}
81+
// known keyworks
82+
// {abi} the value for the current abi
83+
// {version} the version code from the manifest.
84+
public string VersionCodePattern { get; set; }
85+
86+
// Name=Value pair seperated by ';'
87+
// e.g screen=21;abi=11
88+
public string VersionCodeProperties { get; set; }
89+
90+
public string AndroidSdkPlatform { get; set; }
91+
7992
Dictionary<string,string> resource_name_case_map = new Dictionary<string,string> ();
8093

8194
bool ManifestIsUpToDate (string manifestFile)
@@ -190,6 +203,8 @@ public override bool Execute ()
190203
Log.LogDebugMessage (" ExtraArgs: {0}", ExtraArgs);
191204
Log.LogDebugMessage (" CreatePackagePerAbi: {0}", CreatePackagePerAbi);
192205
Log.LogDebugMessage (" ResourceNameCaseMap: {0}", ResourceNameCaseMap);
206+
Log.LogDebugMessage (" VersionCodePattern: {0}", VersionCodePattern);
207+
Log.LogDebugMessage (" VersionCodeProperties: {0}", VersionCodeProperties);
193208
if (CreatePackagePerAbi)
194209
Log.LogDebugMessage (" SupportedAbis: {0}", SupportedAbis);
195210

@@ -244,8 +259,15 @@ protected string GenerateCommandLineCommands (string ManifestFile, string curren
244259
Directory.CreateDirectory (manifestDir);
245260
manifestFile = Path.Combine (manifestDir, Path.GetFileName (ManifestFile));
246261
ManifestDocument manifest = new ManifestDocument (ManifestFile, this.Log);
247-
if (currentAbi != null)
248-
manifest.SetAbi (currentAbi);
262+
manifest.SdkVersion = AndroidSdkPlatform;
263+
if (currentAbi != null) {
264+
if (!string.IsNullOrEmpty (VersionCodePattern))
265+
manifest.CalculateVersionCode (currentAbi, VersionCodePattern, VersionCodeProperties);
266+
else
267+
manifest.SetAbi (currentAbi);
268+
} else if ((manifest.VersionCode == "1" || string.IsNullOrEmpty (manifest.VersionCode)) && !string.IsNullOrEmpty (VersionCodePattern)) {
269+
manifest.CalculateVersionCode (null, VersionCodePattern, VersionCodeProperties);
270+
}
249271
manifest.ApplicationName = ApplicationName;
250272
manifest.Save (manifestFile);
251273

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Linq;
33
using NUnit.Framework;
44
using Xamarin.ProjectTools;
@@ -264,6 +264,144 @@ public void DirectBootAwareAttribute ()
264264
}
265265
}
266266

267+
static object [] VersionCodeTestSource = new object [] {
268+
new object[] {
269+
/* seperateApk */ false,
270+
/* abis */ "armeabi-v7a",
271+
/* versionCode */ "123",
272+
/* pattern */ null,
273+
/* props */ null,
274+
/* shouldBuild */ true,
275+
/* expected */ "123",
276+
},
277+
new object[] {
278+
/* seperateApk */ false,
279+
/* abis */ "armeabi-v7a",
280+
/* versionCode */ "123",
281+
/* pattern */ "{abi}{versionCode}",
282+
/* props */ null,
283+
/* shouldBuild */ true,
284+
/* expected */ "123",
285+
},
286+
new object[] {
287+
/* seperateApk */ false,
288+
/* abis */ "armeabi-v7a",
289+
/* versionCode */ "1",
290+
/* pattern */ "{abi}{versionCode}",
291+
/* props */ "versionCode=123",
292+
/* shouldBuild */ true,
293+
/* expected */ "123",
294+
},
295+
new object[] {
296+
/* seperateApk */ false,
297+
/* abis */ "armeabi-v7a;x86",
298+
/* versionCode */ "123",
299+
/* pattern */ "{abi}{versionCode}",
300+
/* props */ null,
301+
/* shouldBuild */ true,
302+
/* expected */ "123",
303+
},
304+
new object[] {
305+
/* seperateApk */ true,
306+
/* abis */ "armeabi-v7a;x86",
307+
/* versionCode */ "123",
308+
/* pattern */ null,
309+
/* props */ null,
310+
/* shouldBuild */ true,
311+
/* expected */ "131195;196731",
312+
},
313+
new object[] {
314+
/* seperateApk */ true,
315+
/* abis */ "armeabi-v7a;x86",
316+
/* versionCode */ "123",
317+
/* pattern */ "{abi}{versionCode}",
318+
/* props */ null,
319+
/* shouldBuild */ true,
320+
/* expected */ "2123;3123",
321+
},
322+
new object[] {
323+
/* seperateApk */ true,
324+
/* abis */ "armeabi-v7a;x86",
325+
/* versionCode */ "12",
326+
/* pattern */ "{abi}{minSDK:00}{versionCode:000}",
327+
/* props */ null,
328+
/* shouldBuild */ true,
329+
/* expected */ "211012;311012",
330+
},
331+
new object[] {
332+
/* seperateApk */ true,
333+
/* abis */ "armeabi-v7a;x86",
334+
/* versionCode */ "12",
335+
/* pattern */ "{abi}{minSDK:00}{screen}{versionCode:000}",
336+
/* props */ "screen=24",
337+
/* shouldBuild */ true,
338+
/* expected */ "21124012;31124012",
339+
},
340+
new object[] {
341+
/* seperateApk */ true,
342+
/* abis */ "armeabi-v7a;x86",
343+
/* versionCode */ "12",
344+
/* pattern */ "{abi}{minSDK:00}{screen}{foo:0}{versionCode:000}",
345+
/* props */ "screen=24;foo=$(Foo)",
346+
/* shouldBuild */ true,
347+
/* expected */ "211241012;311241012",
348+
},
349+
new object[] {
350+
/* seperateApk */ true,
351+
/* abis */ "armeabi-v7a;x86",
352+
/* versionCode */ "12",
353+
/* pattern */ "{abi}{minSDK:00}{screen}{foo:00}{versionCode:000}",
354+
/* props */ "screen=24;foo=$(Foo)",
355+
/* shouldBuild */ false,
356+
/* expected */ "2112401012;3112401012",
357+
},
358+
};
359+
360+
[Test]
361+
[TestCaseSource("VersionCodeTestSource")]
362+
public void VersionCodeTests (bool seperateApk, string abis, string versionCode, string versionCodePattern, string versionCodeProperties, bool shouldBuild, string expectedVersionCode)
363+
{
364+
var proj = new XamarinAndroidApplicationProject () {
365+
IsRelease = true,
366+
};
367+
proj.SetProperty ("Foo", "1");
368+
proj.SetProperty (proj.ReleaseProperties, KnownProperties.AndroidCreatePackagePerAbi, seperateApk);
369+
if (!string.IsNullOrEmpty (abis))
370+
proj.SetProperty (proj.ReleaseProperties, KnownProperties.AndroidSupportedAbis, abis);
371+
if (!string.IsNullOrEmpty (versionCodePattern))
372+
proj.SetProperty (proj.ReleaseProperties, "AndroidVersionCodePattern", versionCodePattern);
373+
else
374+
proj.RemoveProperty (proj.ReleaseProperties, "AndroidVersionCodePattern");
375+
if (!string.IsNullOrEmpty (versionCodeProperties))
376+
proj.SetProperty (proj.ReleaseProperties, "AndroidVersionCodeProperties", versionCodeProperties);
377+
else
378+
proj.RemoveProperty (proj.ReleaseProperties, "AndroidVersionCodeProperties");
379+
proj.AndroidManifest = proj.AndroidManifest.Replace ("android:versionCode=\"1\"", $"android:versionCode=\"{versionCode}\"");
380+
using (var builder = CreateApkBuilder (Path.Combine ("temp", "VersionCodeTests"), false, false)) {
381+
builder.ThrowOnBuildFailure = false;
382+
Assert.AreEqual (shouldBuild, builder.Build (proj), shouldBuild ? "Build should have succeeded." : "Build should have failed.");
383+
if (!shouldBuild)
384+
return;
385+
var abiItems = seperateApk ? abis.Split (';') : new string[1];
386+
var expectedItems = expectedVersionCode.Split (';');
387+
XNamespace aNS = "http://schemas.android.com/apk/res/android";
388+
Assert.AreEqual (abiItems.Length, expectedItems.Length, "abis parameter should have matching elements for expected");
389+
for (int i = 0; i < abiItems.Length; i++) {
390+
var path = seperateApk ? Path.Combine ("android", abiItems[i], "AndroidManifest.xml") : Path.Combine ("android", "manifest", "AndroidManifest.xml");
391+
var manifest = builder.Output.GetIntermediaryAsText (Root, path);
392+
var doc = XDocument.Parse (manifest);
393+
var nsResolver = new XmlNamespaceManager (new NameTable ());
394+
nsResolver.AddNamespace ("android", "http://schemas.android.com/apk/res/android");
395+
var m = doc.XPathSelectElement ("/manifest") as XElement;
396+
Assert.IsNotNull (m, "no manifest element found");
397+
var vc = m.Attribute (aNS + "versionCode");
398+
Assert.IsNotNull (vc, "no versionCode attribute found");
399+
StringAssert.AreEqualIgnoringCase (expectedItems[i], vc.Value,
400+
$"Version Code is incorrect. Found {vc.Value} expect {expectedItems[i]}");
401+
}
402+
}
403+
}
404+
267405
[Test]
268406
public void ManifestPlaceholders ()
269407
{

src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ internal class ManifestDocument
2929
{
3030
public static XNamespace AndroidXmlNamespace = "http://schemas.android.com/apk/res/android";
3131

32+
const int maxVersionCode = 2100000000;
33+
3234
static XNamespace androidNs = AndroidXmlNamespace;
3335

3436
XDocument doc;
@@ -68,6 +70,19 @@ public string VersionCode {
6870
doc.Root.SetAttributeValue (androidNs + "versionCode", value);
6971
}
7072
}
73+
public string MinimumSdk {
74+
get {
75+
var uses = doc.Root.Element ("uses-sdk");
76+
if (uses?.Attribute (androidNs + "minSdkVersion") == null) {
77+
int minSdkVersion;
78+
if (!int.TryParse (SdkVersionName, out minSdkVersion))
79+
minSdkVersion = 11;
80+
return Math.Min (minSdkVersion, 11).ToString ();
81+
} else {
82+
return uses.Attribute (androidNs + "minSdkVersion").Value;
83+
}
84+
}
85+
}
7186
TaskLoggingHelper log;
7287

7388
public ManifestDocument (string templateFilename, TaskLoggingHelper log) : base ()
@@ -839,11 +854,45 @@ public void SetAbi (string abi)
839854
int code = 1;
840855
if (!string.IsNullOrEmpty (VersionCode)) {
841856
code = Convert.ToInt32 (VersionCode);
842-
if (code > 0xffff || code < 0)
843-
throw new ArgumentOutOfRangeException ("VersionCode", "VersionCode is outside 0, 65535 interval");
857+
if (code > maxVersionCode || code < 0)
858+
throw new ArgumentOutOfRangeException ("VersionCode", $"VersionCode is outside 0, {maxVersionCode} interval");
844859
}
845860
code |= GetAbiCode (abi) << 16;
846861
VersionCode = code.ToString ();
847862
}
863+
864+
public void CalculateVersionCode (string currentAbi, string versionCodePattern, string versionCodeProperties)
865+
{
866+
var regex = new Regex ("\\{(?<key>([A-Za-z]+)):?[D0-9]*[\\}]");
867+
var kvp = new Dictionary<string, int> ();
868+
foreach (var item in versionCodeProperties?.Split (new char [] { ';', ':' }) ?? new string [0]) {
869+
var keyValue = item.Split (new char [] { '=' });
870+
int val;
871+
if (!int.TryParse (keyValue [1], out val))
872+
continue;
873+
kvp.Add (keyValue [0], val);
874+
}
875+
if (!kvp.ContainsKey ("abi") && !string.IsNullOrEmpty (currentAbi))
876+
kvp.Add ("abi", GetAbiCode (currentAbi));
877+
if (!kvp.ContainsKey ("versionCode"))
878+
kvp.Add ("versionCode", int.Parse (VersionCode));
879+
if (!kvp.ContainsKey ("minSDK")) {
880+
kvp.Add ("minSDK", int.Parse (MinimumSdk));
881+
}
882+
var versionCode = String.Empty;
883+
foreach (Match match in regex.Matches (versionCodePattern)) {
884+
var key = match.Groups ["key"].Value;
885+
var format = match.Value.Replace (key, "0");
886+
if (!kvp.ContainsKey (key))
887+
continue;
888+
versionCode += string.Format (format, kvp [key]);
889+
}
890+
int code;
891+
if (!int.TryParse (versionCode, out code))
892+
throw new ArgumentOutOfRangeException ("VersionCode", $"VersionCode {versionCode} is invalid. It must be an integer value.");
893+
if (code > maxVersionCode || code < 0)
894+
throw new ArgumentOutOfRangeException ("VersionCode", $"VersionCode {code} is outside 0, {maxVersionCode} interval");
895+
VersionCode = versionCode;
896+
}
848897
}
849898
}

src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,6 +1807,9 @@ because xbuild doesn't support framework reference assemblies.
18071807
CreatePackagePerAbi="$(AndroidCreatePackagePerAbi)"
18081808
YieldDuringToolExecution="$(YieldDuringToolExecution)"
18091809
ExplicitCrunch="$(AndroidExplicitCrunch)"
1810+
VersionCodePattern="$(AndroidVersionCodePattern)"
1811+
VersionCodeProperties="$(AndroidVersionCodeProperties)"
1812+
AndroidSdkPlatform="$(_AndroidApiLevel)"
18101813
/>
18111814
<Touch Files="$(_PackagedResources)" />
18121815
<!-- LibraryProjectJars must not be used for aapt in BuildApk*, or it will *bundle* the jar! -->

0 commit comments

Comments
 (0)