Add bundled open-webui

Use pyinstaller to compile to a single bundle
This commit is contained in:
I. A. Naval 2025-06-28 15:09:35 -04:00
parent 3392496493
commit 3218c10063
Signed by: potato
GPG Key ID: D22B0F9008C43F2B
11 changed files with 6793 additions and 0 deletions

9
.gitignore vendored
View File

@ -8,3 +8,12 @@ node_modules
!.env.example !.env.example
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
backend/.webui_secret_key
.venv
backend/build
backend/dist
__pycache__
*.pyc
*.pyo
*.pyd
*.pkl

1
backend/.python-version Normal file
View File

@ -0,0 +1 @@
3.12

174
backend/BUILD.md Normal file
View File

@ -0,0 +1,174 @@
# Building GlowPath Backend with PyInstaller
This directory contains everything needed to bundle the GlowPath backend into a standalone executable using PyInstaller.
## Quick Start
The easiest way to build the application is using the provided build script:
```bash
# Install build dependencies and create the executable
python build.py
```
Or using make:
```bash
make build
```
## Build Requirements
- Python 3.12+
- PyInstaller 6.0+
- All dependencies from `pyproject.toml`
## Build Methods
### Method 1: Using the Build Script (Recommended)
The `build.py` script handles everything automatically:
```bash
# Standard build
python build.py
# Debug build (includes debug symbols and verbose output)
python build.py --debug
# Skip dependency installation
python build.py --no-deps
# Skip cleaning previous builds
python build.py --no-clean
```
### Method 2: Using Make
```bash
# Standard build
make build
# Debug build
make build-debug
# Quick build (no cleaning)
make quick-build
# Just clean
make clean
```
### Method 3: Manual PyInstaller
If you prefer to run PyInstaller directly:
```bash
# Install dependencies first
pip install pyinstaller pyinstaller-hooks-contrib
# Run PyInstaller
pyinstaller --clean --noconfirm glowpath-backend.spec
```
## Output
After a successful build, you'll find:
- `dist/glowpath-backend` - The standalone executable
- `dist/glowpath-backend-bundle/` - A complete distribution package with:
- The executable
- Startup scripts (`start.sh` or `start.bat`)
- README.txt with usage instructions
## Configuration
### PyInstaller Spec File
The `glowpath-backend.spec` file contains the PyInstaller configuration. It's configured to:
- Include all open-webui dependencies and data files
- Handle complex packages like bcrypt, cryptography, etc.
- Include SSL certificates
- Exclude unnecessary packages (tkinter, matplotlib, etc.)
- Create a single-file executable
### Custom Hooks
The `hooks/` directory contains PyInstaller hooks for better package detection:
- `hook-open_webui.py` - Ensures all open-webui files are included
## Troubleshooting
### Common Issues
1. **Missing modules**: If you get import errors, add the missing module to `hiddenimports` in the spec file.
2. **Missing data files**: If templates or static files are missing, they may need to be added to the `datas` list.
3. **SSL certificate errors**: The spec file includes SSL certificates, but you may need to adjust the path based on your system.
4. **Large executable size**: The executable includes all dependencies. You can reduce size by:
- Adding more packages to the `excludes` list
- Using `--onedir` instead of `--onefile` (modify the spec file)
### Debug Mode
Build in debug mode to get more information about what's being included:
```bash
python build.py --debug
```
This will:
- Enable PyInstaller debug output
- Include debug symbols
- Show detailed import information
### Testing the Build
After building, test the executable:
```bash
# Run the executable directly
./dist/glowpath-backend
# Or use the bundle
cd dist/glowpath-backend-bundle
./start.sh
```
## Customization
### Adding Dependencies
To include additional Python packages:
1. Add them to the `additional_packages` list in `glowpath-backend.spec`
2. If they have hidden imports, add them to the `hiddenimports` list
### Changing the Executable Name
Modify the `name` parameter in the `EXE` section of the spec file.
### Creating a GUI Application
Change `console=True` to `console=False` in the `EXE` section for a GUI app (no console window).
## Performance Notes
- First build will be slower as PyInstaller analyzes all dependencies
- Subsequent builds with `--no-clean` will be faster
- The executable may take a moment to start as it unpacks dependencies
## Distribution
The bundled application is self-contained and can be distributed to machines without Python installed. The bundle includes:
- All Python dependencies
- SSL certificates
- Static files and templates
- Configuration files
Simply copy the entire `dist/glowpath-backend-bundle/` directory to the target machine.

60
backend/Makefile Normal file
View File

