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