Add PyInstaller Windows release build system.
Frozen builds resolve paths correctly, stage portable onedir output, and zip releases for distribution.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -16,5 +16,11 @@ valid_proxies.txt
|
|||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
|
|
||||||
|
# PyInstaller / release artifacts
|
||||||
|
dist/
|
||||||
|
release/
|
||||||
|
build/pyinstaller/
|
||||||
|
GODCWAK.spec
|
||||||
|
|
||||||
Screenshot *.png
|
Screenshot *.png
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -67,6 +67,45 @@ You can also launch directly:
|
|||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Build Windows Release (PyInstaller)
|
||||||
|
|
||||||
|
Production builds produce a portable **onedir** folder under `dist/GODCWAK/` and a zip under `release/`.
|
||||||
|
|
||||||
|
From PowerShell in the project directory:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# One-time: activate venv and install build deps
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
pip install -r requirements-build.txt
|
||||||
|
|
||||||
|
# Build release (no console window)
|
||||||
|
python build\build_release.py --clean
|
||||||
|
|
||||||
|
# Troubleshooting build (shows console + logs)
|
||||||
|
python build\build_release.py --clean --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Or double-click `build_release.bat` at the project root.
|
||||||
|
|
||||||
|
**Output layout**
|
||||||
|
|
||||||
|
```text
|
||||||
|
dist/GODCWAK/
|
||||||
|
GODCWAK.exe Main application
|
||||||
|
Start GODCWAK.bat Optional launcher
|
||||||
|
config_ollama.json Writable Ollama settings (seeded on first run)
|
||||||
|
data/ Passwords, proxies, attempt logs (created at runtime)
|
||||||
|
_internal/ Bundled Python runtime and dependencies
|
||||||
|
release/
|
||||||
|
GODCWAK-v1.0-win64-YYYYMMDD.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes**
|
||||||
|
|
||||||
|
- Default release excludes CuPy/Numba/Playwright to keep the bundle smaller; use `--gpu` if CUDA libraries are installed and you want them bundled.
|
||||||
|
- Place `assets/icon.ico` before building to embed a custom application icon.
|
||||||
|
- Packaged builds log to `data/godcwak.log` and show error dialogs instead of a console window.
|
||||||
|
|
||||||
## GPU Notes
|
## GPU Notes
|
||||||
|
|
||||||
GODCWAK checks for CuPy at startup. With `cupy-cuda12x` installed and a working NVIDIA CUDA-capable GPU, password-combination index generation can use the GPU for larger workloads. Without CUDA, the app falls back to NumPy CPU acceleration or pure Python.
|
GODCWAK checks for CuPy at startup. With `cupy-cuda12x` installed and a working NVIDIA CUDA-capable GPU, password-combination index generation can use the GPU for larger workloads. Without CUDA, the app falls back to NumPy CPU acceleration or pure Python.
|
||||||
|
|||||||
322
build/build_release.py
Normal file
322
build/build_release.py
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Production Windows release builder for GODCWAK (PyInstaller onedir).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python build/build_release.py
|
||||||
|
python build/build_release.py --debug # console window for troubleshooting
|
||||||
|
python build/build_release.py --gpu # bundle CuPy/Numba if installed
|
||||||
|
python build/build_release.py --clean --no-zip
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import zipfile
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
BUILD_DIR = ROOT / "build"
|
||||||
|
DIST_DIR = ROOT / "dist"
|
||||||
|
RELEASE_DIR = ROOT / "release"
|
||||||
|
SPEC_PATH = ROOT / "GODCWAK.spec"
|
||||||
|
APP_NAME = "GODCWAK"
|
||||||
|
APP_VERSION = "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _read_app_version() -> str:
|
||||||
|
config = ROOT / "config.py"
|
||||||
|
if not config.is_file():
|
||||||
|
return APP_VERSION
|
||||||
|
for line in config.read_text(encoding="utf-8").splitlines():
|
||||||
|
if line.strip().startswith("APP_VERSION"):
|
||||||
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||||
|
return APP_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
def _python_exe() -> str:
|
||||||
|
return sys.executable
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_pyinstaller() -> None:
|
||||||
|
try:
|
||||||
|
import PyInstaller # noqa: F401
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SystemExit(
|
||||||
|
"PyInstaller is not installed. Run:\n"
|
||||||
|
f" {_python_exe()} -m pip install -r requirements-build.txt"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _write_spec(*, console: bool, include_gpu: bool, icon: Path | None) -> None:
|
||||||
|
version_file = BUILD_DIR / "version_info.txt"
|
||||||
|
exe_kwargs = []
|
||||||
|
if version_file.is_file():
|
||||||
|
exe_kwargs.append("version=str(ROOT / 'build' / 'version_info.txt'),")
|
||||||
|
if icon:
|
||||||
|
exe_kwargs.append("icon=str(ROOT / 'assets' / 'icon.ico'),")
|
||||||
|
exe_extra = "\n ".join(exe_kwargs)
|
||||||
|
|
||||||
|
gpu_block = ""
|
||||||
|
if not include_gpu:
|
||||||
|
gpu_block = """
|
||||||
|
excludes += [
|
||||||
|
"cupy",
|
||||||
|
"cupy_backends",
|
||||||
|
"numba",
|
||||||
|
"llvmlite",
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"# -*- mode: python ; coding: utf-8 -*-",
|
||||||
|
"# Generated by build/build_release.py — do not edit by hand.",
|
||||||
|
"from pathlib import Path",
|
||||||
|
"",
|
||||||
|
"from PyInstaller.utils.hooks import collect_all, collect_submodules",
|
||||||
|
"",
|
||||||
|
"block_cipher = None",
|
||||||
|
f'ROOT = Path(r"{ROOT.as_posix()}")',
|
||||||
|
"",
|
||||||
|
'ctk_datas, ctk_binaries, ctk_hidden = collect_all("customtkinter")',
|
||||||
|
'dark_datas, dark_binaries, dark_hidden = collect_all("darkdetect")',
|
||||||
|
"",
|
||||||
|
"datas = [",
|
||||||
|
' (str(ROOT / "config_ollama.json"), "."),',
|
||||||
|
"]",
|
||||||
|
"assets = ROOT / \"assets\"",
|
||||||
|
"if assets.is_dir():",
|
||||||
|
' datas.append((str(assets), "assets"))',
|
||||||
|
"",
|
||||||
|
"datas += ctk_datas + dark_datas",
|
||||||
|
"binaries = ctk_binaries + dark_binaries",
|
||||||
|
"",
|
||||||
|
"hiddenimports = [",
|
||||||
|
' "PIL._tkinter_finder",',
|
||||||
|
' "PIL.Image",',
|
||||||
|
' "PIL.ImageTk",',
|
||||||
|
' "aiohttp",',
|
||||||
|
' "aiohttp_socks",',
|
||||||
|
' "multidict",',
|
||||||
|
' "yarl",',
|
||||||
|
' "frozenlist",',
|
||||||
|
' "aiosignal",',
|
||||||
|
' "async_timeout",',
|
||||||
|
' "charset_normalizer",',
|
||||||
|
' "certifi",',
|
||||||
|
' "socks",',
|
||||||
|
' "urllib3",',
|
||||||
|
' "requests",',
|
||||||
|
' "numpy",',
|
||||||
|
' "darkdetect",',
|
||||||
|
' "config",',
|
||||||
|
' "main",',
|
||||||
|
"]",
|
||||||
|
"hiddenimports += ctk_hidden + dark_hidden",
|
||||||
|
'hiddenimports += collect_submodules("src")',
|
||||||
|
"",
|
||||||
|
"excludes = [",
|
||||||
|
' "playwright",',
|
||||||
|
' "matplotlib",',
|
||||||
|
' "scipy",',
|
||||||
|
' "pandas",',
|
||||||
|
' "pytest",',
|
||||||
|
' "IPython",',
|
||||||
|
' "jupyter",',
|
||||||
|
' "tkinter.test",',
|
||||||
|
"]",
|
||||||
|
gpu_block.rstrip(),
|
||||||
|
"",
|
||||||
|
"a = Analysis(",
|
||||||
|
' [str(ROOT / "main.py")],',
|
||||||
|
" pathex=[str(ROOT)],",
|
||||||
|
" binaries=binaries,",
|
||||||
|
" datas=datas,",
|
||||||
|
" hiddenimports=hiddenimports,",
|
||||||
|
' hookspath=[str(ROOT / "build" / "hooks")],',
|
||||||
|
" hooksconfig={},",
|
||||||
|
' runtime_hooks=[str(ROOT / "build" / "pyi_rth_godcwak.py")],',
|
||||||
|
" excludes=excludes,",
|
||||||
|
" win_no_prefer_redirects=False,",
|
||||||
|
" win_private_assemblies=False,",
|
||||||
|
" cipher=block_cipher,",
|
||||||
|
" noarchive=False,",
|
||||||
|
")",
|
||||||
|
"",
|
||||||
|
"pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)",
|
||||||
|
"",
|
||||||
|
"exe = EXE(",
|
||||||
|
" pyz,",
|
||||||
|
" a.scripts,",
|
||||||
|
" [],",
|
||||||
|
" exclude_binaries=True,",
|
||||||
|
f' name="{APP_NAME}",',
|
||||||
|
" debug=False,",
|
||||||
|
" bootloader_ignore_signals=False,",
|
||||||
|
" strip=False,",
|
||||||
|
" upx=True,",
|
||||||
|
f" console={console},",
|
||||||
|
" disable_windowed_traceback=False,",
|
||||||
|
" argv_emulation=False,",
|
||||||
|
" target_arch=None,",
|
||||||
|
" codesign_identity=None,",
|
||||||
|
" entitlements_file=None,",
|
||||||
|
]
|
||||||
|
if exe_extra:
|
||||||
|
lines.append(f" {exe_extra}")
|
||||||
|
lines.extend([
|
||||||
|
")",
|
||||||
|
"",
|
||||||
|
"coll = COLLECT(",
|
||||||
|
" exe,",
|
||||||
|
" a.binaries,",
|
||||||
|
" a.zipfiles,",
|
||||||
|
" a.datas,",
|
||||||
|
" strip=False,",
|
||||||
|
" upx=True,",
|
||||||
|
" upx_exclude=[],",
|
||||||
|
f' name="{APP_NAME}",',
|
||||||
|
")",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
SPEC_PATH.write_text("\n".join(lines), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_pyinstaller(*, clean: bool) -> None:
|
||||||
|
cmd = [
|
||||||
|
_python_exe(),
|
||||||
|
"-m",
|
||||||
|
"PyInstaller",
|
||||||
|
"--noconfirm",
|
||||||
|
str(SPEC_PATH),
|
||||||
|
"--distpath",
|
||||||
|
str(DIST_DIR),
|
||||||
|
"--workpath",
|
||||||
|
str(BUILD_DIR / "pyinstaller"),
|
||||||
|
]
|
||||||
|
if clean:
|
||||||
|
cmd.insert(4, "--clean")
|
||||||
|
print("Running:", " ".join(cmd))
|
||||||
|
subprocess.run(cmd, cwd=str(ROOT), check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _stage_release(app_dir: Path, version: str, *, console: bool) -> None:
|
||||||
|
"""Copy user-facing files next to the exe (writable config/data live here)."""
|
||||||
|
app_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
config_dst = app_dir / "config_ollama.json"
|
||||||
|
if not config_dst.exists():
|
||||||
|
shutil.copy2(ROOT / "config_ollama.json", config_dst)
|
||||||
|
|
||||||
|
data_dir = app_dir / "data"
|
||||||
|
data_dir.mkdir(exist_ok=True)
|
||||||
|
readme = data_dir / "README.txt"
|
||||||
|
if not readme.exists():
|
||||||
|
readme.write_text(
|
||||||
|
"GODCWAK stores passwords, proxies, and attempt logs in this folder.\n"
|
||||||
|
"Keep this directory backed up and out of version control.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
launcher = app_dir / "Start GODCWAK.bat"
|
||||||
|
launcher.write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
@echo off
|
||||||
|
title {APP_NAME}
|
||||||
|
cd /d "%~dp0"
|
||||||
|
start "" "{APP_NAME}.exe"
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"app": APP_NAME,
|
||||||
|
"version": version,
|
||||||
|
"built_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"platform": "win64",
|
||||||
|
"python": sys.version.split()[0],
|
||||||
|
"console": console,
|
||||||
|
}
|
||||||
|
(app_dir / "build_manifest.json").write_text(
|
||||||
|
json.dumps(manifest, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_zip(app_dir: Path, version: str) -> Path:
|
||||||
|
RELEASE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
stamp = datetime.now(timezone.utc).strftime("%Y%m%d")
|
||||||
|
zip_path = RELEASE_DIR / f"{APP_NAME}-v{version}-win64-{stamp}.zip"
|
||||||
|
if zip_path.exists():
|
||||||
|
zip_path.unlink()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for path in sorted(app_dir.rglob("*")):
|
||||||
|
if path.is_file():
|
||||||
|
zf.write(path, path.relative_to(app_dir.parent).as_posix())
|
||||||
|
return zip_path
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Build GODCWAK Windows release with PyInstaller")
|
||||||
|
parser.add_argument(
|
||||||
|
"--debug",
|
||||||
|
action="store_true",
|
||||||
|
help="Build with a console window (for troubleshooting)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--gpu",
|
||||||
|
action="store_true",
|
||||||
|
help="Include CuPy/Numba in the bundle if installed (much larger build)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--clean",
|
||||||
|
action="store_true",
|
||||||
|
help="Clean PyInstaller cache before building",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-zip",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip creating release/*.zip archive",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = _parse_args()
|
||||||
|
version = _read_app_version()
|
||||||
|
icon = ROOT / "assets" / "icon.ico"
|
||||||
|
|
||||||
|
os.chdir(ROOT)
|
||||||
|
_ensure_pyinstaller()
|
||||||
|
|
||||||
|
print(f"Building {APP_NAME} v{version} (console={'yes' if args.debug else 'no'}, gpu={'yes' if args.gpu else 'no'})")
|
||||||
|
_write_spec(console=args.debug, include_gpu=args.gpu, icon=icon if icon.is_file() else None)
|
||||||
|
_run_pyinstaller(clean=args.clean)
|
||||||
|
|
||||||
|
app_dir = DIST_DIR / APP_NAME
|
||||||
|
if not (app_dir / f"{APP_NAME}.exe").is_file():
|
||||||
|
raise SystemExit(f"Build failed: missing {app_dir / f'{APP_NAME}.exe'}")
|
||||||
|
|
||||||
|
_stage_release(app_dir, version, console=args.debug)
|
||||||
|
|
||||||
|
print(f"\nBuild complete: {app_dir}")
|
||||||
|
print(f"Run: {app_dir / f'{APP_NAME}.exe'}")
|
||||||
|
|
||||||
|
if not args.no_zip:
|
||||||
|
zip_path = _create_zip(app_dir, version)
|
||||||
|
print(f"Release archive: {zip_path}")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
1
build/hooks/.gitkeep
Normal file
1
build/hooks/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# PyInstaller hook search path (optional custom hooks)
|
||||||
12
build/pyi_rth_godcwak.py
Normal file
12
build/pyi_rth_godcwak.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""
|
||||||
|
PyInstaller runtime hook — normalize frozen paths before application imports.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
os.environ.setdefault("GODCWAK_FROZEN", "1")
|
||||||
|
# Ensure the exe directory is on PATH for any subprocess helpers.
|
||||||
|
exe_dir = os.path.dirname(sys.executable)
|
||||||
|
if exe_dir and exe_dir not in os.environ.get("PATH", ""):
|
||||||
|
os.environ["PATH"] = exe_dir + os.pathsep + os.environ.get("PATH", "")
|
||||||
36
build/version_info.txt
Normal file
36
build/version_info.txt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# UTF-8
|
||||||
|
#
|
||||||
|
# Windows version resource for GODCWAK.exe
|
||||||
|
#
|
||||||
|
VSVersionInfo(
|
||||||
|
ffi=FixedFileInfo(
|
||||||
|
filevers=(1, 0, 0, 0),
|
||||||
|
prodvers=(1, 0, 0, 0),
|
||||||
|
mask=0x3f,
|
||||||
|
flags=0x0,
|
||||||
|
OS=0x40004,
|
||||||
|
fileType=0x1,
|
||||||
|
subtype=0x0,
|
||||||
|
date=(0, 0)
|
||||||
|
),
|
||||||
|
kids=[
|
||||||
|
StringFileInfo(
|
||||||
|
[
|
||||||
|
StringTable(
|
||||||
|
'040904B0',
|
||||||
|
[
|
||||||
|
StringStruct('CompanyName', 'GODCWAK'),
|
||||||
|
StringStruct('FileDescription', 'GODCWAK — GPU-accelerated recovery lab tool'),
|
||||||
|
StringStruct('FileVersion', '1.0.0.0'),
|
||||||
|
StringStruct('InternalName', 'GODCWAK'),
|
||||||
|
StringStruct('LegalCopyright', 'Copyright (c) 2026'),
|
||||||
|
StringStruct('OriginalFilename', 'GODCWAK.exe'),
|
||||||
|
StringStruct('ProductName', 'GODCWAK'),
|
||||||
|
StringStruct('ProductVersion', '1.0.0.0'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
VarFileInfo([VarStruct('Translation', [1033, 1200])]),
|
||||||
|
]
|
||||||
|
)
|
||||||
51
build_release.bat
Normal file
51
build_release.bat
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal EnableExtensions
|
||||||
|
title GODCWAK Release Build
|
||||||
|
color 0B
|
||||||
|
|
||||||
|
cd /d "%~dp0\.."
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo =========================================
|
||||||
|
echo GODCWAK - Windows Release Build
|
||||||
|
echo =========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
set PYTHON_EXE=
|
||||||
|
set PYTHON_ARGS=
|
||||||
|
if exist ".venv\Scripts\python.exe" (
|
||||||
|
set "PYTHON_EXE=.venv\Scripts\python.exe"
|
||||||
|
) else (
|
||||||
|
py -3 --version >nul 2>&1
|
||||||
|
if %ERRORLEVEL% EQU 0 (
|
||||||
|
set "PYTHON_EXE=py"
|
||||||
|
set "PYTHON_ARGS=-3"
|
||||||
|
) else (
|
||||||
|
set "PYTHON_EXE=python"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Using: %PYTHON_EXE% %PYTHON_ARGS%
|
||||||
|
echo.
|
||||||
|
|
||||||
|
"%PYTHON_EXE%" %PYTHON_ARGS% -m pip install -q -r requirements-build.txt
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo [ERROR] Failed to install build dependencies.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
"%PYTHON_EXE%" %PYTHON_ARGS% build\build_release.py --clean %*
|
||||||
|
set BUILD_EXIT=%ERRORLEVEL%
|
||||||
|
|
||||||
|
echo.
|
||||||
|
if %BUILD_EXIT% EQU 0 (
|
||||||
|
echo [OK] Release build finished.
|
||||||
|
echo Output: dist\GODCWAK\
|
||||||
|
echo Archive: release\
|
||||||
|
) else (
|
||||||
|
echo [ERROR] Build failed with exit code %BUILD_EXIT%
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b %BUILD_EXIT%
|
||||||
53
config.py
53
config.py
@@ -1,25 +1,35 @@
|
|||||||
"""
|
"""
|
||||||
Configuration and constants for GODCWAK.
|
Configuration and constants for GODCWAK.
|
||||||
"""
|
"""
|
||||||
import os
|
import json
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
APP_NAME = "GODCWAK"
|
APP_NAME = "GODCWAK"
|
||||||
APP_VERSION = "1.0"
|
APP_VERSION = "1.0"
|
||||||
|
|
||||||
# Paths
|
|
||||||
BASE_DIR = Path(__file__).parent
|
def _is_frozen() -> bool:
|
||||||
|
return getattr(sys, "frozen", False)
|
||||||
|
|
||||||
|
|
||||||
|
def _exe_dir() -> Path:
|
||||||
|
return Path(sys.executable).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def _bundle_dir() -> Path:
|
||||||
|
if _is_frozen() and hasattr(sys, "_MEIPASS"):
|
||||||
|
return Path(sys._MEIPASS)
|
||||||
|
return Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
# Writable root: next to the exe when frozen, project root in dev
|
||||||
|
BASE_DIR = _exe_dir() if _is_frozen() else Path(__file__).resolve().parent
|
||||||
|
BUNDLE_DIR = _bundle_dir()
|
||||||
DATA_DIR = BASE_DIR / "data"
|
DATA_DIR = BASE_DIR / "data"
|
||||||
ASSETS_DIR = BASE_DIR / "assets"
|
_assets_bundle = BUNDLE_DIR / "assets"
|
||||||
|
ASSETS_DIR = _assets_bundle if _assets_bundle.is_dir() else BASE_DIR / "assets"
|
||||||
# bug #35: removed DATA_DIR.mkdir() side effect from module level.
|
|
||||||
# Call ensure_dirs() explicitly at startup instead.
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_dirs():
|
|
||||||
"""Create required application directories. Call once at startup."""
|
|
||||||
DATA_DIR.mkdir(exist_ok=True)
|
|
||||||
ASSETS_DIR.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
# Facebook
|
# Facebook
|
||||||
@@ -69,6 +79,23 @@ DEFAULT_OLLAMA_CONFIG = {
|
|||||||
"retry_limit": 3,
|
"retry_limit": 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_dirs():
|
||||||
|
"""Create writable dirs and seed default Ollama config for packaged builds."""
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
(BASE_DIR / "assets").mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
if not OLLAMA_CONFIG_FILE.exists():
|
||||||
|
bundled = BUNDLE_DIR / "config_ollama.json"
|
||||||
|
if bundled.is_file():
|
||||||
|
shutil.copy2(bundled, OLLAMA_CONFIG_FILE)
|
||||||
|
else:
|
||||||
|
OLLAMA_CONFIG_FILE.write_text(
|
||||||
|
json.dumps(DEFAULT_OLLAMA_CONFIG, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# GUI Theme Colors
|
# GUI Theme Colors
|
||||||
THEME = {
|
THEME = {
|
||||||
"bg_dark": "#0a0a0f",
|
"bg_dark": "#0a0a0f",
|
||||||
|
|||||||
49
main.py
49
main.py
@@ -11,23 +11,51 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Add project root to path
|
# Project root on sys.path when running from source (PyInstaller bundles modules directly)
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
if not getattr(sys, "frozen", False):
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from config import ensure_dirs, DATA_DIR, APP_NAME, APP_VERSION
|
||||||
|
|
||||||
# bug #35: ensure data directories exist before any module tries to write them
|
|
||||||
from config import ensure_dirs
|
|
||||||
ensure_dirs()
|
ensure_dirs()
|
||||||
|
|
||||||
# Configure logging
|
_handlers = [logging.StreamHandler()]
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
_log_file = DATA_DIR / "godcwak.log"
|
||||||
|
_handlers.append(logging.FileHandler(_log_file, encoding="utf-8"))
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format="[%(asctime)s] %(levelname)s: %(message)s",
|
format="[%(asctime)s] %(levelname)s: %(message)s",
|
||||||
datefmt="%H:%M:%S",
|
datefmt="%H:%M:%S",
|
||||||
|
handlers=_handlers,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _fatal(message: str, exc: Exception | None = None):
|
||||||
|
"""Show a visible error in dev (console) and in frozen builds (dialog + log file)."""
|
||||||
|
if exc is not None:
|
||||||
|
logger.error(message, exc_info=exc)
|
||||||
|
else:
|
||||||
|
logger.error(message)
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import messagebox
|
||||||
|
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
messagebox.showerror(APP_NAME, message)
|
||||||
|
root.destroy()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
input("\nPress Enter to exit...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def check_dependencies():
|
def check_dependencies():
|
||||||
"""Check that critical dependencies are available."""
|
"""Check that critical dependencies are available."""
|
||||||
missing = []
|
missing = []
|
||||||
@@ -83,12 +111,13 @@ def check_dependencies():
|
|||||||
def main():
|
def main():
|
||||||
"""Application entry point."""
|
"""Application entry point."""
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
logger.info(" GODCWAK v1.0")
|
logger.info(f" {APP_NAME} v{APP_VERSION}")
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
logger.info(" (packaged build)")
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
|
|
||||||
if not check_dependencies():
|
if not check_dependencies():
|
||||||
input("\nPress Enter to exit...")
|
_fatal("Missing required dependencies. Reinstall the application or run pip install -r requirements.txt")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from src.gui.app import GODCWAKApp
|
from src.gui.app import GODCWAKApp
|
||||||
@@ -98,9 +127,7 @@ def main():
|
|||||||
app.mainloop()
|
app.mainloop()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fatal error: {e}", exc_info=True)
|
_fatal(f"Fatal error: {e}", exc=e)
|
||||||
input("\nPress Enter to exit...")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
3
requirements-build.txt
Normal file
3
requirements-build.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Windows release build toolchain (install into .venv alongside requirements.txt)
|
||||||
|
-r requirements.txt
|
||||||
|
pyinstaller>=6.3.0
|
||||||
Reference in New Issue
Block a user