@ -0,0 +1,60 @@
# Makefile for GlowPath Backend
.PHONY: clean build build-debug install-deps install-project-deps install-all-deps dist help
# Default target
all: build
# Install project dependencies
install-project-deps:
@echo "Installing project dependencies..."
@uv sync
# Install build dependencies
install-deps:
@echo "Installing build dependencies..."
@uv sync --group dev
# Install all dependencies (project + build)
install-all-deps:
@echo "Installing all dependencies..."
@uv sync --all-groups
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
@rm -rf build dist __pycache__ *.pyc
@find . -name "*.pyc" -delete
@find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
# Build the application
build: install-all-deps clean
@echo "Building GlowPath backend..."
@uv run python build.py
# Build in debug mode
build-debug: install-all-deps clean
@echo "Building GlowPath backend in debug mode..."
@uv run python build.py --debug
# Quick build without cleaning
quick-build:
@echo "Quick building GlowPath backend..."
@uv run python build.py --no-clean
# Create distribution package
dist: build
@echo "Distribution package created in dist/ directory"
# Show help
help:
@echo "Available targets:"
@echo " build - Install all deps, clean and build the application"
@echo " build-debug - Build in debug mode"
@echo " quick-build - Build without cleaning"
@echo " clean - Clean build artifacts"
@echo " install-deps - Install build dependencies (PyInstaller)"
@echo " install-project-deps - Install project dependencies"
@echo " install-all-deps - Install all dependencies (project + build)"
@echo " dist - Create distribution package"
@echo " help - Show this help message"

0
backend/README.md Normal file
View File

180
backend/build.py Executable file
View File

