tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

appservices_version_bump.py (6769B)


      1 #!/usr/bin/env python3
      2 
      3 # This Source Code Form is subject to the terms of the Mozilla Public
      4 # License, v. 2.0. If a copy of the MPL was not distributed with this
      5 # file, You can obtain one at http://mozilla.org/MPL/2.0/
      6 
      7 # Helper script for mach vendor / updatebot to update the application-services
      8 # version pin in ApplicationServices.kt to the latest available
      9 # application-services nightly build.
     10 #
     11 # This script was adapted from https://github.com/mozilla-mobile/relbot/
     12 
     13 import datetime
     14 import logging
     15 import os
     16 import re
     17 from urllib.parse import quote_plus
     18 
     19 import requests
     20 from mozbuild.vendor.host_base import BaseHost
     21 
     22 log = logging.getLogger(__name__)
     23 logging.basicConfig(
     24    format="%(asctime)s - %(name)s.%(funcName)s:%(lineno)s - %(levelname)s - %(message)s",  # noqa E501
     25    level=logging.INFO,
     26 )
     27 
     28 
     29 def get_contents(path):
     30    with open(path) as f:
     31        return f.read()
     32 
     33 
     34 def write_contents(path, new_content):
     35    with open(path, "w") as f:
     36        f.write(new_content)
     37 
     38 
     39 def get_app_services_version_path():
     40    """Return the file path to dependencies file"""
     41    p = "ApplicationServices.kt"
     42    if os.path.exists(p):
     43        return p
     44    return "mobile/android/android-components/plugins/dependencies/src/main/java/ApplicationServices.kt"
     45 
     46 
     47 def taskcluster_indexed_artifact_url(index_name, artifact_path):
     48    artifact_path = quote_plus(artifact_path)
     49    return (
     50        "https://firefox-ci-tc.services.mozilla.com/"
     51        f"api/index/v1/task/{index_name}/artifacts/{artifact_path}"
     52    )
     53 
     54 
     55 def validate_as_version(v):
     56    """Validate that v is in an expected format for an app-services version. Returns v or raises an exception."""
     57 
     58    match = re.match(r"(^\d+)\.\d+$", v)
     59    if match:
     60        # Application-services switched to following the 2-component the
     61        # Firefox version number in v114
     62        if int(match.group(1)) >= 114:
     63            return v
     64    raise Exception(f"Invalid version format {v}")
     65 
     66 
     67 def validate_as_channel(c):
     68    """Validate that c is a valid app-services channel."""
     69    if c in ("staging", "nightly_staging"):
     70        # These are channels are valid, but only used for preview builds.  We don't have
     71        # any way of auto-updating them
     72        raise Exception(f"Can't update app-services channel {c}")
     73    if c not in ("release", "nightly"):
     74        raise Exception(f"Invalid app-services channel {c}")
     75    return c
     76 
     77 
     78 def get_current_as_version():
     79    """Return the current nightly app-services version"""
     80    regex = re.compile(r'val VERSION = "([\d\.]+)"', re.MULTILINE)
     81 
     82    path = get_app_services_version_path()
     83    src = get_contents(path)
     84    match = regex.search(src)
     85    if match:
     86        return validate_as_version(match[1])
     87    raise Exception(
     88        f"Could not find application-services version in {os.path.basename(path)}"
     89    )
     90 
     91 
     92 def match_as_channel(src):
     93    """
     94    Find the ApplicationServicesChannel channel in the contents of the given
     95    ApplicationServices.kt file.
     96    """
     97    match = re.compile(
     98        r"val CHANNEL = ApplicationServicesChannel."
     99        r"(NIGHTLY|NIGHTLY_STAGING|STAGING|RELEASE)",
    100        re.MULTILINE,
    101    ).search(src)
    102    if match:
    103        return validate_as_channel(match[1].lower())
    104    raise Exception("Could not match the channel in ApplicationServices.kt")
    105 
    106 
    107 def get_current_as_channel():
    108    """Return the current app-services channel"""
    109    content = get_contents(get_app_services_version_path())
    110    return match_as_channel(content)
    111 
    112 
    113 def get_as_nightly_json(version="latest"):
    114    r = requests.get(
    115        taskcluster_indexed_artifact_url(
    116            f"project.application-services.v2.nightly.{version}",
    117            "public/build/nightly.json",
    118        )
    119    )
    120    r.raise_for_status()
    121    return r.json()
    122 
    123 
    124 def compare_as_versions(a, b):
    125    # Tricky cmp()-style function for application services versions.  Note that
    126    # this works with both 2-component versions and 3-component ones, Since
    127    # python compares tuples element by element.
    128    a = tuple(int(x) for x in validate_as_version(a).split("."))
    129    b = tuple(int(x) for x in validate_as_version(b).split("."))
    130    return (a > b) - (a < b)
    131 
    132 
    133 def update_as_version(old_as_version, new_as_version):
    134    """Update the VERSION in ApplicationServices.kt"""
    135    path = get_app_services_version_path()
    136    current_version_string = f'val VERSION = "{old_as_version}"'
    137    new_version_string = f'val VERSION = "{new_as_version}"'
    138    log.info(f"Updating app-services version in {path}")
    139 
    140    content = get_contents(path)
    141    new_content = content.replace(current_version_string, new_version_string)
    142    if content == new_content:
    143        raise Exception(
    144            "Update to ApplicationServices.kt resulted in no changes: "
    145            "maybe the file was already up to date?"
    146        )
    147 
    148    write_contents(path, new_content)
    149 
    150 
    151 def update_application_services(revision):
    152    """Find the app-services nightly build version corresponding to revision;
    153    if it is newer than the current version in ApplicationServices.kt, then
    154    update ApplicationServices.kt with the newer version number."""
    155    as_channel = get_current_as_channel()
    156    log.info(f"Current app-services channel is {as_channel}")
    157    if as_channel != "nightly":
    158        raise NotImplementedError(
    159            "Only the app-services nightly channel is currently supported"
    160        )
    161 
    162    current_as_version = get_current_as_version()
    163    log.info(
    164        f"Current app-services {as_channel.capitalize()} version is {current_as_version}"
    165    )
    166 
    167    json = get_as_nightly_json(f"revision.{revision}")
    168    target_as_version = json["version"]
    169    log.info(
    170        f"Target app-services {as_channel.capitalize()} version is {target_as_version}"
    171    )
    172 
    173    if compare_as_versions(current_as_version, target_as_version) >= 0:
    174        log.warning(
    175            f"No newer app-services {as_channel.capitalize()} release found. Exiting."
    176        )
    177        return
    178 
    179    dry_run = os.getenv("DRY_RUN") == "True"
    180    if dry_run:
    181        log.warning("Dry-run so not continuing.")
    182        return
    183 
    184    update_as_version(
    185        current_as_version,
    186        target_as_version,
    187    )
    188 
    189 
    190 class ASHost(BaseHost):
    191    def upstream_tag(self, revision):
    192        if revision == "HEAD":
    193            index = "latest"
    194        else:
    195            index = f"revision.{revision}"
    196        json = get_as_nightly_json(index)
    197        timestamp = json["version"].rsplit(".", 1)[1]
    198        return (
    199            json["commit"],
    200            datetime.datetime.strptime(timestamp, "%Y%m%d%H%M%S").isoformat(),
    201        )
    202 
    203 
    204 def main():
    205    import sys
    206 
    207    if len(sys.argv) != 2:
    208        print(f"Usage: {sys.argv[0]} <commit hash>", file=sys.stderr)
    209        sys.exit(1)
    210 
    211    update_application_services(sys.argv[1])
    212 
    213 
    214 if __name__ == "__main__":
    215    main()