summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/android_version_code.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozbuild/mozbuild/android_version_code.py')
-rw-r--r--python/mozbuild/mozbuild/android_version_code.py197
1 files changed, 197 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/android_version_code.py b/python/mozbuild/mozbuild/android_version_code.py
new file mode 100644
index 0000000000..aa13609a7a
--- /dev/null
+++ b/python/mozbuild/mozbuild/android_version_code.py
@@ -0,0 +1,197 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import argparse
+import math
+import sys
+import time
+
+# Builds before this build ID use the v0 version scheme. Builds after this
+# build ID use the v1 version scheme.
+V1_CUTOFF = 20150801000000 # YYYYmmddHHMMSS
+
+
+def android_version_code_v0(buildid, cpu_arch=None, min_sdk=0, max_sdk=0):
+ base = int(str(buildid)[:10])
+ # None is interpreted as arm.
+ if not cpu_arch or cpu_arch == "armeabi-v7a":
+ # Increment by MIN_SDK_VERSION -- this adds 9 to every build ID as a
+ # minimum. Our split APK starts at 15.
+ return base + min_sdk + 0
+ elif cpu_arch in ["x86"]:
+ # Increment the version code by 3 for x86 builds so they are offered to
+ # x86 phones that have ARM emulators, beating the 2-point advantage that
+ # the v15+ ARMv7 APK has. If we change our splits in the future, we'll
+ # need to do this further still.
+ return base + min_sdk + 3
+ else:
+ raise ValueError(
+ "Don't know how to compute android:versionCode "
+ "for CPU arch %s" % cpu_arch
+ )
+
+
+def android_version_code_v1(buildid, cpu_arch=None, min_sdk=0, max_sdk=0):
+ """Generate a v1 android:versionCode.
+ The important consideration is that version codes be monotonically
+ increasing (per Android package name) for all published builds. The input
+ build IDs are based on timestamps and hence are always monotonically
+ increasing.
+
+ The generated v1 version codes look like (in binary):
+
+ 0111 1000 0010 tttt tttt tttt tttt txpg
+
+ The 17 bits labelled 't' represent the number of hours since midnight on
+ September 1, 2015. (2015090100 in YYYYMMMDDHH format.) This yields a
+ little under 15 years worth of hourly build identifiers, since 2**17 / (366
+ * 24) =~ 14.92.
+
+ The bits labelled 'x', 'p', and 'g' are feature flags.
+
+ The bit labelled 'x' is 1 if the build is for an x86 or x86-64 architecture,
+ and 0 otherwise, which means the build is for an ARM or ARM64 architecture.
+ (Fennec no longer supports ARMv6, so ARM is equivalent to ARMv7.
+
+ ARM64 is also known as AArch64; it is logically ARMv8.)
+
+ For the same release, x86 and x86_64 builds have higher version codes and
+ take precedence over ARM builds, so that they are preferred over ARM on
+ devices that have ARM emulation.
+
+ The bit labelled 'p' is 1 if the build is for a 64-bit architecture (x86-64
+ or ARM64), and 0 otherwise, which means the build is for a 32-bit
+ architecture (x86 or ARM). 64-bit builds have higher version codes so
+ they take precedence over 32-bit builds on devices that support 64-bit.
+
+ The bit labelled 'g' is 1 if the build targets a recent API level, which
+ is currently always the case, because Firefox no longer ships releases that
+ are split by API levels. However, we may reintroduce a split in the future,
+ in which case the release that targets an older API level will
+
+ We throw an explanatory exception when we are within one calendar year of
+ running out of build events. This gives lots of time to update the version
+ scheme. The responsible individual should then bump the range (to allow
+ builds to continue) and use the time remaining to update the version scheme
+ via the reserved high order bits.
+
+ N.B.: the reserved 0 bit to the left of the highest order 't' bit can,
+ sometimes, be used to bump the version scheme. In addition, by reducing the
+ granularity of the build identifiers (for example, moving to identifying
+ builds every 2 or 4 hours), the version scheme may be adjusted further still
+ without losing a (valuable) high order bit.
+ """
+
+ def hours_since_cutoff(buildid):
+ # The ID is formatted like YYYYMMDDHHMMSS (using
+ # datetime.now().strftime('%Y%m%d%H%M%S'); see build/variables.py).
+ # The inverse function is time.strptime.
+ # N.B.: the time module expresses time as decimal seconds since the
+ # epoch.
+ fmt = "%Y%m%d%H%M%S"
+ build = time.strptime(str(buildid), fmt)
+ cutoff = time.strptime(str(V1_CUTOFF), fmt)
+ return int(
+ math.floor((time.mktime(build) - time.mktime(cutoff)) / (60.0 * 60.0))
+ )
+
+ # Of the 21 low order bits, we take 17 bits for builds.
+ base = hours_since_cutoff(buildid)
+ if base < 0:
+ raise ValueError(
+ "Something has gone horribly wrong: cannot calculate "
+ "android:versionCode from build ID %s: hours underflow "
+ "bits allotted!" % buildid
+ )
+ if base > 2 ** 17:
+ raise ValueError(
+ "Something has gone horribly wrong: cannot calculate "
+ "android:versionCode from build ID %s: hours overflow "
+ "bits allotted!" % buildid
+ )
+ if base > 2 ** 17 - 366 * 24:
+ raise ValueError(
+ "Running out of low order bits calculating "
+ "android:versionCode from build ID %s: "
+ "; YOU HAVE ONE YEAR TO UPDATE THE VERSION SCHEME." % buildid
+ )
+
+ version = 0b1111000001000000000000000000000
+ # We reserve 1 "middle" high order bit for the future, and 3 low order bits
+ # for architecture and APK splits.
+ version |= base << 3
+
+ # 'x' bit is 1 for x86/x86-64 architectures (`None` is interpreted as ARM).
+ if cpu_arch in ["x86", "x86_64"]:
+ version |= 1 << 2
+ elif not cpu_arch or cpu_arch in ["armeabi-v7a", "arm64-v8a"]:
+ pass
+ else:
+ raise ValueError(
+ "Don't know how to compute android:versionCode "
+ "for CPU arch %s" % cpu_arch
+ )
+
+ # 'p' bit is 1 for 64-bit architectures.
+ if cpu_arch in ["arm64-v8a", "x86_64"]:
+ version |= 1 << 1
+ elif cpu_arch in ["armeabi-v7a", "x86"]:
+ pass
+ else:
+ raise ValueError(
+ "Don't know how to compute android:versionCode "
+ "for CPU arch %s" % cpu_arch
+ )
+
+ # 'g' bit is currently always 1, but may depend on `min_sdk` in the future.
+ version |= 1 << 0
+
+ return version
+
+
+def android_version_code(buildid, *args, **kwargs):
+ base = int(str(buildid))
+ if base < V1_CUTOFF:
+ return android_version_code_v0(buildid, *args, **kwargs)
+ else:
+ return android_version_code_v1(buildid, *args, **kwargs)
+
+
+def main(argv):
+ parser = argparse.ArgumentParser("Generate an android:versionCode", add_help=False)
+ parser.add_argument(
+ "--verbose", action="store_true", default=False, help="Be verbose"
+ )
+ parser.add_argument(
+ "--with-android-cpu-arch",
+ dest="cpu_arch",
+ choices=["armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64"],
+ help="The target CPU architecture",
+ )
+ parser.add_argument(
+ "--with-android-min-sdk-version",
+ dest="min_sdk",
+ type=int,
+ default=0,
+ help="The minimum target SDK",
+ )
+ parser.add_argument(
+ "--with-android-max-sdk-version",
+ dest="max_sdk",
+ type=int,
+ default=0,
+ help="The maximum target SDK",
+ )
+ parser.add_argument("buildid", type=int, help="The input build ID")
+
+ args = parser.parse_args(argv)
+ code = android_version_code(
+ args.buildid, cpu_arch=args.cpu_arch, min_sdk=args.min_sdk, max_sdk=args.max_sdk
+ )
+ print(code)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv[1:]))