@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
Build script for bundling GlowPath backend with PyInstaller.
This script handles the entire build process including dependency installation and cleanup.
"""
import os
import sys
import subprocess
import shutil
import argparse
from pathlib import Path
def run_command(cmd, cwd=None, check=True):
"""Run a command and return the result."""
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=cwd, check=check, capture_output=True, text=True)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
return result
def clean_build():
"""Clean previous build artifacts."""
print("Cleaning previous build artifacts...")
dirs_to_clean = ["build", "dist", "__pycache__"]
files_to_clean = ["*.pyc"]
for dir_name in dirs_to_clean:
if os.path.exists(dir_name):
print(f"Removing {dir_name}")
shutil.rmtree(dir_name)
# Clean .pyc files
for root, dirs, files in os.walk("."):
for file in files:
if file.endswith(".pyc"):
os.remove(os.path.join(root, file))
def install_dependencies():
"""Install build dependencies."""
print("Installing build dependencies...")
try:
# Try uv first (faster) - sync dev dependencies
run_command(["uv", "sync", "--group", "dev"])
except (subprocess.CalledProcessError, FileNotFoundError):
# Fallback to pip
run_command(
[
sys.executable,
"-m",
"pip",
"install",
"pyinstaller",
"pyinstaller-hooks-contrib",
]
)
def build_executable(debug=False):
"""Build the executable using PyInstaller."""
print("Building executable with PyInstaller...")
cmd = ["pyinstaller"]
if debug:
cmd.extend(["--debug", "all"])
cmd.extend(["--clean", "--noconfirm", "glowpath-backend.spec"])
run_command(cmd)
def create_distribution():
"""Create a distribution package."""
print("Creating distribution package...")
dist_dir = Path("dist")
if not dist_dir.exists():
print("No dist directory found. Build may have failed.")
return False
# Create a distribution folder with all necessary files
app_name = "glowpath-backend"
bundle_dir = dist_dir / f"{app_name}-bundle"
if bundle_dir.exists():
shutil.rmtree(bundle_dir)
bundle_dir.mkdir()
# Copy the executable
exe_path = dist_dir / app_name
if exe_path.exists():
if sys.platform == "win32":
exe_path = exe_path.with_suffix(".exe")
shutil.copy2(exe_path, bundle_dir)
# Create a simple startup script
if sys.platform != "win32":
startup_script = bundle_dir / "start.sh"
startup_script.write_text(
f"""#!/bin/bash
cd "$(dirname "$0")"
./{app_name} "$@"
"""
)
startup_script.chmod(0o755)
else:
startup_script = bundle_dir / "start.bat"
startup_script.write_text(
f"""@echo off
cd /d "%~dp0"
{app_name}.exe %*
"""
)
# Create README
readme = bundle_dir / "README.txt"
readme.write_text(
f"""GlowPath Backend
===============
This is a bundled version of the GlowPath backend application.
To run the application:
- On Unix/Linux/macOS: ./start.sh
- On Windows: start.bat
- Or run the executable directly: ./{app_name}{"" if sys.platform != "win32" else ".exe"}
The application will start a web server. Check the console output for the URL to access the interface.
"""
)
print(f"Distribution package created in: {bundle_dir}")
return True
def main():
parser = argparse.ArgumentParser(description="Build GlowPath backend executable")
parser.add_argument("--debug", action="store_true", help="Build in debug mode")
parser.add_argument(
"--no-clean", action="store_true", help="Skip cleaning build artifacts"
)
parser.add_argument(
"--no-deps", action="store_true", help="Skip installing dependencies"
)
args = parser.parse_args()
try:
if not args.no_clean:
clean_build()
if not args.no_deps:
install_dependencies()
build_executable(debug=args.debug)
if create_distribution():
print("\n✅ Build completed successfully!")
print(f"Executable and bundle available in the 'dist' directory")
else:
print("\n❌ Build failed!")
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"\n❌ Build failed with error: {e}")
sys.exit(1)
except KeyboardInterrupt:
print("\n⚠️ Build interrupted by user")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,125 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all, collect_data_files, collect_submodules
import os
import sys
# Collect all open-webui files and dependencies
datas = []
binaries = []
hiddenimports = []
# Collect open-webui and its dependencies
tmp_ret = collect_all('open_webui')
datas += tmp_ret[0]
binaries += tmp_ret[1]
hiddenimports += tmp_ret[2]
# Collect additional packages that might have data files or hidden imports
additional_packages = [
'bcrypt',
'cryptography',
'passlib',
'fastapi',
'uvicorn',
'pydantic',
'sqlalchemy',
'alembic',
'jinja2',
'aiofiles',
'python_multipart',
'httpx',
'requests',
'websockets',
'sse_starlette',
'chromadb',
]
for package in additional_packages:
try:
tmp_ret = collect_all(package)
datas += tmp_ret[0]
binaries += tmp_ret[1]
hiddenimports += tmp_ret[2]
except ImportError:
print(f"Warning: Package {package} not found, skipping...")
# Additional hidden imports that might be needed
hiddenimports += [
'uvicorn.main',
'uvicorn.protocols.http.auto',
'uvicorn.protocols.websockets.auto',
'uvicorn.lifespan.on',
'uvicorn.loops.auto',
'uvicorn.logging',
'fastapi.middleware.cors',
'fastapi.responses',
'passlib.handlers.bcrypt',
'passlib.handlers.pbkdf2',
'cryptography.hazmat.backends.openssl',
'sqlalchemy.dialects.sqlite',
'sqlalchemy.pool',
'alembic.runtime.migration',
'email_validator',
'chromadb.utils.embedding_functions.onnx_mini_lm_l6_v2',
'chromadb.utils.embedding_functions',
'chromadb.config',
'chromadb.db',
'chromadb.db.impl',
'onnxruntime',
]
# Include SSL certificates
import ssl
ssl_paths = ssl.get_default_verify_paths()
if ssl_paths.cafile:
datas.append((ssl_paths.cafile, 'certifi'))
a = Analysis(
['main.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=['hooks'],
hooksconfig={},
runtime_hooks=[],
excludes=[
'tkinter',
'matplotlib',
'IPython',
'jupyter',
'notebook',
'test',
'tests',
],
noarchive=False,
optimize=0,
)
# Remove duplicate entries
a.datas = list(set(a.datas))
a.binaries = list(set(a.binaries))
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='glowpath-backend',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=None,
)

View File

@ -0,0 +1,33 @@
"""
PyInstaller hook for open-webui package.
This hook ensures all necessary files and modules are included in the bundle.
"""
from PyInstaller.utils.hooks import (
collect_data_files,
collect_submodules,
collect_dynamic_libs,
)
# Collect all submodules
hiddenimports = collect_submodules("open_webui")
# Collect data files (templates, static files, etc.)
datas = collect_data_files("open_webui")
# Collect any dynamic libraries
binaries = collect_dynamic_libs("open_webui")
# Add specific hidden imports that might be missed
hiddenimports += [
"open_webui.main",
"open_webui.apps",
"open_webui.config",
"open_webui.constants",
"open_webui.utils",
"open_webui.models",
"uvicorn.protocols.http.auto",
"uvicorn.protocols.websockets.auto",
"uvicorn.lifespan.on",
"uvicorn.loops.auto",
]

5
backend/main.py Normal file
View File

@ -0,0 +1,5 @@
from open_webui import app
if __name__ == "__main__":
app()

15
backend/pyproject.toml Normal file
View File

@ -0,0 +1,15 @@
[project]
name = "glowpath-backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"open-webui>=0.6.5",
]
[dependency-groups]
dev = [
"pyinstaller>=6.14.1",
"pyinstaller-hooks-contrib>=2025.5",
]

6191
backend/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff