Coverage for src / lilbee / __main__.py: 100%
44 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-06-28 01:01 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-06-28 01:01 +0000
1"""Allow running as `python -m lilbee`."""
3from __future__ import annotations
5import importlib
6import os
7import sys
8from pathlib import Path
10_FLATPAK_INFO = Path("/.flatpak-info") # present in every Flatpak sandbox
13def _isolate_vendored_openssl() -> None:
14 """Default ``OPENSSL_CONF`` to the empty config inside Flatpak sandboxes.
16 Flatpak's freedesktop runtime ships an openssl.cnf whose engine section
17 dlopens engine modules built against the runtime's own OpenSSL. Bundled
18 wheels that vendor a static OpenSSL (pyarrow's libarrow) honor that
19 config during import-time init, load the ABI-incompatible engine, and
20 segfault before any lilbee code runs. An empty config keeps every
21 vendored OpenSSL self-contained; certificate paths are unaffected.
22 Scoped to Flatpak sandboxes so every other install keeps reading the
23 host config, and an explicitly set ``OPENSSL_CONF`` always wins.
24 Deliberately not gated on a frozen-binary check: Nuitka does not set
25 ``sys.frozen``, and a pip install run inside a sandbox crashes the same
26 way.
27 """
28 if sys.platform != "linux" or not _FLATPAK_INFO.exists():
29 return
30 os.environ.setdefault("OPENSSL_CONF", os.devnull)
33def _is_frozen() -> bool:
34 """True when running from a frozen build.
36 A deliberate local copy of :func:`lilbee._frozen.is_frozen`: the dispatch
37 path below must import nothing from the ``lilbee`` package (the splash
38 subprocess this intercepts is stdlib-only and must stay light), so it
39 cannot import the shared helper. PyInstaller/cx_Freeze set ``sys.frozen``;
40 Nuitka never does, but injects a ``__compiled__`` global into every module
41 it compiles. Both must be checked so the dispatchers fire in the shipped
42 Nuitka onefile binary, not just under PyInstaller.
43 """
44 return bool(sys.__dict__.get("frozen")) or "__compiled__" in globals()
47def _multiprocessing_child_code(argv: list[str]) -> str | None:
48 """Return the ``-c`` payload if *argv* is a multiprocessing child reinvocation.
50 Python's ``multiprocessing.resource_tracker`` respawns ``sys.executable``
51 with interpreter flags followed by ``-c "<code>"`` (e.g.
52 ``-B -s -E -c "from multiprocessing.resource_tracker import main;main(7)"``).
53 In a frozen build (Nuitka onefile) ``sys.executable`` is this exe, so those
54 interpreter flags leak into the typer parser and raise ``No such option: -B``
55 (or, once typer has eaten the flags, ``No such command '<N>'`` for the
56 leftover fd number). The stdlib ``freeze_support()`` hook only covers spawn
57 children (``--multiprocessing-fork``), not resource_tracker.
59 Require the payload to mention ``multiprocessing`` or ``spawn_main`` so a
60 stray ``lilbee -c …`` typed by a human never reaches ``exec``.
61 """
62 try:
63 idx = argv.index("-c")
64 except ValueError:
65 return None
66 if idx + 1 >= len(argv):
67 return None
68 code = argv[idx + 1]
69 if "multiprocessing" not in code and "spawn_main" not in code:
70 return None
71 return code
74def _dispatch_frozen_child() -> bool:
75 """Exec a multiprocessing child payload and return True when handled."""
76 if not _is_frozen():
77 return False
78 code = _multiprocessing_child_code(sys.argv)
79 if code is None:
80 return False
81 exec( # noqa: S102 payload is emitted by Python's own stdlib into sys.executable
82 compile(code, "<frozen-mp-child>", "exec"),
83 {"__name__": "__main__", "__builtins__": __builtins__},
84 )
85 return True
88_DASH_M_MIN_ARGV = 3 # [bin, "-m", "lilbee.<module>"]
91def _dispatch_module_invocation() -> bool:
92 """Run a `python -m lilbee.<module>` reinvocation and return True when handled.
94 Internal subprocesses spawned via ``[sys.executable, "-m", "lilbee.X", ...]``
95 (e.g. ``splash.start`` launching ``lilbee.runtime._splash_runner``) hit
96 typer's `--model -m` short-form parser in a frozen build. Detect the
97 pattern, run the target module's ``main()`` directly, never reach typer.
99 Imports the module and calls ``main()`` rather than ``runpy.run_module``:
100 Nuitka's module loader does not implement ``get_code``, so ``runpy`` raises
101 ``AttributeError: ... has no attribute 'get_code'`` inside the onefile
102 binary. Every ``lilbee.*`` module reinvoked this way exposes a ``main()``
103 entry point. Restricted to the ``lilbee.*`` namespace so a stray
104 ``lilbee -m foo`` typed by a user can't reach exec.
105 """
106 if not _is_frozen():
107 return False
108 if len(sys.argv) < _DASH_M_MIN_ARGV or sys.argv[1] != "-m":
109 return False
110 module_name = sys.argv[2]
111 if not module_name.startswith("lilbee."):
112 return False
113 sys.argv = [module_name, *sys.argv[3:]]
114 module = importlib.import_module(module_name)
115 module.main()
116 return True
119if __name__ == "__main__": # pragma: no cover - process entry glue; logic is unit-tested above
120 # Must run before anything that can initialize OpenSSL (pyarrow import,
121 # ssl), including the multiprocessing child payloads dispatched below.
122 _isolate_vendored_openssl()
124 # Make the frozen exe a valid subprocess target for multiprocessing's
125 # sys.executable reinvocations, BEFORE any import that could pull typer.
126 if _dispatch_module_invocation():
127 sys.exit(0)
128 if _dispatch_frozen_child():
129 sys.exit(0)
131 import multiprocessing
133 multiprocessing.freeze_support()
135 from lilbee.runtime.launcher import main
137 main()