summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/android_version_code.py
blob: aa13609a7a617dd17c26f2e719075426a1a408d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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:]))