Fix: PyInstaller-Bundle für installierte Version repariert (connectorx, SQL-Ressourcen)

- connectorx via collect_all() eingebunden statt hiddenimports (Rust-PYD + __init__.py + Metadaten als Einheit)
- SQL/CSV-Ressourcen (src/res/) ins PyInstaller-Bundle aufgenommen
- Pfadauflösung in database.py auf sys._MEIPASS umgestellt für installierten Modus
- connectorx als explizite Abhängigkeit in pyproject.toml ergänzt
- Dokumentation (windows_distribution.md) um collect_all-Pattern und _MEIPASS-Hinweise erweitert
- Version auf 1.0.0 aktualisiert, Hersteller-Informationen ergänzt
This commit is contained in:
2026-02-15 19:51:58 +01:00
parent ec33a5b586
commit affba2a9ca
7 changed files with 100 additions and 54 deletions
+31 -21
View File
@@ -4,25 +4,36 @@ PyInstaller Konfiguration für DocuMentor
Erstellt eine eigenständige Windows-Executable ohne Python-Installation Erstellt eine eigenständige Windows-Executable ohne Python-Installation
""" """
import sys import sys
from pathlib import Path from pathlib import Path
from PyInstaller.utils.hooks import collect_all
block_cipher = None
block_cipher = None
# Projektpfad
project_root = Path(SPECPATH) # Projektpfad
src_path = project_root / 'src' project_root = Path(SPECPATH)
src_path = project_root / 'src'
# Alle UI-Dateien sammeln
ui_files = [] # connectorx komplett sammeln (Python-Code, native .pyd und Metadaten)
for ui_file in (src_path / 'ui').glob('*.ui'): # PyInstaller erkennt connectorx nicht automatisch, da es zur Laufzeit
ui_files.append((str(ui_file), 'ui')) # von polars per importlib.import_module() geladen wird
cx_datas, cx_binaries, cx_hiddenimports = collect_all('connectorx')
a = Analysis(
[str(src_path / 'main.py')], # Alle UI-Dateien sammeln
pathex=[str(src_path)], ui_files = []
binaries=[], for ui_file in (src_path / 'ui').glob('*.ui'):
datas=ui_files, # UI-Dateien einbinden ui_files.append((str(ui_file), 'ui'))
# Ressource-Dateien (SQL, CSV) einbinden
res_files = []
for res_file in (src_path / 'res').glob('*'):
res_files.append((str(res_file), 'res'))
a = Analysis(
[str(src_path / 'main.py')],
pathex=[str(src_path)],
binaries=cx_binaries,
datas=ui_files + res_files + cx_datas,
hiddenimports=[ hiddenimports=[
'PySide6.QtCore', 'PySide6.QtCore',
'PySide6.QtGui', 'PySide6.QtGui',
@@ -32,8 +43,7 @@ a = Analysis(
'pydantic_yaml', 'pydantic_yaml',
'polars', 'polars',
'pyarrow', 'pyarrow',
'connectorx', ] + cx_hiddenimports,
],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
runtime_hooks=[], runtime_hooks=[],
+2 -2
View File
@@ -4,8 +4,8 @@
<!-- Paket-Definition (ersetzt Product in v4) --> <!-- Paket-Definition (ersetzt Product in v4) -->
<Package <Package
Name="DocuMentor" Name="DocuMentor"
Version="0.1.0" Version="1.0.0"
Manufacturer="Ihr Name/Firma" Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E" UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
Language="1031" Language="1031"
Compressed="yes" Compressed="yes"
+3 -3
View File
@@ -70,11 +70,11 @@ VSVersionInfo(
[ [
StringTable( StringTable(
'040904B0', # Deutsch (0x0409 = Englisch, 0x0407 = Deutsch), Unicode '040904B0', # Deutsch (0x0409 = Englisch, 0x0407 = Deutsch), Unicode
[StringStruct('CompanyName', 'Ihr Name/Organisation'), [StringStruct('CompanyName', 'Vitali Graf / Software- und Datenbankentwicklung'),
StringStruct('FileDescription', '{description}'), StringStruct('FileDescription', '{description}'),
StringStruct('FileVersion', '{version}'), StringStruct('FileVersion', '{version}'),
StringStruct('InternalName', '{name}'), StringStruct('InternalName', '{name}'),
StringStruct('LegalCopyright', '© {year} Ihr Name. Alle Rechte vorbehalten.'), StringStruct('LegalCopyright', '© {year} Vitali Graf. Alle Rechte vorbehalten.'),
StringStruct('OriginalFilename', '{name}.exe'), StringStruct('OriginalFilename', '{name}.exe'),
StringStruct('ProductName', '{name}'), StringStruct('ProductName', '{name}'),
StringStruct('ProductVersion', '{version}')]) StringStruct('ProductVersion', '{version}')])
@@ -88,7 +88,7 @@ VSVersionInfo(
version_info_path = project_root / "version_info.txt" version_info_path = project_root / "version_info.txt"
version_info_path.write_text(version_info_content, encoding='utf-8') version_info_path.write_text(version_info_content, encoding='utf-8')
print(f"✓ version_info.txt erstellt") print("✓ version_info.txt erstellt")
print(f" Version: {version}") print(f" Version: {version}")
print(f" Datei: {version_info_path}") print(f" Datei: {version_info_path}")
+52 -25
View File
@@ -166,14 +166,16 @@ exe = EXE(
) )
``` ```
## PyInstaller .spec Konfiguration ## PyInstaller .spec Konfiguration
Die Datei `DocuMentor.spec` enthält: Die Datei `DocuMentor.spec` enthält:
- **datas**: UI-Dateien (.ui) werden mitgepackt - **datas**: UI-Dateien (.ui), Ressource-Dateien (SQL, CSV) und connectorx-Package
- **hiddenimports**: Alle notwendigen PySide6/Polars-Module - **binaries**: Native Extensions (connectorx `.pyd`)
- **console=False**: GUI-Anwendung ohne Konsole - **hiddenimports**: Alle notwendigen PySide6/Polars-Module
- **upx=True**: Kompression der Binaries - **collect_all('connectorx')**: Sammelt Python-Code, native `.pyd`-Extension und Metadaten als Einheit (nötig, da `polars` connectorx erst zur Laufzeit per `importlib.import_module()` lädt)
- **console=False**: GUI-Anwendung ohne Konsole
- **upx=True**: Kompression der Binaries
### Wichtige Einstellungen ### Wichtige Einstellungen
@@ -294,24 +296,49 @@ wine dist/DocuMentor/DocuMentor.exe
## Troubleshooting ## Troubleshooting
### Problem: "Module not found" Fehler ### Problem: "Module not found" Fehler
**Lösung A** Einfache Python-Module: Hidden imports in `DocuMentor.spec` ergänzen:
```python
hiddenimports=[
# ... existing
'missing_module',
]
```
**Lösung B** Packages mit nativen Extensions (Rust/C, z.B. `connectorx`):
`hiddenimports` allein reicht nicht, da PyInstaller die `__init__.py` ins PYZ-Archiv packt, aber die `.pyd`-Extension separat im Dateisystem erwartet. Stattdessen `collect_all` verwenden:
```python
from PyInstaller.utils.hooks import collect_all
cx_datas, cx_binaries, cx_hiddenimports = collect_all('problematic_package')
a = Analysis(
# ...
binaries=cx_binaries,
datas=cx_datas,
hiddenimports=cx_hiddenimports,
)
```
**Lösung**: Hidden imports in `DocuMentor.spec` ergänzen: ### Problem: UI-Dateien oder Ressourcen nicht gefunden
```python
hiddenimports=[ **Lösung**: Prüfen ob datas korrekt konfiguriert. Ressource-Pfade müssen im Code PyInstaller-kompatibel aufgelöst werden (`sys._MEIPASS`):
# ... existing ```python
'missing_module', # In DocuMentor.spec:
] datas=[
``` ('src/ui/*.ui', 'ui'),
('src/res/*', 'res'),
### Problem: UI-Dateien nicht gefunden ]
**Lösung**: Prüfen ob datas korrekt konfiguriert: # Im Code:
```python import sys
datas=[ from pathlib import Path
('src/ui/*.ui', 'ui'),
] if hasattr(sys, "_MEIPASS"):
``` res_path = Path(sys._MEIPASS) / "res" / "data.sql"
else:
res_path = Path(__file__).parents[3] / "src" / "res" / "data.sql"
```
### Problem: Executable zu groß ### Problem: Executable zu groß
+2 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "DocuMentor" name = "DocuMentor"
version = "0.1.0" version = "1.0.0"
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung" description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
readme = "README.md" readme = "README.md"
license = {text = "MIT"} license = {text = "MIT"}
@@ -9,6 +9,7 @@ dependencies = [
"pydantic-settings>=2.12.0", "pydantic-settings>=2.12.0",
"pyside6>=6.10.1", "pyside6>=6.10.1",
"polars[connectorx,pyarrow]>=1.37.0", "polars[connectorx,pyarrow]>=1.37.0",
"connectorx>=0.4.0",
"pydantic-yaml>=1.6.0", "pydantic-yaml>=1.6.0",
"psutil>=6.1.1", "psutil>=6.1.1",
] ]
+7 -1
View File
@@ -5,6 +5,7 @@ Dieses Mixin enthält alle Methoden zur PostgreSQL-Datenbankanbindung
und Datenverarbeitung für das MainWindow. und Datenverarbeitung für das MainWindow.
""" """
import sys
import time import time
import logging import logging
from pathlib import Path from pathlib import Path
@@ -162,7 +163,12 @@ class DatabaseMixin:
tuple[str, str]|tuple[None, None]: (sql_query, connection_string) oder (None, None) bei Fehler tuple[str, str]|tuple[None, None]: (sql_query, connection_string) oder (None, None) bei Fehler
""" """
try: try:
sql_file_path = Path("src/res/data.sql") # PyInstaller entpackt Ressourcen nach sys._MEIPASS;
# im Entwicklungsmodus liegt die Datei relativ zum Repo-Root
if hasattr(sys, "_MEIPASS"):
sql_file_path = Path(sys._MEIPASS) / "res" / "data.sql" # type: ignore[attr-defined]
else:
sql_file_path = Path(__file__).parents[3] / "src" / "res" / "data.sql"
if not sql_file_path.exists(): if not sql_file_path.exists():
QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}") QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
return None, None return None, None
Generated
+3 -1
View File
@@ -34,9 +34,10 @@ wheels = [
[[package]] [[package]]
name = "documentor" name = "documentor"
version = "0.1.0" version = "1.0.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "connectorx" },
{ name = "polars", extra = ["connectorx", "pyarrow"] }, { name = "polars", extra = ["connectorx", "pyarrow"] },
{ name = "psutil" }, { name = "psutil" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
@@ -53,6 +54,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "connectorx", specifier = ">=0.4.0" },
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.37.0" }, { name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.37.0" },
{ name = "psutil", specifier = ">=6.1.1" }, { name = "psutil", specifier = ">=6.1.1" },
{ name = "pydantic-settings", specifier = ">=2.12.0" }, { name = "pydantic-settings", specifier = ">=2.12.0" },