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.
@@ -0,0 +1,730 @@
1
+ # ghc_compiler_python/wrapper.py
2
+ """
3
+ Subprocess proxy wrappers for GHC and Cabal binaries.
4
+
5
+ Provides hermetic execution isolation, environment sterilization,
6
+ pre-flight C-linker validation, runtime path resolution, and process proxying.
7
+
8
+ FIX v5: Fixed LD_LIBRARY_PATH propagation to ghc-pkg recache.
9
+ Added platform-specific lib subdir to LD_LIBRARY_PATH on Linux.
10
+ Thread sterilized environment through _resolve_runtime_paths → _rebuild_package_cache → _ghc_pkg_recache.
11
+ FIX v4: Added ghc-pkg recache after @GHC_PREFIX@ replacement to regenerate package.cache.
12
+ FIX v3: Fixed platform-specific path detection for settings and package.conf.d.
13
+ FIX v2: Added DYLD_LIBRARY_PATH for macOS runtime library resolution.
14
+ """
15
+
16
+ import os
17
+ import sys
18
+ import shutil
19
+ import subprocess
20
+ import tempfile
21
+ import functools
22
+ import mmap
23
+ import re
24
+ from pathlib import Path
25
+ from typing import Any, List, NoReturn, Optional, Type
26
+
27
+
28
+ GHC_VERSION = "9.4.8"
29
+ CABAL_VERSION = "3.10.3.0"
30
+
31
+ HASKELL_POLLUTION_VARS = frozenset(
32
+ {
33
+ "GHC_PACKAGE_PATH",
34
+ "GHC_ENVIRONMENT",
35
+ "CABAL_DIR",
36
+ "CABAL_CONFIG",
37
+ "HASKELL_DIST_DIR",
38
+ "HASKELL_PACKAGE_SANDBOX",
39
+ "HASKELL_PACKAGE_SANDBOXES",
40
+ "STACK_ROOT",
41
+ "STACK_YAML",
42
+ "GHCRTS",
43
+ "GHCRTS_OPTS",
44
+ "HEAPSIZE",
45
+ "HOME",
46
+ }
47
+ )
48
+
49
+ _HOME_ORIGINAL: Optional[str] = None
50
+
51
+
52
+ def _die(msg: str) -> NoReturn:
53
+ sys.stderr.write(f"{msg}\n")
54
+ sys.exit(1)
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Toolchain root resolution
59
+ #
60
+ # The toolchain reaches a machine one of two ways:
61
+ #
62
+ # offline wheel — platform-tagged, toolchain bundled under sys.prefix
63
+ # thin wheel — pure Python, toolchain fetched to a user cache on first use
64
+ #
65
+ # Nothing records which tier is installed; it is observed. A bundled toolchain
66
+ # always wins, so an offline install never touches the network even if a cached
67
+ # payload happens to exist alongside it.
68
+ #
69
+ # Note that not every sys.prefix reference means "the GHC root". The writable
70
+ # HOME and the script directory are properties of the *environment* and stay on
71
+ # sys.prefix regardless of where GHC itself lives.
72
+ # ---------------------------------------------------------------------------
73
+
74
+ def _looks_like_ghc_root(base: Path) -> bool:
75
+ """True when `base` contains a usable GHC installation."""
76
+ if not base.is_dir():
77
+ return False
78
+ bin_dir = base / ("bin" if (base / "bin").is_dir() else "Scripts")
79
+ exe = "ghc.exe" if sys.platform == "win32" else "ghc"
80
+ if (bin_dir / exe).exists():
81
+ return True
82
+ # A staged install may expose ghc only under lib/ghc-<version>/bin.
83
+ return (base / "lib" / f"ghc-{GHC_VERSION}" / "bin" / exe).exists()
84
+
85
+
86
+ def _bundled_root() -> Optional[Path]:
87
+ """Return the bundled toolchain root, or None if this is a thin install."""
88
+ for candidate in (
89
+ Path(sys.prefix),
90
+ Path(__file__).resolve().parent.parent,
91
+ ):
92
+ if _looks_like_ghc_root(candidate):
93
+ return candidate
94
+ return None
95
+
96
+
97
+ def _ghc_root_if_present() -> Optional[Path]:
98
+ """Return an already-available toolchain root, or None. Never acquires.
99
+
100
+ Everything that merely *describes* the toolchain -- library search paths,
101
+ PATH construction, resource location -- uses this. Building an environment
102
+ must not trigger a download as a side effect; only executing a tool may,
103
+ and by then _resolve_binary has already acquired it.
104
+ """
105
+ bundled = _bundled_root()
106
+ if bundled is not None:
107
+ return bundled
108
+
109
+ try:
110
+ from . import bootstrap
111
+ return bootstrap.find_installed_root()
112
+ except ImportError: # pragma: no cover - defensive
113
+ return None
114
+
115
+
116
+ def _ghc_root_or_prefix() -> Path:
117
+ """Non-acquiring root with a harmless fallback for path construction."""
118
+ return _ghc_root_if_present() or Path(sys.prefix)
119
+
120
+
121
+ def _ghc_root() -> Path:
122
+ """Return the active toolchain root, acquiring it if absent.
123
+
124
+ Only the execution path calls this. Not cached: `ensure_payload` is already
125
+ idempotent and cheap once installed (a single stat), and caching a failure
126
+ would strand a process that could otherwise retry.
127
+ """
128
+ present = _ghc_root_if_present()
129
+ if present is not None:
130
+ return present
131
+
132
+ try:
133
+ from . import bootstrap
134
+ except ImportError: # pragma: no cover - defensive
135
+ _die(
136
+ "FATAL ERROR: no bundled GHC toolchain and the bootstrap module is "
137
+ "unavailable. Reinstall ghc-compiler-python."
138
+ )
139
+
140
+ try:
141
+ return bootstrap.ensure_payload()
142
+ except bootstrap.BootstrapError as exc:
143
+ _die(f"FATAL ERROR: {exc}")
144
+
145
+
146
+ def _is_text_file(filepath: Path) -> bool:
147
+ """Check if a file is a text file by looking for null bytes in the first 1024 bytes."""
148
+ try:
149
+ with filepath.open("rb") as f:
150
+ chunk = f.read(1024)
151
+ return b"\0" not in chunk
152
+ except OSError:
153
+ return False
154
+
155
+
156
+ def _search_roots() -> List[Path]:
157
+ """Roots to probe for native binaries, in priority order.
158
+
159
+ Deliberately does NOT call _ghc_root(): probing must not trigger a download
160
+ as a side effect of merely asking whether something is present.
161
+ """
162
+ roots: List[Path] = []
163
+ bundled = _bundled_root()
164
+ if bundled is not None:
165
+ roots.append(bundled)
166
+
167
+ try:
168
+ from . import bootstrap
169
+ cached = bootstrap.find_installed_root()
170
+ if cached is not None:
171
+ roots.append(cached)
172
+ except ImportError: # pragma: no cover - defensive
173
+ pass
174
+
175
+ roots.append(Path(sys.prefix))
176
+ roots.append(Path(__file__).resolve().parent.parent)
177
+ return roots
178
+
179
+
180
+ def _try_resolve_binary(name: str) -> Optional[str]:
181
+ """Resolve a native binary from what is already installed, or None.
182
+
183
+ Never acquires. Callers that require the binary go through _resolve_binary.
184
+ """
185
+ binary_name = f"{name}.exe" if sys.platform == "win32" else name
186
+
187
+ for root in _search_roots():
188
+ for bin_dir in ("bin", "Scripts"):
189
+ candidate = root / bin_dir / binary_name
190
+ if candidate.exists():
191
+ return str(candidate)
192
+ nested = root / "lib" / f"ghc-{GHC_VERSION}" / "bin" / binary_name
193
+ if nested.exists():
194
+ return str(nested)
195
+
196
+ # Deliberately NOT falling back to shutil.which().
197
+ #
198
+ # This package exists to provide a hermetic, pinned GHC 9.4.8. Resolving
199
+ # through PATH would silently hand the caller whatever GHC happens to be
200
+ # installed system-wide -- a different version, or a broken one -- while
201
+ # still reporting success. A machine with GHC 9.6 on PATH would never
202
+ # download our toolchain and would compile against the wrong compiler.
203
+ # Absent means absent; the caller acquires it.
204
+ return None
205
+
206
+
207
+ def _resolve_binary(name: str) -> str:
208
+ """Resolve a native binary, acquiring the toolchain if it is absent."""
209
+ resolved = _try_resolve_binary(name)
210
+ if resolved:
211
+ return resolved
212
+
213
+ # Nothing installed yet: materialise the toolchain, then look again.
214
+ root = _ghc_root()
215
+ binary_name = f"{name}.exe" if sys.platform == "win32" else name
216
+ for bin_dir in ("bin", "Scripts"):
217
+ candidate = root / bin_dir / binary_name
218
+ if candidate.exists():
219
+ return str(candidate)
220
+
221
+ return _try_resolve_binary(name) or _die(
222
+ f"FATAL ERROR: compiler binary '{name}' could not be located under {root}."
223
+ )
224
+
225
+
226
+ def _validate_c_linker() -> None:
227
+ """Pre-flight validation: assert the existence of a host C-linker."""
228
+ if not shutil.which("gcc") and not shutil.which("clang"):
229
+ _die("FATAL ERROR: The GHC compiler requires a host C-linker (gcc or clang).")
230
+
231
+
232
+ def _find_platform_lib_subdir() -> str:
233
+ """Find the platform-specific library subdirectory inside the GHC lib directory.
234
+
235
+ On Linux: lib/ghc-9.4.8/lib/x86_64-linux-ghc-9.4.8/
236
+ On macOS: lib/ghc-9.4.8/lib/aarch64-osx-ghc-9.4.8/ (or similar)
237
+ On Windows: Does not exist (DLLs are in mingw/bin/)
238
+ """
239
+ ghc_lib_dir = _ghc_root_or_prefix() / "lib" / f"ghc-{GHC_VERSION}" / "lib"
240
+ if not ghc_lib_dir.is_dir():
241
+ return ""
242
+
243
+ # 🧪 Alchemist: Generator expression with next() replaces manual iteration loop
244
+ return next((str(c) for c in ghc_lib_dir.iterdir() if c.is_dir() and c.name.endswith(f"-ghc-{GHC_VERSION}")), "")
245
+
246
+
247
+ def _sterilize_environment() -> dict:
248
+ """Create a sterilized subprocess environment with proper library paths."""
249
+ global _HOME_ORIGINAL
250
+ env = {k: v for k, v in os.environ.items() if k not in HASKELL_POLLUTION_VARS}
251
+
252
+ _HOME_ORIGINAL = os.environ.get("HOME", os.environ.get("USERPROFILE", ""))
253
+
254
+ def _get_home_path() -> Path:
255
+ try:
256
+ return Path.home() / ".ghc-compiler-python-home"
257
+ except RuntimeError:
258
+ return Path()
259
+
260
+ def _try_mkdir(path: Path) -> Optional[Path]:
261
+ try:
262
+ if str(path) != ".":
263
+ path.mkdir(parents=True, exist_ok=True)
264
+ return path
265
+ except (OSError, RuntimeError):
266
+ pass
267
+ return None
268
+
269
+ # 🧪 Alchemist: Declarative fallback chain replaces nested try-except blocks.
270
+ # Lazily evaluate Path.home() to prevent premature RuntimeError.
271
+ safe_home = (
272
+ _try_mkdir(Path(sys.prefix) / ".ghc-compiler-python-home") or
273
+ _try_mkdir(_get_home_path()) or
274
+ Path(tempfile.mkdtemp(prefix="ghc-compiler-python-home-"))
275
+ )
276
+
277
+ env["HOME"] = str(safe_home)
278
+
279
+ # The toolchain's own bin/ must lead PATH so GHC finds its sibling tools
280
+ # (ghc-pkg, hsc2hs, the mingw toolchain on Windows). When the toolchain is
281
+ # bundled these are the same directory; when bootstrapped they are not, and
282
+ # only the cache root holds the binaries.
283
+ bin_dir = "Scripts" if sys.platform == "win32" else "bin"
284
+ path_entries = []
285
+ ghc_root = _ghc_root_or_prefix()
286
+ for root in (ghc_root, Path(sys.prefix)):
287
+ for candidate in (root / "bin", root / bin_dir):
288
+ if candidate.is_dir() and str(candidate) not in path_entries:
289
+ path_entries.append(str(candidate))
290
+ if sys.platform == "win32":
291
+ mingw_bin = ghc_root / "mingw" / "bin"
292
+ if mingw_bin.is_dir():
293
+ path_entries.append(str(mingw_bin))
294
+
295
+ current_path = env.get("PATH", "")
296
+ env["PATH"] = os.pathsep.join([*path_entries, current_path]) if current_path \
297
+ else os.pathsep.join(path_entries)
298
+
299
+ # 🧪 Alchemist: Structural pattern matching replaces lambda-based dictionary lookup
300
+ match sys.platform:
301
+ case "darwin":
302
+ candidates = [
303
+ ghc_root / "lib" / f"ghc-{GHC_VERSION}" / "lib",
304
+ ghc_root / "lib",
305
+ ]
306
+ vars_to_update = ["DYLD_LIBRARY_PATH", "LD_LIBRARY_PATH"]
307
+ case "linux":
308
+ candidates = [
309
+ ghc_root / "lib" / f"ghc-{GHC_VERSION}",
310
+ ghc_root / "lib" / f"ghc-{GHC_VERSION}" / "lib",
311
+ Path(_find_platform_lib_subdir() or "."),
312
+ # auditwheel vendors shared objects here on the offline wheel.
313
+ Path(__file__).resolve().parent.parent / "ghc_compiler_python.libs",
314
+ ]
315
+ vars_to_update = ["LD_LIBRARY_PATH"]
316
+ case _:
317
+ candidates, vars_to_update = [], []
318
+
319
+ if lib_dirs_str := os.pathsep.join(
320
+ str(p) for p in candidates if p.is_dir() and str(p) != "."
321
+ ):
322
+ for var in vars_to_update:
323
+ env[var] = (
324
+ f"{lib_dirs_str}{os.pathsep}{env[var]}"
325
+ if env.get(var)
326
+ else lib_dirs_str
327
+ )
328
+
329
+ return env
330
+
331
+
332
+
333
+ class BaseResource:
334
+ """Base class for all GHC resource locators and patchers."""
335
+ name: str = ""
336
+ is_dir: bool = False
337
+ registry = []
338
+
339
+ def __init_subclass__(cls, **kwargs):
340
+ super().__init_subclass__(**kwargs)
341
+ cls.registry.append(cls)
342
+
343
+ @classmethod
344
+ @functools.lru_cache(maxsize=None)
345
+ def locate(cls, base: Optional[str] = None, version: str = GHC_VERSION) -> List[Path]:
346
+ """Locate all instances of this resource relative to a base directory.
347
+
348
+ `base` defaults to None rather than sys.prefix because a default
349
+ argument is bound once at import time: with sys.prefix baked in, a
350
+ bootstrapped toolchain would be searched for in the venv and never
351
+ found. Resolving inside the call also keeps the lru_cache keyed on the
352
+ root that was actually used.
353
+ """
354
+ base_path = Path(base) if base is not None else _ghc_root_or_prefix()
355
+ candidates = cls.get_candidates(base_path, version)
356
+
357
+ # Check explicit candidates first
358
+ for c in candidates:
359
+ # 🧪 Alchemist: Ternary conditional combines file and directory checks
360
+ if (c.is_dir() if cls.is_dir else c.is_file()) and cls.validate(c):
361
+ return [c]
362
+
363
+ # Dynamic fallback
364
+ found = []
365
+ if base_path.exists():
366
+ lib_dir = base_path / "lib"
367
+ search_dir = lib_dir if lib_dir.exists() else base_path
368
+
369
+ for root, dirs, files in os.walk(search_dir):
370
+ # ⚡ Bolt: Prune os.walk to prevent recursion into massive Python directories.
371
+ # Modifying `dirs` in place avoids walking into these branches entirely.
372
+ dirs[:] = [
373
+ d for d in dirs
374
+ if d not in {"site-packages", "dist-packages"}
375
+ and not d.startswith(("python", "pypy"))
376
+ ]
377
+
378
+ if cls.is_dir:
379
+ if cls.name in dirs:
380
+ p = Path(root) / cls.name
381
+ if cls.validate(p):
382
+ found.append(p)
383
+ else:
384
+ if cls.name in files:
385
+ p = Path(root) / cls.name
386
+ if cls.validate(p):
387
+ found.append(p)
388
+ return found
389
+
390
+ @classmethod
391
+ def get_candidates(cls, base: Path, version: str) -> List[Path]:
392
+ return []
393
+
394
+ @classmethod
395
+ def validate(cls, path: Path) -> bool:
396
+ return True
397
+
398
+ @classmethod
399
+ def extract_targets(cls, path: Path) -> List[str]:
400
+ """Extract actual file targets to be patched at runtime from the located resource."""
401
+ if not cls.is_dir:
402
+ return [str(path)]
403
+ return []
404
+
405
+ @classmethod
406
+ def patch_build_time(cls, path: Path, version: str, placeholder: str) -> int:
407
+ """Patch the resource for relocatability during build."""
408
+ return 0
409
+
410
+
411
+ class SettingsResource(BaseResource):
412
+ name = "settings"
413
+
414
+ @classmethod
415
+ def get_candidates(cls, base: Path, version: str) -> List[Path]:
416
+ return [
417
+ base / "lib" / f"ghc-{version}" / "lib" / "settings",
418
+ base / "lib" / "settings",
419
+ base / "settings"
420
+ ]
421
+
422
+ @classmethod
423
+ def validate(cls, path: Path) -> bool:
424
+ try:
425
+ content = path.read_text(encoding="utf-8", errors="replace")
426
+ return '"C compiler command"' in content or '"C preprocessor command"' in content
427
+ except OSError:
428
+ return False
429
+
430
+ @classmethod
431
+ def patch_build_time(cls, path: Path, version: str, placeholder: str) -> int:
432
+ try:
433
+ content = path.read_text(encoding="utf-8", errors="replace")
434
+ # 🧪 Alchemist: Combine regex patterns into a single pass using alternation
435
+ pattern = re.compile(
436
+ r"/(?:usr/local/lib|usr/lib|opt|ghc-prefix)/ghc(?:-|/)" + re.escape(version) + r"|/ghc-prefix"
437
+ )
438
+
439
+ def repl(m: re.Match) -> str:
440
+ match = m.group(0)
441
+ if match == "/ghc-prefix":
442
+ return placeholder
443
+ return f"{placeholder}/lib/ghc-{version}"
444
+
445
+ new_content = pattern.sub(repl, content)
446
+ if new_content != content:
447
+ path.write_text(new_content, encoding="utf-8")
448
+ return 1
449
+ except OSError as e:
450
+ sys.stderr.write(f"WARNING: Failed to patch {path}: {e}\n")
451
+ return 0
452
+
453
+
454
+ class PackageDBResource(BaseResource):
455
+ name = "package.conf.d"
456
+ is_dir = True
457
+
458
+ @classmethod
459
+ def get_candidates(cls, base: Path, version: str) -> List[Path]:
460
+ return [
461
+ base / "lib" / f"ghc-{version}" / "lib" / "package.conf.d",
462
+ base / "lib" / "package.conf.d",
463
+ base / "package.conf.d"
464
+ ]
465
+
466
+ @classmethod
467
+ def validate(cls, path: Path) -> bool:
468
+ try:
469
+ return any(f.name.endswith(".conf") for f in path.iterdir())
470
+ except OSError:
471
+ return False
472
+
473
+ @classmethod
474
+ def extract_targets(cls, path: Path) -> List[str]:
475
+ try:
476
+ return [str(f) for f in path.iterdir() if f.name.endswith(".conf") and not f.is_symlink()]
477
+ except OSError:
478
+ return []
479
+
480
+ @classmethod
481
+ def patch_build_time(cls, path: Path, version: str, placeholder: str) -> int:
482
+ patched_count = 0
483
+ for conf_file in path.glob("*.conf"):
484
+ try:
485
+ original = conf_file.read_text(encoding="utf-8", errors="replace")
486
+ # 🧪 Alchemist: Combine regex patterns into a single pass using alternation
487
+ pattern = re.compile(
488
+ r"(dynamic-library-dirs:\s*|library-dirs:\s*|include-dirs:\s*)/[^\s]+|/ghc-prefix/lib/ghc-" + re.escape(version) + r"|/ghc-prefix"
489
+ )
490
+
491
+ def repl(m: re.Match) -> str:
492
+ g1 = m.group(1)
493
+ if g1:
494
+ return f"{g1}{placeholder}/lib/ghc-{version}{'/include' if 'include' in g1 else ''}"
495
+ return placeholder if m.group(0) == "/ghc-prefix" else f"{placeholder}/lib/ghc-{version}"
496
+
497
+ content = pattern.sub(repl, original)
498
+
499
+ if content != original:
500
+ conf_file.write_text(content, encoding="utf-8")
501
+ patched_count += 1
502
+ except OSError as e:
503
+ sys.stderr.write(f"WARNING: Failed to patch {conf_file}: {e}\n")
504
+
505
+ # 🧪 Alchemist: Walrus operator (:=) consolidates variable assignment and existence check
506
+ try:
507
+ (path / "package.cache").unlink(missing_ok=True)
508
+ except OSError as e:
509
+ sys.stderr.write(f"WARNING: Failed to unlink {path / 'package.cache'}: {e}\n")
510
+ return patched_count
511
+
512
+
513
+ class BinWrappersResource(BaseResource):
514
+ name = "bin"
515
+ is_dir = True
516
+
517
+ @classmethod
518
+ def get_candidates(cls, base: Path, version: str) -> List[Path]:
519
+ return [
520
+ base / ("Scripts" if sys.platform == "win32" else "bin"),
521
+ base / "lib" / f"ghc-{version}" / "bin",
522
+ base / "bin",
523
+ base / "lib" / "bin"
524
+ ]
525
+
526
+ @classmethod
527
+ def validate(cls, path: Path) -> bool:
528
+ return True
529
+
530
+ @classmethod
531
+ def extract_targets(cls, path: Path) -> List[str]:
532
+ try:
533
+ return [
534
+ str(f) for f in path.iterdir()
535
+ if f.is_file() and not f.is_symlink() and not f.name.endswith(".exe") and _is_text_file(f)
536
+ ]
537
+ except OSError:
538
+ return []
539
+
540
+ @classmethod
541
+ def patch_build_time(cls, path: Path, version: str, placeholder: str) -> int:
542
+ patched = 0
543
+ for script in path.iterdir():
544
+ if not script.is_file() or script.is_symlink() or script.name.endswith(".exe") or not _is_text_file(script):
545
+ continue
546
+ try:
547
+ content = script.read_text(encoding="utf-8", errors="replace")
548
+ original = content
549
+
550
+ staging_dir = path.parent.parent if path.parent.name == f"ghc-{version}" else path.parent
551
+ abs_staging = staging_dir.absolute().as_posix()
552
+ abs_staging_win = str(staging_dir.absolute()).replace("/", "\\")
553
+
554
+ # 🧪 Alchemist: Combine regex patterns into a single pass using alternation
555
+ pattern = re.compile(
556
+ r"/usr/local/lib/ghc-" + re.escape(version) + r"|/ghc-prefix|" +
557
+ re.escape(abs_staging) + r"|" + re.escape(abs_staging_win)
558
+ )
559
+
560
+ def repl(m: re.Match) -> str:
561
+ return f"{placeholder}/lib/ghc-{version}" if m.group(0).startswith(f"/usr/local/lib/ghc-{version}") else placeholder
562
+
563
+ content = pattern.sub(repl, content)
564
+
565
+ if content != original:
566
+ script.write_text(content, encoding="utf-8")
567
+ patched += 1
568
+ except OSError as e:
569
+ sys.stderr.write(f"WARNING: Failed to patch {script}: {e}\n")
570
+ return patched
571
+ def _resolve_runtime_paths(env: dict) -> None:
572
+ """Dynamically replace @GHC_PREFIX@ with the active sys.prefix at runtime,
573
+ then regenerate package.cache.
574
+
575
+ Args:
576
+ env: The sterilized environment dict with proper LD_LIBRARY_PATH set.
577
+ """
578
+ # @GHC_PREFIX@ must resolve to wherever the toolchain actually lives. For a
579
+ # bundled install that is sys.prefix; for a bootstrapped one it is the cache
580
+ # root, and writing sys.prefix here would point GHC at a directory holding
581
+ # no compiler at all.
582
+ ghc_root = _ghc_root_or_prefix()
583
+ prefix_clean = str(ghc_root).replace("\\", "/")
584
+
585
+ # ⚡ Bolt: Fast-path to avoid scanning and patching on every invocation.
586
+ # If the marker file exists and contains the current prefix, we are already patched.
587
+ marker_file = ghc_root / "lib" / f".ghc_patched_{GHC_VERSION}.txt"
588
+ try:
589
+ if marker_file.is_file() and marker_file.read_text(encoding="utf-8") == prefix_clean:
590
+ return
591
+ except OSError:
592
+ pass
593
+
594
+ # 🐍 Ouroboros: Iterate over the BaseResource registry to locate all path targets dynamically
595
+ # 🧪 Alchemist: List comprehension condenses nested loops for dynamic target extraction
596
+ targets = [
597
+ target for resource_cls in BaseResource.registry
598
+ for resource_path in resource_cls.locate()
599
+ for target in resource_cls.extract_targets(resource_path)
600
+ ]
601
+
602
+ # Replace @GHC_PREFIX@ in all target files
603
+ prefix_clean_bytes = prefix_clean.encode("utf-8")
604
+ patched_any_conf = False
605
+ for target in set(targets): # 🧪 Alchemist: Deduplicate targets in a single pass
606
+ target_path = Path(target)
607
+ try:
608
+ # ⚡ Bolt: Use mmap to efficiently search for @GHC_PREFIX@ without loading
609
+ # the entire binary into memory. Drastically reduces I/O latency for large binaries.
610
+ content_to_write = None
611
+ with target_path.open("rb") as f:
612
+ try:
613
+ with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as m:
614
+ if m.find(b"@GHC_PREFIX@") != -1:
615
+ f.seek(0)
616
+ content_to_write = f.read()
617
+ except (ValueError, OSError):
618
+ # mmap throws ValueError for empty files, OSError for unmappable ones
619
+ f.seek(0)
620
+ content_to_write = f.read()
621
+ if b"@GHC_PREFIX@" not in content_to_write:
622
+ content_to_write = None
623
+
624
+ if content_to_write is not None:
625
+ # 🧪 Alchemist: Native byte regex replaces verbose decode/encode logic
626
+ if b" " in prefix_clean_bytes and b"\0" not in content_to_write:
627
+ content_to_write = re.sub(rb'(?<!")(@GHC_PREFIX@[^\s"]+)', rb'"\1"', content_to_write)
628
+ with target_path.open("wb") as out:
629
+ out.write(content_to_write.replace(b"@GHC_PREFIX@", prefix_clean_bytes))
630
+ if target.endswith(".conf"):
631
+ patched_any_conf = True
632
+ except OSError as e:
633
+ sys.stderr.write(f"WARNING: Failed to resolve runtime paths for {target_path}: {e}\n")
634
+
635
+ # 🧪 Alchemist: any() replaces manual flag variables and loops for succinct boolean reduction
636
+ if patched_any_conf or any(
637
+ not (pkg_db / "package.cache").exists()
638
+ for pkg_db in PackageDBResource.locate()
639
+ ):
640
+ for pkg_db in PackageDBResource.locate():
641
+ _ghc_pkg_recache(str(pkg_db), env)
642
+
643
+ # ⚡ Bolt: Write marker file to indicate this prefix has been successfully patched
644
+ try:
645
+ marker_file.parent.mkdir(parents=True, exist_ok=True)
646
+ marker_file.write_text(prefix_clean, encoding="utf-8")
647
+ except OSError:
648
+ pass
649
+
650
+
651
+ def _ghc_pkg_recache(pkg_db_dir: str, env: dict) -> None:
652
+ """Run ghc-pkg recache for the given package database directory.
653
+
654
+ Args:
655
+ pkg_db_dir: Path to the package.conf.d directory.
656
+ env: The sterilized environment dict with proper LD_LIBRARY_PATH set.
657
+ """
658
+ ghc_pkg = _try_resolve_binary("ghc-pkg")
659
+ if not ghc_pkg:
660
+ return # Can't recache without ghc-pkg
661
+
662
+ try:
663
+ # Use the sterilized environment which has LD_LIBRARY_PATH properly set
664
+ # 🧪 Alchemist: Dictionary merge operator (|) replaces unpacking
665
+ subprocess.run(
666
+ [ghc_pkg, "recache", "--package-db", pkg_db_dir],
667
+ env=env | {"GHC_PACKAGE_PATH": pkg_db_dir},
668
+ stdout=subprocess.DEVNULL,
669
+ stderr=subprocess.DEVNULL,
670
+ timeout=30,
671
+ check=True,
672
+ )
673
+ except (subprocess.SubprocessError, OSError) as e:
674
+ sys.stderr.write(f"WARNING: ghc-pkg recache failed for {pkg_db_dir}: {e}\n")
675
+
676
+
677
+ def _execute_tool(tool_name: str, extra_args: Optional[List[str]] = None) -> NoReturn:
678
+ """Generic subprocess proxy for bundled Haskell tooling."""
679
+ _validate_c_linker()
680
+ env = _sterilize_environment()
681
+ _resolve_runtime_paths(env)
682
+ binary_path = _resolve_binary(tool_name)
683
+
684
+ cmd = [binary_path]
685
+ if extra_args:
686
+ cmd.extend(extra_args)
687
+ cmd.extend(sys.argv[1:])
688
+
689
+ try:
690
+ # 🧪 Alchemist: On POSIX systems, os.execve replaces the Python interpreter entirely.
691
+ # This eliminates the need for subprocess.run(), manual exit code forwarding,
692
+ # and signal handlers, freeing the memory of the wrapper script completely.
693
+ # However, Windows lacks native exec() and implements it by creating a new process
694
+ # and terminating the current one, which breaks shell wait() expectations.
695
+ if sys.platform != "win32":
696
+ os.execve(binary_path, cmd, env)
697
+ else:
698
+ sys.exit(subprocess.run(cmd, env=env).returncode)
699
+ except FileNotFoundError:
700
+ _die(f"FATAL ERROR: Binary not found at '{binary_path}'.")
701
+ except KeyboardInterrupt:
702
+ sys.exit(130)
703
+ except (subprocess.SubprocessError, OSError) as e:
704
+ _die(f"FATAL ERROR: Execution failed: {e}")
705
+
706
+
707
+ def __getattr__(name: str) -> Any:
708
+ """Dynamic console script entry point generator.
709
+
710
+ Generates execution closures dynamically for any binary requested via entry points
711
+ (e.g., execute_ghc, execute_cabal, execute_haddock).
712
+ """
713
+ if name.startswith("execute_"):
714
+ tool_name = name[8:].replace("_", "-")
715
+ extra_args = ["-v0"] if tool_name == "ghc" else None
716
+
717
+ def executor() -> NoReturn:
718
+ _execute_tool(tool_name, extra_args=extra_args)
719
+
720
+ executor.__name__ = name
721
+ return executor
722
+
723
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
724
+
725
+
726
+ def __dir__() -> List[str]:
727
+ """Provide explicit autocompletion for common dynamically generated entry points."""
728
+ base_dir = list(globals().keys())
729
+ dynamic_tools = ["execute_ghc", "execute_ghci", "execute_cabal"]
730
+ return base_dir + dynamic_tools