1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function 6 7import argparse 8import math 9import sys 10import time 11 12# Builds before this build ID use the v0 version scheme. Builds after this 13# build ID use the v1 version scheme. 14V1_CUTOFF = 20150801000000 # YYYYmmddHHMMSS 15 16def android_version_code_v0(buildid, cpu_arch=None, min_sdk=0, max_sdk=0): 17 base = int(str(buildid)[:10]) 18 # None is interpreted as arm. 19 if not cpu_arch or cpu_arch == 'armeabi-v7a': 20 # Increment by MIN_SDK_VERSION -- this adds 9 to every build ID as a 21 # minimum. Our split APK starts at 15. 22 return base + min_sdk + 0 23 elif cpu_arch in ['x86']: 24 # Increment the version code by 3 for x86 builds so they are offered to 25 # x86 phones that have ARM emulators, beating the 2-point advantage that 26 # the v15+ ARMv7 APK has. If we change our splits in the future, we'll 27 # need to do this further still. 28 return base + min_sdk + 3 29 else: 30 raise ValueError("Don't know how to compute android:versionCode " 31 "for CPU arch %s" % cpu_arch) 32 33def android_version_code_v1(buildid, cpu_arch=None, min_sdk=0, max_sdk=0): 34 '''Generate a v1 android:versionCode. 35 36 The important consideration is that version codes be monotonically 37 increasing (per Android package name) for all published builds. The input 38 build IDs are based on timestamps and hence are always monotonically 39 increasing. 40 41 The generated v1 version codes look like (in binary): 42 43 0111 1000 0010 tttt tttt tttt tttt txpg 44 45 The 17 bits labelled 't' represent the number of hours since midnight on 46 September 1, 2015. (2015090100 in YYYYMMMDDHH format.) This yields a 47 little under 15 years worth of hourly build identifiers, since 2**17 / (366 48 * 24) =~ 14.92. 49 50 The bits labelled 'x', 'p', and 'g' are feature flags. 51 52 The bit labelled 'x' is 1 if the build is for an x86 or ARM64 architecture, 53 and 0 otherwise, which means the build is for a (32-bit) ARM architecture. 54 (Fennec no longer supports ARMv6, so ARM is equivalent to ARMv7. 55 ARM64 is also known as AArch64; it is logically ARMv8.) 56 57 For the same release, x86 and ARM64 builds have higher version codes and 58 take precedence over ARM builds, so that they are preferred over ARM on 59 devices that have ARM emulation. 60 61 The bit labelled 'p' is a placeholder that is always 0 (for now). 62 63 Firefox no longer supports API 14 or earlier. 64 65 This version code computation allows for a split on API levels that allowed 66 us to ship builds specifically for Gingerbread (API 9-10); we preserve 67 that functionality for sanity's sake, and to allow us to reintroduce a 68 split in the future. 69 70 At present, the bit labelled 'g' is 1 if the build is an ARM build 71 targeting API 16+, which will always be the case. 72 73 We throw an explanatory exception when we are within one calendar year of 74 running out of build events. This gives lots of time to update the version 75 scheme. The responsible individual should then bump the range (to allow 76 builds to continue) and use the time remaining to update the version scheme 77 via the reserved high order bits. 78 79 N.B.: the reserved 0 bit to the left of the highest order 't' bit can, 80 sometimes, be used to bump the version scheme. In addition, by reducing the 81 granularity of the build identifiers (for example, moving to identifying 82 builds every 2 or 4 hours), the version scheme may be adjusted further still 83 without losing a (valuable) high order bit. 84 ''' 85 def hours_since_cutoff(buildid): 86 # The ID is formatted like YYYYMMDDHHMMSS (using 87 # datetime.now().strftime('%Y%m%d%H%M%S'); see build/variables.py). 88 # The inverse function is time.strptime. 89 # N.B.: the time module expresses time as decimal seconds since the 90 # epoch. 91 fmt = '%Y%m%d%H%M%S' 92 build = time.strptime(str(buildid), fmt) 93 cutoff = time.strptime(str(V1_CUTOFF), fmt) 94 return int(math.floor((time.mktime(build) - time.mktime(cutoff)) / (60.0 * 60.0))) 95 96 # Of the 21 low order bits, we take 17 bits for builds. 97 base = hours_since_cutoff(buildid) 98 if base < 0: 99 raise ValueError("Something has gone horribly wrong: cannot calculate " 100 "android:versionCode from build ID %s: hours underflow " 101 "bits allotted!" % buildid) 102 if base > 2**17: 103 raise ValueError("Something has gone horribly wrong: cannot calculate " 104 "android:versionCode from build ID %s: hours overflow " 105 "bits allotted!" % buildid) 106 if base > 2**17 - 366 * 24: 107 raise ValueError("Running out of low order bits calculating " 108 "android:versionCode from build ID %s: " 109 "; YOU HAVE ONE YEAR TO UPDATE THE VERSION SCHEME." % buildid) 110 111 version = 0b1111000001000000000000000000000 112 # We reserve 1 "middle" high order bit for the future, and 3 low order bits 113 # for architecture and APK splits. 114 version |= base << 3 115 116 # None is interpreted as arm. 117 if not cpu_arch or cpu_arch == 'armeabi-v7a': 118 # 0 is interpreted as SDK 9. 119 if not min_sdk or min_sdk == 9: 120 pass 121 # This used to compare to 11. The 16+ APK directly supersedes 11+, so 122 # we reuse this check. 123 elif min_sdk == 16: 124 version |= 1 << 0 125 else: 126 raise ValueError("Don't know how to compute android:versionCode " 127 "for CPU arch %s and min SDK %s" % (cpu_arch, min_sdk)) 128 elif cpu_arch in ['x86', 'arm64-v8a']: 129 version |= 1 << 2 130 else: 131 raise ValueError("Don't know how to compute android:versionCode " 132 "for CPU arch %s" % cpu_arch) 133 134 return version 135 136def android_version_code(buildid, *args, **kwargs): 137 base = int(str(buildid)) 138 if base < V1_CUTOFF: 139 return android_version_code_v0(buildid, *args, **kwargs) 140 else: 141 return android_version_code_v1(buildid, *args, **kwargs) 142 143 144def main(argv): 145 parser = argparse.ArgumentParser('Generate an android:versionCode', 146 add_help=False) 147 parser.add_argument('--verbose', action='store_true', 148 default=False, 149 help='Be verbose') 150 parser.add_argument('--with-android-cpu-arch', dest='cpu_arch', 151 choices=['armeabi', 152 'armeabi-v7a', 153 'arm64-v8a', 154 'mips', 155 'x86'], 156 help='The target CPU architecture') 157 parser.add_argument('--with-android-min-sdk-version', dest='min_sdk', 158 type=int, default=0, 159 help='The minimum target SDK') 160 parser.add_argument('--with-android-max-sdk-version', dest='max_sdk', 161 type=int, default=0, 162 help='The maximum target SDK') 163 parser.add_argument('buildid', type=int, 164 help='The input build ID') 165 166 args = parser.parse_args(argv) 167 code = android_version_code(args.buildid, 168 cpu_arch=args.cpu_arch, 169 min_sdk=args.min_sdk, 170 max_sdk=args.max_sdk) 171 print(code) 172 return 0 173 174 175if __name__ == '__main__': 176 sys.exit(main(sys.argv[1:])) 177