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