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