summary refs log tree commit diff
path: root/pkgs/desktops/gnome/find-latest-version.py
blob: 1cc2b55fadb3a85135898036594c74862b8ada2e (plain) (blame)
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
import argparse
import math
import json
import requests
import sys
from enum import Enum
from libversion import Version
from typing import (
    Callable,
    Iterable,
    List,
    NamedTuple,
    Optional,
    Tuple,
    TypeVar,
    Type,
    cast,
)


EnumValue = TypeVar("EnumValue", bound=Enum)


def enum_to_arg(enum: Enum) -> str:
    return enum.name.lower().replace("_", "-")


def arg_to_enum(enum_meta: Type[EnumValue], name: str) -> EnumValue:
    return enum_meta[name.upper().replace("-", "_")]


def enum_to_arg_choices(enum_meta: Type[EnumValue]) -> Tuple[str, ...]:
    return tuple(enum_to_arg(v) for v in cast(Iterable[EnumValue], enum_meta))


class Stability(Enum):
    STABLE = "stable"
    UNSTABLE = "unstable"


VersionPolicy = Callable[[Version], bool]
VersionPredicate = Callable[[Version, Stability], bool]


class VersionPredicateHolder(NamedTuple):
    function: VersionPredicate


def version_to_list(version: str) -> List[int]:
    return list(map(int, version.split(".")))


def odd_unstable(version: Version, selected: Stability) -> bool:
    try:
        version_parts = version_to_list(version.value)
    except:
        # Failing to parse as a list of numbers likely means the version contains a string tag like “beta”, therefore it is not a stable release.
        return selected != Stability.STABLE

    if len(version_parts) < 2:
        return True

    even = version_parts[1] % 2 == 0
    prerelease = (version_parts[1] >= 90 and version_parts[1] < 100) or (version_parts[1] >= 900 and version_parts[1] < 1000)
    stable = even and not prerelease
    if selected == Stability.STABLE:
        return stable
    else:
        return True


def tagged(version: Version, selected: Stability) -> bool:
    if selected == Stability.STABLE:
        return not ("alpha" in version.value or "beta" in version.value or "rc" in version.value)
    else:
        return True


def no_policy(version: Version, selected: Stability) -> bool:
    return True


class VersionPolicyKind(Enum):
    # HACK: Using function as values directly would make Enum
    # think they are methods and skip them.
    ODD_UNSTABLE = VersionPredicateHolder(odd_unstable)
    TAGGED = VersionPredicateHolder(tagged)
    NONE = VersionPredicateHolder(no_policy)


def make_version_policy(
    version_policy_kind: VersionPolicyKind,
    selected: Stability,
    upper_bound: Optional[Version],
) -> VersionPolicy:
    version_predicate = version_policy_kind.value.function
    if not upper_bound:
        return lambda version: version_predicate(version, selected)
    else:
        return lambda version: version_predicate(version, selected) and version < upper_bound


def find_versions(package_name: str, version_policy: VersionPolicy) -> List[Version]:
    # The structure of cache.json: https://gitlab.gnome.org/Infrastructure/sysadmin-bin/blob/master/ftpadmin#L762
    cache = json.loads(requests.get(f"https://ftp.gnome.org/pub/GNOME/sources/{package_name}/cache.json").text)
    if type(cache) != list or cache[0] != 4:
        raise Exception("Unknown format of cache.json file.")

    versions: Iterable[Version] = map(Version, cache[2][package_name])
    versions = sorted(filter(version_policy, versions))

    return versions


parser = argparse.ArgumentParser(
    description="Find latest version for a GNOME package by crawling their release server.",
)
parser.add_argument(
    "package-name",
    help="Name of the directory in https://ftp.gnome.org/pub/GNOME/sources/ containing the package.",
)
parser.add_argument(
    "version-policy",
    help="Policy determining which versions are considered stable. GNOME packages usually denote stability by alpha/beta/rc tag in the version. For older packages, odd minor versions are unstable but there are exceptions.",
    choices=enum_to_arg_choices(VersionPolicyKind),
    nargs="?",
    default=enum_to_arg(VersionPolicyKind.TAGGED),
)
parser.add_argument(
    "requested-release",
    help="Most of the time, we will want to update to stable version but sometimes it is useful to test.",
    choices=enum_to_arg_choices(Stability),
    nargs="?",
    default=enum_to_arg(Stability.STABLE),
)
parser.add_argument(
    "--upper-bound",
    dest="upper-bound",
    help="Only look for versions older than this one (useful for pinning dependencies).",
)


if __name__ == "__main__":
    args = parser.parse_args()

    package_name = getattr(args, "package-name")
    requested_release = arg_to_enum(Stability, getattr(args, "requested-release"))
    upper_bound = getattr(args, "upper-bound")
    if upper_bound is not None:
        upper_bound = Version(upper_bound)
    version_policy_kind = arg_to_enum(VersionPolicyKind, getattr(args, "version-policy"))
    version_policy = make_version_policy(version_policy_kind, requested_release, upper_bound)

    try:
        versions = find_versions(package_name, version_policy)
    except Exception as error:
        print(error, file=sys.stderr)
        sys.exit(1)

    if len(versions) == 0:
        print("No versions matched.", file=sys.stderr)
        sys.exit(1)

    print(versions[-1].value)