Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at devShellTools-shell 288 lines 8.8 kB view raw
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)