Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
1#!/usr/bin/env nix-shell 2#! nix-shell -i python -p "python3.withPackages (ps: with ps; [ ps.absl-py ps.requests ])" nix 3 4from collections import defaultdict 5import copy 6from dataclasses import dataclass 7import json 8import os.path 9import subprocess 10from typing import Callable, Dict 11 12from absl import app 13from absl import flags 14from absl import logging 15import requests 16 17 18FACTORIO_API = "https://factorio.com/api/latest-releases" 19 20 21FLAGS = flags.FLAGS 22 23flags.DEFINE_string('username', '', 'Factorio username for retrieving binaries.') 24flags.DEFINE_string('token', '', 'Factorio token for retrieving binaries.') 25flags.DEFINE_string('out', '', 'Output path for versions.json.') 26flags.DEFINE_list('release_type', '', 'If non-empty, a comma-separated list of release types to update (e.g. alpha).') 27flags.DEFINE_list('release_channel', '', 'If non-empty, a comma-separated list of release channels to update (e.g. experimental).') 28 29 30@dataclass 31class System: 32 nix_name: str 33 url_name: str 34 tar_name: str 35 36 37@dataclass 38class ReleaseType: 39 name: str 40 needs_auth: bool = False 41 42 43@dataclass 44class ReleaseChannel: 45 name: str 46 47 48FactorioVersionsJSON = Dict[str, Dict[str, str]] 49OurVersionJSON = Dict[str, Dict[str, Dict[str, Dict[str, str]]]] 50 51 52SYSTEMS = [ 53 System(nix_name="x86_64-linux", url_name="linux64", tar_name="x64"), 54] 55 56RELEASE_TYPES = [ 57 ReleaseType("alpha", needs_auth=True), 58 ReleaseType("demo"), 59 ReleaseType("headless"), 60] 61 62RELEASE_CHANNELS = [ 63 ReleaseChannel("experimental"), 64 ReleaseChannel("stable"), 65] 66 67 68def find_versions_json() -> str: 69 if FLAGS.out: 70 return FLAGS.out 71 try_paths = ["pkgs/games/factorio/versions.json", "versions.json"] 72 for path in try_paths: 73 if os.path.exists(path): 74 return path 75 raise Exception("Couldn't figure out where to write versions.json; try specifying --out") 76 77 78def fetch_versions() -> FactorioVersionsJSON: 79 return json.loads(requests.get("https://factorio.com/api/latest-releases").text) 80 81 82def generate_our_versions(factorio_versions: FactorioVersionsJSON) -> OurVersionJSON: 83 rec_dd = lambda: defaultdict(rec_dd) 84 output = rec_dd() 85 86 # Deal with times where there's no experimental version 87 for rc in RELEASE_CHANNELS: 88 if not factorio_versions[rc.name]: 89 factorio_versions[rc.name] = factorio_versions['stable'] 90 91 for system in SYSTEMS: 92 for release_type in RELEASE_TYPES: 93 for release_channel in RELEASE_CHANNELS: 94 version = factorio_versions[release_channel.name].get(release_type.name) 95 if version == None: 96 continue 97 this_release = { 98 "name": f"factorio_{release_type.name}_{system.tar_name}-{version}.tar.xz", 99 "url": f"https://factorio.com/get-download/{version}/{release_type.name}/{system.url_name}", 100 "version": version, 101 "needsAuth": release_type.needs_auth, 102 "tarDirectory": system.tar_name, 103 } 104 output[system.nix_name][release_type.name][release_channel.name] = this_release 105 return output 106 107 108def iter_version(versions: OurVersionJSON, it: Callable[[str, str, str, Dict[str, str]], Dict[str, str]]) -> OurVersionJSON: 109 versions = copy.deepcopy(versions) 110 for system_name, system in versions.items(): 111 for release_type_name, release_type in system.items(): 112 for release_channel_name, release in release_type.items(): 113 release_type[release_channel_name] = it(system_name, release_type_name, release_channel_name, dict(release)) 114 return versions 115 116 117def merge_versions(old: OurVersionJSON, new: OurVersionJSON) -> OurVersionJSON: 118 """Copies already-known hashes from version.json to avoid having to re-fetch.""" 119 def _merge_version(system_name: str, release_type_name: str, release_channel_name: str, release: Dict[str, str]) -> Dict[str, str]: 120 old_system = old.get(system_name, {}) 121 old_release_type = old_system.get(release_type_name, {}) 122 old_release = old_release_type.get(release_channel_name, {}) 123 if FLAGS.release_type and release_type_name not in FLAGS.release_type: 124 logging.info("%s/%s/%s: not in --release_type, not updating", system_name, release_type_name, release_channel_name) 125 return old_release 126 if FLAGS.release_channel and release_channel_name not in FLAGS.release_channel: 127 logging.info("%s/%s/%s: not in --release_channel, not updating", system_name, release_type_name, release_channel_name) 128 return old_release 129 if not "sha256" in old_release: 130 logging.info("%s/%s/%s: not copying sha256 since it's missing", system_name, release_type_name, release_channel_name) 131 return release 132 if not all(old_release.get(k, None) == release[k] for k in ['name', 'version', 'url']): 133 logging.info("%s/%s/%s: not copying sha256 due to mismatch", system_name, release_type_name, release_channel_name) 134 return release 135 release["sha256"] = old_release["sha256"] 136 return release 137 return iter_version(new, _merge_version) 138 139 140def nix_prefetch_url(name: str, url: str, algo: str = 'sha256') -> str: 141 cmd = ['nix-prefetch-url', '--type', algo, '--name', name, url] 142 logging.info('running %s', cmd) 143 out = subprocess.check_output(cmd) 144 return out.decode('utf-8').strip() 145 146 147def fill_in_hash(versions: OurVersionJSON) -> OurVersionJSON: 148 """Fill in sha256 hashes for anything missing them.""" 149 urls_to_hash = {} 150 def _fill_in_hash(system_name: str, release_type_name: str, release_channel_name: str, release: Dict[str, str]) -> Dict[str, str]: 151 if "sha256" in release: 152 logging.info("%s/%s/%s: skipping fetch, sha256 already present", system_name, release_type_name, release_channel_name) 153 return release 154 url = release["url"] 155 if url in urls_to_hash: 156 logging.info("%s/%s/%s: found url %s in cache", system_name, release_type_name, release_channel_name, url) 157 release["sha256"] = urls_to_hash[url] 158 return release 159 logging.info("%s/%s/%s: fetching %s", system_name, release_type_name, release_channel_name, url) 160 if release["needsAuth"]: 161 if not FLAGS.username or not FLAGS.token: 162 raise Exception("fetching %s/%s/%s from %s requires --username and --token" % (system_name, release_type_name, release_channel_name, url)) 163 url += f"?username={FLAGS.username}&token={FLAGS.token}" 164 release["sha256"] = nix_prefetch_url(release["name"], url) 165 urls_to_hash[url] = release["sha256"] 166 return release 167 return iter_version(versions, _fill_in_hash) 168 169 170def main(argv): 171 factorio_versions = fetch_versions() 172 new_our_versions = generate_our_versions(factorio_versions) 173 old_our_versions = None 174 our_versions_path = find_versions_json() 175 if our_versions_path: 176 logging.info('Loading old versions.json from %s', our_versions_path) 177 with open(our_versions_path, 'r') as f: 178 old_our_versions = json.load(f) 179 if old_our_versions: 180 logging.info('Merging in old hashes') 181 new_our_versions = merge_versions(old_our_versions, new_our_versions) 182 logging.info('Fetching necessary tars to get hashes') 183 new_our_versions = fill_in_hash(new_our_versions) 184 with open(our_versions_path, 'w') as f: 185 logging.info('Writing versions.json to %s', our_versions_path) 186 json.dump(new_our_versions, f, sort_keys=True, indent=2) 187 f.write("\n") 188 189if __name__ == '__main__': 190 app.run(main)