Add bundled open-webui
Use pyinstaller to compile to a single bundle
This commit is contained in:
parent
3392496493
commit
3218c10063
9
.gitignore
vendored
9
.gitignore
vendored
@ -8,3 +8,12 @@ node_modules
|
||||
!.env.example
|
||||
vite.config.js.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
1
backend/.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.12
|
174
backend/BUILD.md
Normal file
174
backend/BUILD.md
Normal 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
60
backend/Makefile
Normal 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
0
backend/README.md
Normal file
180
backend/build.py
Executable file
180
backend/build.py
Executable 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()
|
125
backend/glowpath-backend.spec
Normal file
125
backend/glowpath-backend.spec
Normal 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,
|
||||
)
|
33
backend/hooks/hook-open_webui.py
Normal file
33
backend/hooks/hook-open_webui.py
Normal 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
5
backend/main.py
Normal file
@ -0,0 +1,5 @@
|
||||
from open_webui import app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
15
backend/pyproject.toml
Normal file
15
backend/pyproject.toml
Normal 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
6191
backend/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user