1 #!/usr/bin/env nix-shell
2 #! nix-shell -i python -p "python3.withPackages (ps: with ps; [ ps.absl-py ps.requests ])"
4 from collections
import defaultdict
6 from dataclasses
import dataclass
9 from typing
import Callable
, Dict
12 from absl
import flags
13 from absl
import logging
17 FACTORIO_RELEASES
= "https://factorio.com/api/latest-releases"
18 FACTORIO_HASHES
= "https://factorio.com/download/sha256sums/"
23 flags
.DEFINE_string("out", "", "Output path for versions.json.")
27 "If non-empty, a comma-separated list of release types to update (e.g. alpha).",
32 "If non-empty, a comma-separated list of release channels to update (e.g. experimental).",
46 hash_filename_format
: list[str]
47 needs_auth
: bool = False
55 FactorioVersionsJSON
= Dict
[str, Dict
[str, str]]
56 OurVersionJSON
= Dict
[str, Dict
[str, Dict
[str, Dict
[str, str]]]]
58 FactorioHashes
= Dict
[str, str]
62 System(nix_name
="x86_64-linux", url_name
="linux64", tar_name
="x64"),
69 hash_filename_format
=["factorio_linux_{version}.tar.xz"],
71 ReleaseType("demo", hash_filename_format
=["factorio_demo_x64_{version}.tar.xz"]),
74 hash_filename_format
=[
75 "factorio-headless_linux_{version}.tar.xz",
76 "factorio_headless_x64_{version}.tar.xz",
82 hash_filename_format
=["factorio-space-age_linux_{version}.tar.xz"],
87 ReleaseChannel("experimental"),
88 ReleaseChannel("stable"),
92 def find_versions_json() -> str:
95 try_paths
= ["pkgs/by-name/fa/factorio/versions.json", "versions.json"]
96 for path
in try_paths
:
97 if os
.path
.exists(path
):
100 "Couldn't figure out where to write versions.json; try specifying --out"
104 def fetch_versions() -> FactorioVersionsJSON
:
105 return json
.loads(requests
.get(FACTORIO_RELEASES
).text
)
108 def fetch_hashes() -> FactorioHashes
:
109 resp
= requests
.get(FACTORIO_HASHES
)
110 resp
.raise_for_status()
112 for ln
in resp
.text
.split("\n"):
116 sha256
, filename
= ln
.split()
117 out
[filename
] = sha256
121 def generate_our_versions(factorio_versions
: FactorioVersionsJSON
) -> OurVersionJSON
:
123 return defaultdict(rec_dd
)
127 # Deal with times where there's no experimental version
128 for rc
in RELEASE_CHANNELS
:
129 if rc
.name
not in factorio_versions
or not factorio_versions
[rc
.name
]:
130 factorio_versions
[rc
.name
] = factorio_versions
["stable"]
131 for rt
in RELEASE_TYPES
:
133 rt
.name
not in factorio_versions
[rc
.name
]
134 or not factorio_versions
[rc
.name
][rt
.name
]
136 factorio_versions
[rc
.name
][rt
.name
] = factorio_versions
["stable"][
140 for system
in SYSTEMS
:
141 for release_type
in RELEASE_TYPES
:
142 for release_channel
in RELEASE_CHANNELS
:
143 version
= factorio_versions
[release_channel
.name
].get(release_type
.name
)
147 "name": f
"factorio_{release_type.name}_{system.tar_name}-{version}.tar.xz",
148 "url": f
"https://factorio.com/get-download/{version}/{release_type.name}/{system.url_name}",
150 "needsAuth": release_type
.needs_auth
,
151 "candidateHashFilenames": [
152 fmt
.format(version
=version
)
153 for fmt
in release_type
.hash_filename_format
155 "tarDirectory": system
.tar_name
,
157 output
[system
.nix_name
][release_type
.name
][release_channel
.name
] = (
164 versions
: OurVersionJSON
,
165 it
: Callable
[[str, str, str, Dict
[str, str]], Dict
[str, str]],
167 versions
= copy
.deepcopy(versions
)
168 for system_name
, system
in versions
.items():
169 for release_type_name
, release_type
in system
.items():
170 for release_channel_name
, release
in release_type
.items():
171 release_type
[release_channel_name
] = it(
172 system_name
, release_type_name
, release_channel_name
, dict(release
)
177 def merge_versions(old
: OurVersionJSON
, new
: OurVersionJSON
) -> OurVersionJSON
:
178 """Copies already-known hashes from version.json to avoid having to re-fetch."""
182 release_type_name
: str,
183 release_channel_name
: str,
184 release
: Dict
[str, str],
186 old_system
= old
.get(system_name
, {})
187 old_release_type
= old_system
.get(release_type_name
, {})
188 old_release
= old_release_type
.get(release_channel_name
, {})
189 if FLAGS
.release_type
and release_type_name
not in FLAGS
.release_type
:
191 "%s/%s/%s: not in --release_type, not updating",
194 release_channel_name
,
197 if FLAGS
.release_channel
and release_channel_name
not in FLAGS
.release_channel
:
199 "%s/%s/%s: not in --release_channel, not updating",
202 release_channel_name
,
205 if "sha256" not in old_release
:
207 "%s/%s/%s: not copying sha256 since it's missing",
210 release_channel_name
,
214 old_release
.get(k
, None) == release
[k
] for k
in ["name", "version", "url"]
217 "%s/%s/%s: not copying sha256 due to mismatch",
220 release_channel_name
,
223 release
["sha256"] = old_release
["sha256"]
226 return iter_version(new
, _merge_version
)
230 versions
: OurVersionJSON
, factorio_hashes
: FactorioHashes
232 """Fill in sha256 hashes for anything missing them."""
236 release_type_name
: str,
237 release_channel_name
: str,
238 release
: Dict
[str, str],
240 for candidate_filename
in release
["candidateHashFilenames"]:
241 if candidate_filename
in factorio_hashes
:
242 release
["sha256"] = factorio_hashes
[candidate_filename
]
246 "%s/%s/%s: failed to find any of %s in %s",
249 release_channel_name
,
250 release
["candidateHashFilenames"],
254 if "sha256" in release
:
256 "%s/%s/%s: skipping fetch, sha256 already present",
259 release_channel_name
,
264 return iter_version(versions
, _fill_in_hash
)
268 factorio_versions
= fetch_versions()
269 factorio_hashes
= fetch_hashes()
270 new_our_versions
= generate_our_versions(factorio_versions
)
271 old_our_versions
= None
272 our_versions_path
= find_versions_json()
273 if our_versions_path
:
274 logging
.info("Loading old versions.json from %s", our_versions_path
)
275 with
open(our_versions_path
, "r") as f
:
276 old_our_versions
= json
.load(f
)
278 logging
.info("Merging in old hashes")
279 new_our_versions
= merge_versions(old_our_versions
, new_our_versions
)
280 logging
.info("Updating hashes from Factorio SHA256")
281 new_our_versions
= fill_in_hash(new_our_versions
, factorio_hashes
)
282 with
open(our_versions_path
, "w") as f
:
283 logging
.info("Writing versions.json to %s", our_versions_path
)
284 json
.dump(new_our_versions
, f
, sort_keys
=True, indent
=2)
288 if __name__
== "__main__":