Skip to content

Commit

Permalink
[Xamarin.Android.Build.Tasks] Implement a new process for defining ve…
Browse files Browse the repository at this point in the history
…rsionCode.

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
  • Loading branch information
dellis1972 committed Jul 14, 2017
1 parent 42ec1af commit b07d47f
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 5 deletions.
62 changes: 62 additions & 0 deletions Documentation/build_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,68 @@ when packaing Release applications.

Added in Xamarin.Android 7.1.

- **AndroidVersionCodePattern** &ndash; 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 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** &ndash; Inserts the targetted abi for the app
- 1 &ndash; `armeabi`
- 2 &ndash; `armeabi-v7a`
- 3 &ndash; `x86`
- 4 &ndash; `arm64-v8a`
- 5 &ndash; `x86_64`

- **minSDK** &ndash; Inserts the minimum supported Sdk
value from the `AndroidManifest.xml` or `11` if none is
defined.

- **versionCode** &ndash; 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** &ndash; 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
Expand Down
26 changes: 24 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,string> resource_name_case_map = new Dictionary<string,string> ();

bool ManifestIsUpToDate (string manifestFile)
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 ((manifest.VersionCode == "1" || string.IsNullOrEmpty (manifest.VersionCode)) && !string.IsNullOrEmpty (VersionCodePattern)) {
manifest.CalculateVersionCode (null, VersionCodePattern, VersionCodeProperties);
}
manifest.ApplicationName = ApplicationName;
manifest.Save (manifestFile);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using NUnit.Framework;
using Xamarin.ProjectTools;
Expand Down Expand Up @@ -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 ()
{
Expand Down
53 changes: 51 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ()
Expand Down Expand Up @@ -839,11 +854,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 ("\\{(?<key>([A-Za-z]+)):?[D0-9]*[\\}]");
var kvp = new Dictionary<string, int> ();
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 (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]);
}
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1807,6 +1807,9 @@ because xbuild doesn't support framework reference assemblies.
CreatePackagePerAbi="$(AndroidCreatePackagePerAbi)"
YieldDuringToolExecution="$(YieldDuringToolExecution)"
ExplicitCrunch="$(AndroidExplicitCrunch)"
VersionCodePattern="$(AndroidVersionCodePattern)"
VersionCodeProperties="$(AndroidVersionCodeProperties)"
AndroidSdkPlatform="$(_AndroidApiLevel)"
/>
<Touch Files="$(_PackagedResources)" />
<!-- LibraryProjectJars must not be used for aapt in BuildApk*, or it will *bundle* the jar! -->
Expand Down

0 comments on commit b07d47f

Please sign in to comment.