ghc-compiler-python 9.4.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ghc_compiler_python/__init__.py +13 -0
- ghc_compiler_python/bootstrap.py +417 -0
- ghc_compiler_python/payload_hashes.json +5 -0
- ghc_compiler_python/py.typed +0 -0
- ghc_compiler_python/wrapper.py +730 -0
- ghc_compiler_python-9.4.8.dist-info/METADATA +208 -0
- ghc_compiler_python-9.4.8.dist-info/RECORD +10 -0
- ghc_compiler_python-9.4.8.dist-info/WHEEL +4 -0
- ghc_compiler_python-9.4.8.dist-info/entry_points.txt +11 -0
- ghc_compiler_python-9.4.8.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""ghc-compiler-python: Native GHC and Cabal packaged as a Python Wheel."""
|
|
2
|
+
|
|
3
|
+
__version__ = "9.4.8"
|
|
4
|
+
__ghc_version__ = "9.4.8"
|
|
5
|
+
__cabal_version__ = "3.10.3.0"
|
|
6
|
+
__author__ = "ghc-compiler-python contributors"
|
|
7
|
+
__license__ = "MIT"
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"__version__",
|
|
11
|
+
"__ghc_version__",
|
|
12
|
+
"__cabal_version__",
|
|
13
|
+
]
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# ghc_compiler_python/bootstrap.py
|
|
2
|
+
"""
|
|
3
|
+
First-run acquisition of the native GHC/Cabal payload.
|
|
4
|
+
|
|
5
|
+
This package ships in two tiers:
|
|
6
|
+
|
|
7
|
+
* **thin** — the PyPI wheel (``py3-none-any``). Pure Python; the native
|
|
8
|
+
toolchain is absent and is fetched from GitHub Releases on first use.
|
|
9
|
+
* **offline** — the platform-tagged wheels published on GitHub Releases.
|
|
10
|
+
The toolchain is bundled; this module is never invoked.
|
|
11
|
+
|
|
12
|
+
The tier is not recorded anywhere: it is *observed*. If a bundled toolchain is
|
|
13
|
+
present the wrapper uses it, otherwise it asks this module for a payload root.
|
|
14
|
+
|
|
15
|
+
Design constraints, in order of precedence:
|
|
16
|
+
|
|
17
|
+
1. Never half-install. Download, hash-verify, and extract happen out of line
|
|
18
|
+
and are promoted into place with a single atomic rename. A cache directory
|
|
19
|
+
that exists is a cache directory that is complete.
|
|
20
|
+
2. Never silently substitute. A payload whose SHA-256 does not match the
|
|
21
|
+
manifest shipped in this wheel is a hard failure, not a warning.
|
|
22
|
+
3. Never leave the user guessing. Failures name the offline wheel that would
|
|
23
|
+
have made the network unnecessary.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
import sys
|
|
33
|
+
import tarfile
|
|
34
|
+
import tempfile
|
|
35
|
+
import time
|
|
36
|
+
import urllib.error
|
|
37
|
+
import urllib.request
|
|
38
|
+
import zipfile
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import NoReturn, Optional
|
|
41
|
+
|
|
42
|
+
GHC_VERSION = "9.4.8"
|
|
43
|
+
|
|
44
|
+
#: Release assets are addressed by tag, so a wheel always fetches the payload
|
|
45
|
+
#: built alongside it rather than whatever happens to be newest.
|
|
46
|
+
_RELEASE_BASE = (
|
|
47
|
+
"https://github.com/Saimonokuma/GHC-COMPILER-PYTHON/releases/download"
|
|
48
|
+
f"/v{GHC_VERSION}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_HASH_MANIFEST = Path(__file__).resolve().parent / "payload_hashes.json"
|
|
52
|
+
|
|
53
|
+
_ENV_HOME = "GHC_COMPILER_PYTHON_HOME"
|
|
54
|
+
_ENV_OFFLINE = "GHC_COMPILER_PYTHON_OFFLINE"
|
|
55
|
+
|
|
56
|
+
_DOWNLOAD_TIMEOUT = 60
|
|
57
|
+
_LOCK_TIMEOUT = 900
|
|
58
|
+
_CHUNK = 1 << 20
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BootstrapError(RuntimeError):
|
|
62
|
+
"""Raised when the native payload cannot be made available."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _die(msg: str) -> NoReturn:
|
|
66
|
+
raise BootstrapError(msg)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# --------------------------------------------------------------------------
|
|
70
|
+
# platform identity
|
|
71
|
+
# --------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def platform_tag() -> str:
|
|
74
|
+
"""Return the payload tag for the running interpreter.
|
|
75
|
+
|
|
76
|
+
These strings are the *payload* identifiers and deliberately mirror the
|
|
77
|
+
wheel platform tags used for the offline tier, so a user who hits a network
|
|
78
|
+
failure can map the error message straight onto a release asset.
|
|
79
|
+
"""
|
|
80
|
+
machine = (os.uname().machine if hasattr(os, "uname") else os.environ.get(
|
|
81
|
+
"PROCESSOR_ARCHITECTURE", "AMD64")).lower()
|
|
82
|
+
|
|
83
|
+
if sys.platform.startswith("linux"):
|
|
84
|
+
if machine in ("x86_64", "amd64"):
|
|
85
|
+
return "manylinux_2_39_x86_64"
|
|
86
|
+
if machine in ("aarch64", "arm64"):
|
|
87
|
+
return "manylinux_2_39_aarch64"
|
|
88
|
+
elif sys.platform == "darwin":
|
|
89
|
+
if machine in ("arm64", "aarch64"):
|
|
90
|
+
return "macosx_11_0_arm64"
|
|
91
|
+
if machine == "x86_64":
|
|
92
|
+
return "macosx_10_9_x86_64"
|
|
93
|
+
elif sys.platform == "win32":
|
|
94
|
+
if machine in ("amd64", "x86_64"):
|
|
95
|
+
return "win_amd64"
|
|
96
|
+
|
|
97
|
+
_die(
|
|
98
|
+
f"Unsupported platform: {sys.platform}/{machine}. "
|
|
99
|
+
f"GHC {GHC_VERSION} payloads are published for Linux x86_64, "
|
|
100
|
+
"macOS arm64/x86_64 and Windows x86_64."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _archive_suffix() -> str:
|
|
105
|
+
# Windows runners produce a zip; the Unix payloads are xz tarballs.
|
|
106
|
+
return ".zip" if sys.platform == "win32" else ".tar.xz"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def payload_name() -> str:
|
|
110
|
+
return f"ghc-payload-{GHC_VERSION}-{platform_tag()}{_archive_suffix()}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def payload_url() -> str:
|
|
114
|
+
return f"{_RELEASE_BASE}/{payload_name()}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# --------------------------------------------------------------------------
|
|
118
|
+
# cache location
|
|
119
|
+
# --------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def cache_root() -> Path:
|
|
122
|
+
"""Directory holding extracted payloads, one subdirectory per version.
|
|
123
|
+
|
|
124
|
+
``GHC_COMPILER_PYTHON_HOME`` overrides the default for users who cannot
|
|
125
|
+
write to the standard location (CI images, locked-down hosts, or anyone who
|
|
126
|
+
simply wants the toolchain on a different volume).
|
|
127
|
+
"""
|
|
128
|
+
override = os.environ.get(_ENV_HOME)
|
|
129
|
+
if override:
|
|
130
|
+
return Path(override).expanduser().resolve()
|
|
131
|
+
|
|
132
|
+
if sys.platform == "win32":
|
|
133
|
+
base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
|
134
|
+
return Path(base) / "ghc-compiler-python" / "Cache"
|
|
135
|
+
if sys.platform == "darwin":
|
|
136
|
+
return Path.home() / "Library" / "Caches" / "ghc-compiler-python"
|
|
137
|
+
|
|
138
|
+
xdg = os.environ.get("XDG_CACHE_HOME")
|
|
139
|
+
base = Path(xdg) if xdg else Path.home() / ".cache"
|
|
140
|
+
return base / "ghc-compiler-python"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def payload_root() -> Path:
|
|
144
|
+
"""Where this version's toolchain lives once installed."""
|
|
145
|
+
return cache_root() / GHC_VERSION / platform_tag()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def is_installed() -> bool:
|
|
149
|
+
"""True when a complete payload is present.
|
|
150
|
+
|
|
151
|
+
Completion is signalled by a stamp written *after* the atomic rename, so a
|
|
152
|
+
directory that exists without a stamp is treated as absent and replaced.
|
|
153
|
+
"""
|
|
154
|
+
return (payload_root() / ".complete").is_file()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --------------------------------------------------------------------------
|
|
158
|
+
# integrity
|
|
159
|
+
# --------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _expected_digest() -> str:
|
|
162
|
+
if not _HASH_MANIFEST.is_file():
|
|
163
|
+
_die(
|
|
164
|
+
"Payload hash manifest is missing from the installed package "
|
|
165
|
+
f"({_HASH_MANIFEST}). This wheel was built incorrectly; reinstall "
|
|
166
|
+
"from PyPI or use an offline wheel from the releases page."
|
|
167
|
+
)
|
|
168
|
+
try:
|
|
169
|
+
manifest = json.loads(_HASH_MANIFEST.read_text(encoding="utf-8"))
|
|
170
|
+
except (OSError, ValueError) as exc:
|
|
171
|
+
_die(f"Payload hash manifest is unreadable: {exc}")
|
|
172
|
+
|
|
173
|
+
digest = manifest.get(payload_name())
|
|
174
|
+
if not digest:
|
|
175
|
+
_die(
|
|
176
|
+
f"No SHA-256 recorded for {payload_name()} in the hash manifest. "
|
|
177
|
+
"This platform was not published for this version."
|
|
178
|
+
)
|
|
179
|
+
return str(digest).lower()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _digest_file(path: Path) -> str:
|
|
183
|
+
h = hashlib.sha256()
|
|
184
|
+
with path.open("rb") as fh:
|
|
185
|
+
for block in iter(lambda: fh.read(_CHUNK), b""):
|
|
186
|
+
h.update(block)
|
|
187
|
+
return h.hexdigest()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# --------------------------------------------------------------------------
|
|
191
|
+
# acquisition
|
|
192
|
+
# --------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
def _offline_hint() -> str:
|
|
195
|
+
return (
|
|
196
|
+
"If this machine has no network access, install the self-contained "
|
|
197
|
+
"wheel instead — it bundles the toolchain and never downloads:\n"
|
|
198
|
+
f" {_RELEASE_BASE}/"
|
|
199
|
+
f"ghc_compiler_python-{GHC_VERSION}-py3-none-{platform_tag()}.whl"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _download(url: str, dest: Path, quiet: bool) -> None:
|
|
204
|
+
if not quiet:
|
|
205
|
+
sys.stderr.write(f"Fetching GHC {GHC_VERSION} payload ({platform_tag()})\n")
|
|
206
|
+
sys.stderr.write(f" from {url}\n")
|
|
207
|
+
|
|
208
|
+
request = urllib.request.Request(
|
|
209
|
+
url, headers={"User-Agent": f"ghc-compiler-python/{GHC_VERSION}"}
|
|
210
|
+
)
|
|
211
|
+
try:
|
|
212
|
+
with urllib.request.urlopen(request, timeout=_DOWNLOAD_TIMEOUT) as response:
|
|
213
|
+
total = int(response.headers.get("Content-Length") or 0)
|
|
214
|
+
seen = 0
|
|
215
|
+
last = 0.0
|
|
216
|
+
with dest.open("wb") as out:
|
|
217
|
+
while True:
|
|
218
|
+
chunk = response.read(_CHUNK)
|
|
219
|
+
if not chunk:
|
|
220
|
+
break
|
|
221
|
+
out.write(chunk)
|
|
222
|
+
seen += len(chunk)
|
|
223
|
+
now = time.monotonic()
|
|
224
|
+
if not quiet and total and now - last > 0.5:
|
|
225
|
+
last = now
|
|
226
|
+
pct = seen * 100 // total
|
|
227
|
+
sys.stderr.write(
|
|
228
|
+
f"\r {pct:3d}% {seen >> 20} / {total >> 20} MiB"
|
|
229
|
+
)
|
|
230
|
+
sys.stderr.flush()
|
|
231
|
+
if not quiet and total:
|
|
232
|
+
sys.stderr.write("\r 100% done \n")
|
|
233
|
+
except urllib.error.HTTPError as exc:
|
|
234
|
+
_die(
|
|
235
|
+
f"Download failed with HTTP {exc.code} for {url}\n"
|
|
236
|
+
f"{_offline_hint()}"
|
|
237
|
+
)
|
|
238
|
+
except (urllib.error.URLError, OSError, TimeoutError) as exc:
|
|
239
|
+
_die(f"Download failed: {exc}\n{_offline_hint()}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _is_within(base: Path, target: Path) -> bool:
|
|
243
|
+
try:
|
|
244
|
+
target.resolve().relative_to(base.resolve())
|
|
245
|
+
return True
|
|
246
|
+
except ValueError:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _extract(archive: Path, dest: Path) -> None:
|
|
251
|
+
"""Extract the payload, refusing entries that escape the destination.
|
|
252
|
+
|
|
253
|
+
Release assets are our own, but an archive member is still untrusted input:
|
|
254
|
+
a traversal entry would write outside the cache. Python 3.12 grew a
|
|
255
|
+
``filter`` argument for exactly this; older interpreters are checked by hand
|
|
256
|
+
so the guarantee does not depend on the runtime version.
|
|
257
|
+
"""
|
|
258
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
|
|
260
|
+
if archive.suffix == ".zip":
|
|
261
|
+
with zipfile.ZipFile(archive) as zf:
|
|
262
|
+
for member in zf.namelist():
|
|
263
|
+
if not _is_within(dest, dest / member):
|
|
264
|
+
_die(f"Refusing archive entry outside destination: {member}")
|
|
265
|
+
zf.extractall(dest)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
with tarfile.open(archive, "r:xz") as tf:
|
|
269
|
+
if hasattr(tarfile, "data_filter"):
|
|
270
|
+
tf.extractall(dest, filter="data")
|
|
271
|
+
else:
|
|
272
|
+
for member in tf.getmembers():
|
|
273
|
+
if member.issym() or member.islnk():
|
|
274
|
+
link = dest / Path(member.name).parent / member.linkname
|
|
275
|
+
if not _is_within(dest, link):
|
|
276
|
+
_die(f"Refusing link escaping destination: {member.name}")
|
|
277
|
+
elif not _is_within(dest, dest / member.name):
|
|
278
|
+
_die(f"Refusing archive entry outside destination: {member.name}")
|
|
279
|
+
tf.extractall(dest)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _restore_exec_bits(root: Path) -> None:
|
|
283
|
+
"""Re-assert the executable bit on ``bin/`` after extraction.
|
|
284
|
+
|
|
285
|
+
Zip carries no POSIX mode, and a payload unpacked from one on a Unix host
|
|
286
|
+
would otherwise yield a toolchain that cannot be run.
|
|
287
|
+
"""
|
|
288
|
+
if sys.platform == "win32":
|
|
289
|
+
return
|
|
290
|
+
for directory in (root / "bin", root / "lib"):
|
|
291
|
+
if not directory.is_dir():
|
|
292
|
+
continue
|
|
293
|
+
for path in directory.rglob("*"):
|
|
294
|
+
if path.is_file() and not path.is_symlink():
|
|
295
|
+
mode = path.stat().st_mode
|
|
296
|
+
if mode & 0o111 or path.suffix in ("", ".so"):
|
|
297
|
+
path.chmod(mode | 0o755)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class _DirectoryLock:
|
|
301
|
+
"""Cross-process lock built on atomic ``mkdir``.
|
|
302
|
+
|
|
303
|
+
Two interpreters importing the package at once must not both download
|
|
304
|
+
350 MB. ``mkdir`` is atomic on every filesystem we target, which makes it a
|
|
305
|
+
more portable primitive here than ``fcntl`` or ``msvcrt`` locking.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
def __init__(self, path: Path) -> None:
|
|
309
|
+
self._path = path
|
|
310
|
+
self._held = False
|
|
311
|
+
|
|
312
|
+
def __enter__(self) -> "_DirectoryLock":
|
|
313
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
deadline = time.monotonic() + _LOCK_TIMEOUT
|
|
315
|
+
announced = False
|
|
316
|
+
while True:
|
|
317
|
+
try:
|
|
318
|
+
self._path.mkdir()
|
|
319
|
+
self._held = True
|
|
320
|
+
return self
|
|
321
|
+
except FileExistsError:
|
|
322
|
+
if is_installed():
|
|
323
|
+
# Whoever held the lock finished the job for us.
|
|
324
|
+
return self
|
|
325
|
+
if time.monotonic() > deadline:
|
|
326
|
+
_die(
|
|
327
|
+
f"Timed out waiting for another process to finish the "
|
|
328
|
+
f"GHC download (lock: {self._path}). If no other "
|
|
329
|
+
"install is running, remove that directory and retry."
|
|
330
|
+
)
|
|
331
|
+
if not announced:
|
|
332
|
+
announced = True
|
|
333
|
+
sys.stderr.write(
|
|
334
|
+
"Waiting for a concurrent GHC installation to finish...\n"
|
|
335
|
+
)
|
|
336
|
+
time.sleep(0.5)
|
|
337
|
+
|
|
338
|
+
def __exit__(self, *_exc: object) -> None:
|
|
339
|
+
if self._held:
|
|
340
|
+
shutil.rmtree(self._path, ignore_errors=True)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def ensure_payload(quiet: bool = False) -> Path:
|
|
344
|
+
"""Return the payload root, installing it first if necessary.
|
|
345
|
+
|
|
346
|
+
Idempotent and safe to call from every entry point on every invocation:
|
|
347
|
+
the fast path is a single ``stat`` of the completion stamp.
|
|
348
|
+
"""
|
|
349
|
+
root = payload_root()
|
|
350
|
+
if is_installed():
|
|
351
|
+
return root
|
|
352
|
+
|
|
353
|
+
if os.environ.get(_ENV_OFFLINE, "").strip().lower() in ("1", "true", "yes"):
|
|
354
|
+
_die(
|
|
355
|
+
f"{_ENV_OFFLINE} is set and no toolchain is installed at {root}.\n"
|
|
356
|
+
f"{_offline_hint()}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
expected = _expected_digest()
|
|
360
|
+
|
|
361
|
+
with _DirectoryLock(cache_root() / f".lock-{GHC_VERSION}-{platform_tag()}"):
|
|
362
|
+
if is_installed():
|
|
363
|
+
return root
|
|
364
|
+
|
|
365
|
+
root.parent.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
staging = Path(tempfile.mkdtemp(prefix=".ghc-staging-", dir=str(root.parent)))
|
|
367
|
+
archive = staging / payload_name()
|
|
368
|
+
try:
|
|
369
|
+
_download(payload_url(), archive, quiet)
|
|
370
|
+
|
|
371
|
+
actual = _digest_file(archive)
|
|
372
|
+
if actual != expected:
|
|
373
|
+
_die(
|
|
374
|
+
"Payload integrity check FAILED — refusing to install.\n"
|
|
375
|
+
f" expected SHA-256: {expected}\n"
|
|
376
|
+
f" actual SHA-256: {actual}\n"
|
|
377
|
+
"The download was corrupted or the release asset was "
|
|
378
|
+
"modified. Nothing has been installed."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
unpacked = staging / "root"
|
|
382
|
+
_extract(archive, unpacked)
|
|
383
|
+
archive.unlink(missing_ok=True)
|
|
384
|
+
|
|
385
|
+
# GHC tarballs carry a single top-level directory; flatten it so the
|
|
386
|
+
# payload root always has bin/ and lib/ directly beneath it.
|
|
387
|
+
entries = [p for p in unpacked.iterdir()]
|
|
388
|
+
if len(entries) == 1 and entries[0].is_dir():
|
|
389
|
+
unpacked = entries[0]
|
|
390
|
+
|
|
391
|
+
_restore_exec_bits(unpacked)
|
|
392
|
+
(unpacked / ".complete").write_text(
|
|
393
|
+
f"{GHC_VERSION} {platform_tag()}\n", encoding="utf-8"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
if root.exists():
|
|
397
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
398
|
+
os.replace(unpacked, root)
|
|
399
|
+
finally:
|
|
400
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
401
|
+
|
|
402
|
+
if not quiet:
|
|
403
|
+
sys.stderr.write(f"GHC {GHC_VERSION} installed to {root}\n")
|
|
404
|
+
return root
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def find_installed_root() -> Optional[Path]:
|
|
408
|
+
"""Return the cached payload root, or ``None`` — never downloads.
|
|
409
|
+
|
|
410
|
+
Used by the wrapper to decide whether a bundled toolchain or a previously
|
|
411
|
+
bootstrapped one should win, without triggering acquisition as a side
|
|
412
|
+
effect of merely asking.
|
|
413
|
+
"""
|
|
414
|
+
try:
|
|
415
|
+
return payload_root() if is_installed() else None
|
|
416
|
+
except BootstrapError:
|
|
417
|
+
return None
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ghc-payload-9.4.8-macosx_11_0_arm64.tar.xz": "eb826736bb3fa238603183089872fced21231660bc5c1e74067c77a7a77965f9",
|
|
3
|
+
"ghc-payload-9.4.8-manylinux_2_39_x86_64.tar.xz": "09b4127c288f0c08f77ec4bfbcd2dbc4192566d84b0db4d7c16942dc53d7fd72",
|
|
4
|
+
"ghc-payload-9.4.8-win_amd64.zip": "31629e8a008d3adf83e0869463c1a3bdf696ae02bf5be51fa110cc8cba484d1d"
|
|
5
|
+
}
|
|
File without changes
|