136 lines
5.0 KiB
RPMSpec
136 lines
5.0 KiB
RPMSpec
# PyInstaller spec for Dev Assist Chatbot.
|
|
#
|
|
# Build (from the repo root, in an environment with the CUDA 13.x toolkit
|
|
# installed - the toolkit is only needed at BUILD time, to copy its runtime
|
|
# DLLs into the bundle; end users only need an NVIDIA driver):
|
|
#
|
|
# uv run pyinstaller packaging/build.spec --noconfirm
|
|
#
|
|
# Output lands in dist/DevAssistChatbot/. That whole folder is the app -
|
|
# onedir rather than onefile, since unpacking a multi-GB bundle to a temp
|
|
# dir on every launch would make startup unusable.
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from PyInstaller.utils.hooks import collect_submodules
|
|
|
|
PROJECT_ROOT = Path(SPECPATH).parent # noqa: F821 - injected by PyInstaller
|
|
VENV_SITE_PACKAGES = PROJECT_ROOT / ".venv" / "Lib" / "site-packages"
|
|
LLAMA_CPP_LIB = VENV_SITE_PACKAGES / "llama_cpp" / "lib"
|
|
|
|
if not LLAMA_CPP_LIB.is_dir():
|
|
raise SystemExit(
|
|
f"Expected llama_cpp's compiled libs at {LLAMA_CPP_LIB} - run this via "
|
|
"'uv run pyinstaller ...' from the repo root so the project venv is used."
|
|
)
|
|
|
|
CUDA_PATH = os.environ.get("CUDA_PATH")
|
|
if not CUDA_PATH:
|
|
raise SystemExit(
|
|
"CUDA_PATH is not set. Building requires the CUDA toolkit that "
|
|
"llama-cpp-python was compiled against, so its runtime DLLs can be "
|
|
"copied into the bundle (end users won't need the toolkit installed)."
|
|
)
|
|
CUDA_BIN = Path(CUDA_PATH) / "bin" / "x64"
|
|
|
|
# llama.cpp's ggml backend (ggml-cuda.dll etc.) is loaded dynamically at
|
|
# runtime, not linked at import time, so PyInstaller's static analysis can't
|
|
# discover it - every DLL in llama_cpp/lib/ has to be added explicitly. They
|
|
# all go into the same output folder so Windows' DLL search (which checks a
|
|
# loaded DLL's own directory for its dependencies) can resolve cublas from
|
|
# next to ggml-cuda.dll.
|
|
CUDA_RUNTIME_DLLS = [
|
|
"cudart64_13.dll",
|
|
"cublas64_13.dll",
|
|
"cublasLt64_13.dll",
|
|
"nvrtc64_130_0.dll",
|
|
"nvrtc-builtins64_134.dll",
|
|
]
|
|
|
|
binaries = [(str(dll), "llama_cpp/lib") for dll in LLAMA_CPP_LIB.glob("*.dll")]
|
|
for name in CUDA_RUNTIME_DLLS:
|
|
dll_path = CUDA_BIN / name
|
|
if not dll_path.is_file():
|
|
raise SystemExit(f"Expected CUDA runtime DLL not found: {dll_path}")
|
|
binaries.append((str(dll_path), "llama_cpp/lib"))
|
|
|
|
# ddgs picks its search backend (duckduckgo, bing, ...) via importlib at
|
|
# runtime rather than a static import, so PyInstaller won't find those
|
|
# engine modules on its own either.
|
|
hidden_imports = collect_submodules("ddgs") + ["pynvml"]
|
|
|
|
a = Analysis( # noqa: F821
|
|
[str(PROJECT_ROOT / "main.py")],
|
|
pathex=[str(PROJECT_ROOT)],
|
|
binaries=binaries,
|
|
datas=[],
|
|
hiddenimports=hidden_imports,
|
|
hookspath=[],
|
|
excludes=[],
|
|
noarchive=False,
|
|
)
|
|
|
|
# PyInstaller's own dependency walker separately discovers cublas64_13.dll
|
|
# and cublasLt64_13.dll (ggml-cuda.dll imports them directly) and stages its
|
|
# own copy at the bundle root, on top of the copy the `binaries` list above
|
|
# already places in llama_cpp/lib/ (which is where they need to be, next to
|
|
# ggml-cuda.dll, for ITS dependency lookup to find them - see the comment
|
|
# above). The root copy is pure duplication: nothing loads cublas from
|
|
# there. Drop it - it's ~547MB.
|
|
_DUPLICATED_AT_ROOT = {"cublas64_13.dll", "cublasLt64_13.dll"}
|
|
a.binaries = [
|
|
entry
|
|
for entry in a.binaries
|
|
if not (Path(entry[0]).name in _DUPLICATED_AT_ROOT and Path(entry[0]).parent == Path("."))
|
|
]
|
|
|
|
# Qt/PyQt6 features this app doesn't use, bundled defensively by PyQt6's
|
|
# PyInstaller hook. Confirmed safe to drop: Qt6Widgets/Qt6Gui/Qt6Core don't
|
|
# link any of these at the DLL level (only load them on demand), no
|
|
# PyQt6.QtNetwork/QtSvg/QtPdf Python module is bundled or imported anywhere
|
|
# in this app, no QTranslator is ever installed, and app.setStyle("Fusion")
|
|
# in gui.py forces a style that's built into Qt6Widgets itself rather than
|
|
# loaded from the styles/ plugin.
|
|
_UNUSED_QT_BASENAMES = {
|
|
"opengl32sw.dll", # software OpenGL fallback; no QOpenGLWidget/3D used
|
|
"qt6pdf.dll",
|
|
"qt6network.dll",
|
|
"qt6svg.dll",
|
|
"qsvg.dll", # imageformats SVG plugin
|
|
"qsvgicon.dll", # iconengines SVG plugin
|
|
"qpdf.dll", # imageformats PDF plugin (depends on the Qt6Pdf.dll dropped above)
|
|
"qmodernwindowsstyle.dll", # styles plugin; app always forces Fusion
|
|
}
|
|
|
|
|
|
def _is_unused_qt_binary(dest: str) -> bool:
|
|
return Path(dest).name.lower() in _UNUSED_QT_BASENAMES
|
|
|
|
|
|
a.binaries = [entry for entry in a.binaries if not _is_unused_qt_binary(entry[0])]
|
|
# Qt's .qm translation files ship as data, not binaries - this app never
|
|
# installs a QTranslator, so none of the 96 bundled languages are ever used.
|
|
a.datas = [entry for entry in a.datas if not str(entry[0]).lower().endswith(".qm")]
|
|
|
|
pyz = PYZ(a.pure) # noqa: F821
|
|
|
|
exe = EXE( # noqa: F821
|
|
pyz,
|
|
a.scripts,
|
|
[],
|
|
exclude_binaries=True,
|
|
name="DevAssistChatbot",
|
|
console=False,
|
|
)
|
|
|
|
coll = COLLECT( # noqa: F821
|
|
exe,
|
|
a.binaries,
|
|
a.zipfiles,
|
|
a.datas,
|
|
# UPX would try (slowly, and sometimes badly) to compress the ~650MB of
|
|
# CUDA runtime DLLs; skip it.
|
|
upx=False,
|
|
name="DevAssistChatbot",
|
|
)
|