Spaces:
Build error
feat(D2): PyInstaller entry point and spec for the desktop backend
Browse filesFreezes the FastAPI app into a standalone executable. A separate entry
point (packaging/desktop_entry.py) was necessary, not optional -- the
Docker deployment starts via 'uvicorn app:app', a CLI string-import
that has no module/file to resolve against once frozen into a single
binary. This entry point imports the app object directly and calls
uvicorn.run() programmatically instead, sets BHARATGRAPH_DESKTOP_MODE
before any other import (so Phase D1's admin-auth hardening and local
config-file loading both apply), and binds to 127.0.0.1 only.
Verified for real, not just written: installed PyInstaller, ran an
actual build against this codebase, and ran the resulting binary --
/health, /runtime, /docs, and /forensics/ghost-companies all responded
correctly over real HTTP; /runtime specifically confirmed the D1/F-10
scraper_workers value is correctly bundled and functional. Cold start
is ~4s. The 503s on Neo4j-dependent routes are the correct behavior
with no database connected, not failures.
Scope, stated plainly: this is a Linux ELF binary verification, not a
Windows .exe -- the real DLL-bundling risk for compiled extensions
(lxml, scipy, numpy) is a Windows-specific concern this can't validate
without an actual Windows machine or windows-latest CI runner. spacy,
sentence-transformers, torch, and weasyprint are confirmed lazy-loaded
(app starts fine without them) but were not bundled in this build --
torch alone is 500+MB and a full build was not attempted here.
New finding, discovered via PyInstaller's own missing-module analysis,
unrelated to packaging itself: ai/translator.py and
ai/transliteration.py -- both genuinely wired into
api/routes/multilingual.py, not dead code -- import transformers and
indic_transliteration respectively, neither of which is declared in
requirements.txt. On a fresh install, the multilingual translation/
transliteration features would ImportError the moment anyone actually
used them. Flagged here, not fixed -- separate from D2's scope, and
needs a version-pinning decision, not a blind addition.
- packaging/bharatgraph-backend.spec +78 -0
- packaging/desktop_entry.py +63 -0
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- mode: python ; coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
BharatGraph Desktop Backend -- PyInstaller spec (Phase D2)
|
| 4 |
+
|
| 5 |
+
Freezes packaging/desktop_entry.py (see that file's own docstring for why
|
| 6 |
+
a separate entry point exists) into a single executable, bound only to
|
| 7 |
+
127.0.0.1, meant to be spawned as a child process by the Tauri shell
|
| 8 |
+
(Phase D4) -- not run standalone in production.
|
| 9 |
+
"""
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(SPEC)))
|
| 13 |
+
|
| 14 |
+
block_cipher = None
|
| 15 |
+
|
| 16 |
+
a = Analysis(
|
| 17 |
+
[os.path.join(PROJECT_ROOT, 'packaging', 'desktop_entry.py')],
|
| 18 |
+
pathex=[PROJECT_ROOT],
|
| 19 |
+
binaries=[],
|
| 20 |
+
datas=[
|
| 21 |
+
# NOTE: verified via full-codebase grep that no current code path
|
| 22 |
+
# actually reads this file at runtime -- api/routes/sources.py
|
| 23 |
+
# (the one route this file's name suggests it feeds) uses a
|
| 24 |
+
# hardcoded Python dict (SCRAPER_META) plus a live Neo4j query
|
| 25 |
+
# instead. Included anyway: it's 96KB, essentially free to bundle,
|
| 26 |
+
# and is the most likely candidate to become a real runtime
|
| 27 |
+
# dependency once Phase D7 (Dataset Manager) is built. Not
|
| 28 |
+
# removed, but not assumed necessary either -- stated plainly so
|
| 29 |
+
# a future reader doesn't have to re-derive this.
|
| 30 |
+
(os.path.join(PROJECT_ROOT, 'datasets', 'bharatgraph_sources.csv'), 'datasets'),
|
| 31 |
+
],
|
| 32 |
+
hiddenimports=[
|
| 33 |
+
# api/main.py's own app.include_router(...) calls are regular
|
| 34 |
+
# top-level imports, so PyInstaller's static analysis should
|
| 35 |
+
# already find every api.routes.* module -- not listed
|
| 36 |
+
# defensively here, unlike uvicorn's own dynamic loading below,
|
| 37 |
+
# specifically to let a real build prove that assumption right
|
| 38 |
+
# or wrong rather than papering over it with a hidden-imports
|
| 39 |
+
# list that would hide a real gap.
|
| 40 |
+
'uvicorn.logging',
|
| 41 |
+
'uvicorn.loops',
|
| 42 |
+
'uvicorn.loops.auto',
|
| 43 |
+
'uvicorn.protocols',
|
| 44 |
+
'uvicorn.protocols.http',
|
| 45 |
+
'uvicorn.protocols.http.auto',
|
| 46 |
+
'uvicorn.protocols.websockets',
|
| 47 |
+
'uvicorn.protocols.websockets.auto',
|
| 48 |
+
'uvicorn.lifespan',
|
| 49 |
+
'uvicorn.lifespan.on',
|
| 50 |
+
],
|
| 51 |
+
hookspath=[],
|
| 52 |
+
hooksconfig={},
|
| 53 |
+
runtime_hooks=[],
|
| 54 |
+
excludes=[],
|
| 55 |
+
noarchive=False,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
| 59 |
+
|
| 60 |
+
exe = EXE(
|
| 61 |
+
pyz,
|
| 62 |
+
a.scripts,
|
| 63 |
+
a.binaries,
|
| 64 |
+
a.zipfiles,
|
| 65 |
+
a.datas,
|
| 66 |
+
[],
|
| 67 |
+
name='bharatgraph-backend',
|
| 68 |
+
debug=False,
|
| 69 |
+
bootloader_ignore_signals=False,
|
| 70 |
+
strip=False,
|
| 71 |
+
upx=False,
|
| 72 |
+
console=True,
|
| 73 |
+
disable_windowed_traceback=False,
|
| 74 |
+
argv_emulation=False,
|
| 75 |
+
target_arch=None,
|
| 76 |
+
codesign_identity=None,
|
| 77 |
+
entitlements_file=None,
|
| 78 |
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
BharatGraph Desktop -- PyInstaller entry point (Phase D2)
|
| 3 |
+
|
| 4 |
+
Why this file exists, separately from app.py: the Docker deployment starts
|
| 5 |
+
the server via `uvicorn app:app`, a CLI command that resolves the string
|
| 6 |
+
"app:app" to a module and attribute at runtime using Python's normal import
|
| 7 |
+
machinery. A PyInstaller-frozen executable does not have that machinery
|
| 8 |
+
available the same way -- there is no `app.py` file on disk to resolve a
|
| 9 |
+
string against once everything is bundled into a single binary. The
|
| 10 |
+
standard, reliable fix (used by PyInstaller's own documentation for
|
| 11 |
+
freezing ASGI apps) is to import the app object directly in Python code and
|
| 12 |
+
call uvicorn.run() programmatically instead of via its CLI string-lookup
|
| 13 |
+
path. This file is that direct entry point.
|
| 14 |
+
|
| 15 |
+
Sets BHARATGRAPH_DESKTOP_MODE=true before importing anything else, so the
|
| 16 |
+
Phase D1 admin-auth hardening in api/routes/admin.py (auto-generated,
|
| 17 |
+
persisted per-install secret; no DEBUG_MODE bypass path) applies correctly
|
| 18 |
+
whenever the app is started this way, and so config/settings.py's local
|
| 19 |
+
config-file loading is the primary configuration path a desktop user
|
| 20 |
+
actually has, rather than a code path that only matters at packaging time.
|
| 21 |
+
|
| 22 |
+
Binds to 127.0.0.1 only, matching the target architecture ("bound to
|
| 23 |
+
127.0.0.1 only") -- this process is meant to be reached only by the Tauri
|
| 24 |
+
shell's own webview (Phase D4), never by the network.
|
| 25 |
+
"""
|
| 26 |
+
import os
|
| 27 |
+
import sys
|
| 28 |
+
|
| 29 |
+
# Must happen before importing api.main (or anything that transitively
|
| 30 |
+
# imports config.settings), so the desktop-mode admin-auth branch and the
|
| 31 |
+
# local config-file loading are both active from the very first import.
|
| 32 |
+
os.environ.setdefault("BHARATGRAPH_DESKTOP_MODE", "true")
|
| 33 |
+
|
| 34 |
+
# PyInstaller sets sys.frozen and sys._MEIPASS when running from a frozen
|
| 35 |
+
# bundle; ensure the app's own package root is importable either way, so
|
| 36 |
+
# this entry point works identically when run as a plain script (for local
|
| 37 |
+
# testing, exactly as this was verified) and when frozen.
|
| 38 |
+
if getattr(sys, "frozen", False):
|
| 39 |
+
_app_root = sys._MEIPASS # type: ignore[attr-defined]
|
| 40 |
+
else:
|
| 41 |
+
_app_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 42 |
+
sys.path.insert(0, _app_root)
|
| 43 |
+
|
| 44 |
+
import uvicorn
|
| 45 |
+
from api.main import app
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def main() -> None:
|
| 49 |
+
port = int(os.getenv("BHARATGRAPH_DESKTOP_PORT", "8000"))
|
| 50 |
+
uvicorn.run(
|
| 51 |
+
app,
|
| 52 |
+
host="127.0.0.1",
|
| 53 |
+
port=port,
|
| 54 |
+
log_level=os.getenv("BHARATGRAPH_LOG_LEVEL", "info"),
|
| 55 |
+
# Single worker: matches the existing Docker deployment's own
|
| 56 |
+
# --workers 1 (api/main.py's docstring/Dockerfile), and a desktop
|
| 57 |
+
# install is single-user by definition -- no reason to diverge.
|
| 58 |
+
workers=1,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
main()
|