1import argparse
2import math
3import json
4import requests
5import sys
6from enum import Enum
7from libversion import Version
8from typing import (
9 Callable,
10 Iterable,
11 List,
12 NamedTuple,
13 Optional,
14 Tuple,
15 TypeVar,
16 Type,
17 cast,
18)
19
20
21EnumValue = TypeVar("EnumValue", bound=Enum)
22
23
24def enum_to_arg(enum: Enum) -> str:
25 return enum.name.lower().replace("_", "-")
26
27
28def arg_to_enum(enum_meta: Type[EnumValue], name: str) -> EnumValue:
29 return enum_meta[name.upper().replace("-", "_")]
30
31
32def enum_to_arg_choices(enum_meta: Type[EnumValue]) -> Tuple[str, ...]:
33 return tuple(enum_to_arg(v) for v in cast(Iterable[EnumValue], enum_meta))
34
35
36class Stability(Enum):
37 STABLE = "stable"
38 UNSTABLE = "unstable"
39
40 def allows(self, target: "Stability") -> bool:
41 """
42 Whether selected stability `self` allows version
43 with a specified `target` stability.
44 """
45 match self:
46 case Stability.STABLE:
47 return target == Stability.STABLE
48 case Stability.UNSTABLE:
49 return True
50
51 def __repr__(self) -> str:
52 """
53 Useful for tests.
54 """
55 match self:
56 case Stability.STABLE:
57 return "Stability.STABLE"
58 case Stability.UNSTABLE:
59 return "Stability.UNSTABLE"
60
61
62VersionPolicy = Callable[[Version], bool]
63VersionClassifier = Callable[[Version], Stability]
64
65
66class VersionClassifierHolder(NamedTuple):
67 function: VersionClassifier
68
69
70def version_to_list(version: str) -> List[int]:
71 return list(map(int, version.split(".")))
72
73
74def odd_unstable(version: Version) -> Stability:
75 """
76 Traditional GNOME version policy
77
78 >>> odd_unstable(Version("32"))
79 Stability.STABLE
80 >>> odd_unstable(Version("3.2.1"))
81 Stability.STABLE
82 >>> odd_unstable(Version("3.2.1.alpha"))
83 Stability.UNSTABLE
84 >>> odd_unstable(Version("3.2.1beta"))
85 Stability.UNSTABLE
86 >>> odd_unstable(Version("4.2.89"))
87 Stability.STABLE
88 >>> odd_unstable(Version("4.2.90"))
89 Stability.STABLE
90 >>> odd_unstable(Version("4.88.2"))
91 Stability.STABLE
92 >>> odd_unstable(Version("4.90.2"))
93 Stability.UNSTABLE
94 >>> odd_unstable(Version("4.3.0"))
95 Stability.UNSTABLE
96 >>> odd_unstable(Version("4.3.89"))
97 Stability.UNSTABLE
98 >>> odd_unstable(Version("4.2.899"))
99 Stability.STABLE
100 >>> odd_unstable(Version("4.2.900"))
101 Stability.STABLE
102 >>> odd_unstable(Version("4.898.2"))
103 Stability.STABLE
104 >>> odd_unstable(Version("4.900.2"))
105 Stability.UNSTABLE
106 """
107 try:
108 version_parts = version_to_list(version.value)
109 except:
110 # 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.
111 return Stability.UNSTABLE
112
113 if len(version_parts) < 2:
114 return Stability.STABLE
115
116 even = version_parts[1] % 2 == 0
117 prerelease = (version_parts[1] >= 90 and version_parts[1] < 100) or (version_parts[1] >= 900 and version_parts[1] < 1000)
118 stable = even and not prerelease
119 if stable:
120 return Stability.STABLE
121 else:
122 return Stability.UNSTABLE
123
124
125def ninety_micro_unstable(version: Version) -> Stability:
126 """
127 <https://gitlab.gnome.org/GNOME/gcr/-/tree/4.3.90.3#versions>:
128 > To denote unstable versions, the micro version number will correspond to 90 or
129 > higher, e.g. 4.$MINOR.90.
130
131 >>> ninety_micro_unstable(Version("3.2.1"))
132 Stability.STABLE
133 >>> ninety_micro_unstable(Version("3.2.1.alpha"))
134 Stability.UNSTABLE
135 >>> ninety_micro_unstable(Version("3.2.1beta"))
136 Stability.UNSTABLE
137 >>> ninety_micro_unstable(Version("4.2.89"))
138 Stability.STABLE
139 >>> ninety_micro_unstable(Version("4.3.89"))
140 Stability.STABLE
141 >>> ninety_micro_unstable(Version("4.2.90"))
142 Stability.UNSTABLE
143 >>> ninety_micro_unstable(Version("4.2.89.3"))
144 Stability.STABLE
145 >>> ninety_micro_unstable(Version("4.2.90.3"))
146 Stability.UNSTABLE
147 >>> ninety_micro_unstable(Version("4.90.1"))
148 Stability.STABLE
149 """
150 try:
151 version_parts = version_to_list(version.value)
152 except:
153 # 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.
154 return Stability.UNSTABLE
155
156 if len(version_parts) < 3:
157 return Stability.STABLE
158
159 prerelease = version_parts[2] >= 90
160 if prerelease:
161 return Stability.UNSTABLE
162 else:
163 return Stability.STABLE
164
165
166def tagged(version: Version) -> Stability:
167 """
168 Considers only versions with explicit `alpha`, `beta` or `rc` tags unstable.
169
170 >>> tagged(Version("32"))
171 Stability.STABLE
172 >>> tagged(Version("3.2.1"))
173 Stability.STABLE
174 >>> tagged(Version("4.3.0"))
175 Stability.STABLE
176 >>> tagged(Version("3.2.1.alpha"))
177 Stability.UNSTABLE
178 >>> tagged(Version("3.2.1beta"))
179 Stability.UNSTABLE
180 >>> tagged(Version("3.2.1rc.3"))
181 Stability.UNSTABLE
182 """
183 prerelease = "alpha" in version.value or "beta" in version.value or "rc" in version.value
184 if prerelease:
185 return Stability.UNSTABLE
186 else:
187 return Stability.STABLE
188
189
190def no_policy(version: Version) -> Stability:
191 """
192 Considers any version stable.
193
194 >>> no_policy(Version("32"))
195 Stability.STABLE
196 >>> no_policy(Version("3.2.1"))
197 Stability.STABLE
198 >>> no_policy(Version("3.2.1.alpha"))
199 Stability.STABLE
200 >>> no_policy(Version("3.2.1beta"))
201 Stability.STABLE
202 """
203 return Stability.STABLE
204
205
206class VersionPolicyKind(Enum):
207 # HACK: Using function as values directly would make Enum
208 # think they are methods and skip them.
209 ODD_UNSTABLE = VersionClassifierHolder(odd_unstable)
210 NINETY_MICRO_UNSTABLE = VersionClassifierHolder(ninety_micro_unstable)
211 TAGGED = VersionClassifierHolder(tagged)
212 NONE = VersionClassifierHolder(no_policy)
213
214
215def make_version_policy(
216 version_policy_kind: VersionPolicyKind,
217 selected: Stability,
218 upper_bound: Optional[Version],
219) -> VersionPolicy:
220 version_classifier = version_policy_kind.value.function
221 if not upper_bound:
222 return lambda version: selected.allows(version_classifier(version))
223 else:
224 return lambda version: selected.allows(version_classifier(version)) and version < upper_bound
225
226
227def find_versions(package_name: str, version_policy: VersionPolicy) -> List[Version]:
228 # The structure of cache.json: https://gitlab.gnome.org/Infrastructure/sysadmin-bin/blob/master/ftpadmin#L762
229 cache = json.loads(requests.get(f"https://download.gnome.org/sources/{package_name}/cache.json").text)
230 if type(cache) != list or cache[0] != 4:
231 raise Exception("Unknown format of cache.json file.")
232
233 versions: Iterable[Version] = map(Version, cache[2][package_name])
234 versions = sorted(filter(version_policy, versions))
235
236 return versions
237
238
239parser = argparse.ArgumentParser(
240 description="Find latest version for a GNOME package by crawling their release server.",
241)
242parser.add_argument(
243 "package-name",
244 help="Name of the directory in https://download.gnome.org/sources/ containing the package.",
245)
246parser.add_argument(
247 "version-policy",
248 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.",
249 choices=enum_to_arg_choices(VersionPolicyKind),
250 nargs="?",
251 default=enum_to_arg(VersionPolicyKind.TAGGED),
252)
253parser.add_argument(
254 "requested-release",
255 help="Most of the time, we will want to update to stable version but sometimes it is useful to test.",
256 choices=enum_to_arg_choices(Stability),
257 nargs="?",
258 default=enum_to_arg(Stability.STABLE),
259)
260parser.add_argument(
261 "--upper-bound",
262 dest="upper-bound",
263 help="Only look for versions older than this one (useful for pinning dependencies).",
264)
265
266
267if __name__ == "__main__":
268 args = parser.parse_args()
269
270 package_name = getattr(args, "package-name")
271 requested_release = arg_to_enum(Stability, getattr(args, "requested-release"))
272 upper_bound = getattr(args, "upper-bound")
273 if upper_bound is not None:
274 upper_bound = Version(upper_bound)
275 version_policy_kind = arg_to_enum(VersionPolicyKind, getattr(args, "version-policy"))
276 version_policy = make_version_policy(version_policy_kind, requested_release, upper_bound)
277
278 try:
279 versions = find_versions(package_name, version_policy)
280 except Exception as error:
281 print(error, file=sys.stderr)
282 sys.exit(1)
283
284 if len(versions) == 0:
285 print("No versions matched.", file=sys.stderr)
286 sys.exit(1)
287
288 print(versions[-1].value)