# PyInstaller spec for the bundled Windows node daemon, and the command-line # client beside it. # # Output: dist/meshbay-node/meshbay-node.exe, meshbay-nodew.exe and # meshbay-cli.exe (--onedir, relocatable, one _internal\ for the three) # Consumed by: packaging/win/build-node-runtime.ps1, which moves the tree to # packages/meshbay-client/node-runtime/ for electron-builder to carry as an # extraResource. # # The dependency set (aiortc, av, aioquic, pydantic-core, uvicorn, watchdog, # guessit, blake3, ...) has C/Rust extensions and does dynamic imports, so # each awkward package is pulled in whole with collect_all rather than left to # PyInstaller's static analysis. Add to COLLECT_ALL / HIDDEN / COPY_META when a # frozen run raises ModuleNotFoundError — that is the expected way this list grows. from PyInstaller.utils.hooks import collect_all, copy_metadata from PyInstaller.utils.win32.versioninfo import ( FixedFileInfo, StringFileInfo, StringStruct, StringTable, VarFileInfo, VarStruct, VSVersionInfo, ) # Version resource stamped into meshbay-node.exe so the Windows Firewall prompt, # Task Manager and the file's Properties read "MeshBay Node" rather than a bare # exe name. Read from the installed package so it cannot drift from pyproject. try: from importlib.metadata import version as _pkg_version _ver = _pkg_version("meshbay-node") except Exception: _ver = "0.0.0" _vtuple = tuple( int(p) for p in (_ver.split("+")[0].split(".") + ["0", "0", "0", "0"])[:4] ) def _version_resource(name: str, description: str) -> VSVersionInfo: return VSVersionInfo( ffi=FixedFileInfo(filevers=_vtuple, prodvers=_vtuple), kids=[ StringFileInfo([StringTable("040904B0", [ StringStruct("CompanyName", "MeshBay"), StringStruct("FileDescription", description), StringStruct("FileVersion", _ver), StringStruct("InternalName", name), StringStruct("OriginalFilename", f"{name}.exe"), StringStruct("ProductName", description), StringStruct("ProductVersion", _ver), ])]), VarFileInfo([VarStruct("Translation", [0x0409, 1200])]), ], ) _version_info = _version_resource("meshbay-node", "MeshBay Node") # Packages taken in full: data files + shared libs + every submodule. COLLECT_ALL = [ "meshbay_node", "meshbay_common", "aiortc", "aioice", "aioquic", "av", "cryptography", "pylibsrtp", "pyee", "google_crc32c", "pydantic", "pydantic_core", "fastapi", "starlette", "uvicorn", "anyio", "sniffio", "h11", "websockets", "httpx", "httpcore", "watchdog", "guessit", "rebulk", "babelfish", "mutagen", "PIL", "blake3", "msgpack", "aiosqlite", "jwt", "dns", "tzdata", ] # importlib.metadata lookups that survive freezing only if the dist-info is copied. COPY_META = [ "meshbay-node", "meshbay-common", "guessit", "rebulk", "babelfish", "uvicorn", "fastapi", ] # Imports PyInstaller's static pass misses even with collect_all. HIDDEN = [ "uvicorn.logging", "uvicorn.loops.auto", "uvicorn.loops.asyncio", "uvicorn.protocols.http.auto", "uvicorn.protocols.http.h11_impl", "uvicorn.protocols.websockets.auto", "uvicorn.protocols.websockets.websockets_impl", "uvicorn.lifespan.on", ] datas, binaries, hiddenimports = [], [], list(HIDDEN) for pkg in COLLECT_ALL: d, b, h = collect_all(pkg) datas += d binaries += b hiddenimports += h for dist in COPY_META: datas += copy_metadata(dist) a = Analysis( ["node-entry.py"], pathex=[], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=["tkinter", "pytest", "ruff", "piexif", "IPython"], noarchive=False, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name="meshbay-node", debug=False, bootloader_ignore_signals=False, strip=False, upx=False, console=True, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon="../../packages/meshbay-client/build/icon.ico", version=_version_info, ) # The same daemon without a console, for what starts it with nobody watching: # the Store package's startup task runs its executable as is, and a console # executable opened a window whose close button killed the node. The CLI stays # meshbay-node.exe, which has to print. Same entry point and archive, same # _internal\ beside both (pythonw.exe beside python.exe). exe_windowless = EXE( pyz, a.scripts, [], exclude_binaries=True, name="meshbay-nodew", debug=False, bootloader_ignore_signals=False, strip=False, upx=False, console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon="../../packages/meshbay-client/build/icon.ico", version=_version_info, ) # --- The command-line client --------------------------------------------------- # Its own analysis: another entry point, and packages the daemon does not use # (Textual loads its widgets lazily, Rich its Unicode tables by version, # Pygments its lexers by name, keyring its backends through entry points — # hence keyring's metadata). What both import (aiortc, av, cryptography, ...) # lands once in _internal\: COLLECT keeps one copy of a file the two share. CLI_COLLECT_ALL = [ "meshbay_cli", "meshbay_common", "aiortc", "aioice", "av", "cryptography", "pylibsrtp", "pyee", "google_crc32c", "httpx", "httpcore", "h11", "anyio", "dns", "argon2", "_argon2_cffi_bindings", "keyring", "win32ctypes", "textual", "rich", "pygments", "markdown_it", "mdit_py_plugins", "linkify_it", "platformdirs", "blake3", "msgpack", "jwt", "tzdata", ] CLI_COPY_META = ["meshbay-cli", "meshbay-common", "keyring", "textual"] cli_datas, cli_binaries, cli_hidden = [], [], [] for pkg in CLI_COLLECT_ALL: d, b, h = collect_all(pkg) cli_datas += d cli_binaries += b cli_hidden += h for dist in CLI_COPY_META: cli_datas += copy_metadata(dist) cli_a = Analysis( ["cli-entry.py"], pathex=[], binaries=cli_binaries, datas=cli_datas, hiddenimports=cli_hidden, hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=["tkinter", "pytest", "ruff", "IPython", "meshbay_node", "meshbay_hub"], noarchive=False, ) cli_pyz = PYZ(cli_a.pure) exe_cli = EXE( cli_pyz, cli_a.scripts, [], exclude_binaries=True, name="meshbay-cli", debug=False, bootloader_ignore_signals=False, strip=False, upx=False, console=True, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon="../../packages/meshbay-client/build/icon.ico", version=_version_resource("meshbay-cli", "MeshBay command-line client"), ) coll = COLLECT( exe, exe_windowless, exe_cli, a.binaries, a.datas, cli_a.binaries, cli_a.datas, strip=False, upx=False, upx_exclude=[], name="meshbay-node", )