diff --git a/.gitignore b/.gitignore
index 6da1502..55a4fe8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,9 @@ version_info.txt
# Virtual environments
.venv
+
+# WiX Installer Build-Artefakte
+ProductFiles.wxs
+*.msi
+*.wixpdb
+.wix/
diff --git a/DocuMentor.wxs b/DocuMentor.wxs
new file mode 100644
index 0000000..0987872
--- /dev/null
+++ b/DocuMentor.wxs
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build_msi.py b/build_msi.py
new file mode 100644
index 0000000..3183eed
--- /dev/null
+++ b/build_msi.py
@@ -0,0 +1,53 @@
+"""
+WiX MSI Build-Skript für DocuMentor (WiX v6)
+
+Erstellt einen MSI-Installer aus dem PyInstaller Build.
+"""
+
+import subprocess
+import sys
+from pathlib import Path
+
+
+def build_msi():
+ """Erstellt MSI-Installer mit WiX v6."""
+ project_root = Path(__file__).parent
+ dist_dir = project_root / "dist" / "DocuMentor"
+
+ if not dist_dir.exists():
+ print("FEHLER: PyInstaller Build nicht gefunden!")
+ print("Bitte zuerst ausführen: uv run python build_windows.py")
+ sys.exit(1)
+
+ print(f"DocuMentor Build gefunden: {dist_dir}")
+ print()
+
+ # Schritt 1: ProductFiles.wxs generieren (ersetzt WiX heat)
+ print("Schritt 1/2: Generiere ProductFiles.wxs...")
+ result = subprocess.run(["uv", "run", "python", "generate_wix_files.py"], check=False)
+
+ if result.returncode != 0:
+ print("\nFEHLER: ProductFiles.wxs Generierung fehlgeschlagen!")
+ sys.exit(1)
+
+ print()
+
+ # Schritt 2: MSI kompilieren mit WiX v6
+ print("Schritt 2/2: Kompiliere MSI-Installer...")
+ result = subprocess.run(["wix", "build", "DocuMentor.wxs", "ProductFiles.wxs", "-o", "DocuMentor.msi"], check=False)
+
+ if result.returncode != 0:
+ print("\nFEHLER: MSI-Kompilierung fehlgeschlagen!")
+ print("Stelle sicher, dass WiX v6 installiert ist:")
+ print(" dotnet tool install --global wix")
+ sys.exit(1)
+
+ print()
+ print("[OK] MSI erfolgreich erstellt: DocuMentor.msi")
+ print()
+ print("Installation testen mit:")
+ print(" msiexec /i DocuMentor.msi")
+
+
+if __name__ == "__main__":
+ build_msi()
diff --git a/docs/windows_distribution.md b/docs/windows_distribution.md
index c0e0cb5..e07666c 100644
--- a/docs/windows_distribution.md
+++ b/docs/windows_distribution.md
@@ -51,10 +51,8 @@ Dies erstellt:
**Voraussetzungen:**
```bash
-# WiX Toolset v4 installieren (empfohlen)
+# WiX Toolset v6 installieren (aktuell)
dotnet tool install --global wix
-
-# Oder WiX v3 von https://wixtoolset.org/releases/
```
**Schritt 1: PyInstaller Build erstellen**
@@ -62,166 +60,33 @@ dotnet tool install --global wix
uv run python build_windows.py
```
-**Schritt 2: WiX-Konfiguration erstellen (`DocuMentor.wxs`):**
-```xml
-
-
-
-
-
-
-
+**Schritt 2: ProductFiles.wxs generieren**
-
-
-
-
+**WICHTIG**: WiX v6 hat das `heat` Tool entfernt. Stattdessen verwenden wir ein Python-Skript:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-**Schritt 3: GUIDs generieren**
```bash
-# GUIDs mit PowerShell generieren
-powershell -Command "[guid]::NewGuid()"
-
-# Oder Python-Skript verwenden
-uv run python generate_guid.py
+# Generiert automatisch ProductFiles.wxs mit allen Dateien aus dist/DocuMentor
+uv run python generate_wix_files.py
```
-**Schritt 4: Dateien automatisch harvesten (empfohlen)**
+**Schritt 3: MSI kompilieren**
```bash
-# WiX v3: Heat Tool verwenden
-heat dir dist\DocuMentor -cg ProductComponents -gg -sfrag -srd -dr INSTALLFOLDER -out ProductFiles.wxs
-
-# WiX v4:
-wix heat dir dist\DocuMentor -cg ProductComponents -gg -sfrag -srd -dr INSTALLFOLDER -out ProductFiles.wxs
+# WiX v6 Build
+wix build DocuMentor.wxs ProductFiles.wxs -o DocuMentor.msi
```
-**Schritt 5: MSI kompilieren**
-```bash
-# WiX v3
-candle DocuMentor.wxs -out obj\
-light obj\DocuMentor.wixobj -out dist\installer\DocuMentor-0.1.0.msi -ext WixUIExtension
+**Hinweis**: Die Dateien `DocuMentor.wxs` und `generate_wix_files.py` sind bereits im Repository vorhanden und WiX v4/v6-kompatibel.
-# WiX v4
-wix build DocuMentor.wxs -out dist\installer\DocuMentor-0.1.0.msi
-```
-
-**Schritt 6: MSI testen**
+**Schritt 4: MSI testen**
```bash
# Installation (als Administrator)
-msiexec /i dist\installer\DocuMentor-0.1.0.msi
+msiexec /i DocuMentor.msi
# Silent Installation für Deployment
-msiexec /i DocuMentor-0.1.0.msi /quiet /qn /norestart
+msiexec /i DocuMentor.msi /quiet /qn /norestart
# Deinstallation
-msiexec /x dist\installer\DocuMentor-0.1.0.msi
+msiexec /x DocuMentor.msi
```
**MSI-Vorteile gegenüber Inno Setup:**
@@ -234,7 +99,7 @@ msiexec /x dist\installer\DocuMentor-0.1.0.msi
**Build-Automatisierung (`build_msi.py`):**
```python
-"""WiX MSI Build-Skript für DocuMentor"""
+"""WiX MSI Build-Skript für DocuMentor (WiX v6)"""
import subprocess
import sys
from pathlib import Path
@@ -247,35 +112,22 @@ def build_msi():
print("Fehler: PyInstaller Build nicht gefunden. Erst build_windows.py ausführen!")
sys.exit(1)
- # Heat: Dateien harvesten
- print("Harvesting Dateien mit Heat...")
+ # Schritt 1: ProductFiles.wxs generieren (ersetzt heat)
+ print("Generiere ProductFiles.wxs...")
subprocess.run([
- "heat", "dir", str(dist_dir),
- "-cg", "ProductComponents",
- "-gg", "-sfrag", "-srd",
- "-dr", "INSTALLFOLDER",
- "-out", "ProductFiles.wxs"
+ "uv", "run", "python", "generate_wix_files.py"
], check=True)
- # Candle: WXS zu WIXOBJ kompilieren
- print("Kompiliere WXS-Dateien...")
+ # Schritt 2: MSI kompilieren mit WiX v6
+ print("Kompiliere MSI-Installer...")
subprocess.run([
- "candle", "DocuMentor.wxs", "ProductFiles.wxs",
- "-out", "obj\\"
+ "wix", "build",
+ "DocuMentor.wxs",
+ "ProductFiles.wxs",
+ "-o", "DocuMentor.msi"
], check=True)
- # Light: WIXOBJ zu MSI linken
- print("Erzeuge MSI-Installer...")
- (project_root / "dist" / "installer").mkdir(exist_ok=True)
- subprocess.run([
- "light",
- "obj\\DocuMentor.wixobj",
- "obj\\ProductFiles.wixobj",
- "-out", "dist\\installer\\DocuMentor-0.1.0.msi",
- "-ext", "WixUIExtension"
- ], check=True)
-
- print("\n✅ MSI erstellt: dist\\installer\\DocuMentor-0.1.0.msi")
+ print("\n[OK] MSI erstellt: DocuMentor.msi")
if __name__ == "__main__":
build_msi()
diff --git a/generate_wix_files.py b/generate_wix_files.py
new file mode 100644
index 0000000..4b60858
--- /dev/null
+++ b/generate_wix_files.py
@@ -0,0 +1,184 @@
+"""
+WiX ProductFiles.wxs Generator
+
+Generiert automatisch eine WXS-Datei mit allen Dateien aus dist/DocuMentor.
+Ersetzt die veraltete 'wix heat' Funktionalität für WiX v6.
+"""
+
+import uuid
+from pathlib import Path
+from xml.etree import ElementTree as ET
+
+
+def generate_guid() -> str:
+ """Generiert eine neue GUID."""
+ return str(uuid.uuid4()).upper()
+
+
+def sanitize_id(name: str) -> str:
+ """
+ Macht einen String WiX-konform für IDs.
+
+ WiX erlaubt nur: A-Z, a-z, 0-9, _, .
+ Darf nicht mit Zahl beginnen.
+ """
+ # Ersetze illegale Zeichen
+ sanitized = name.replace("-", "_").replace(" ", "_").replace(".", "_")
+
+ # Entferne alle anderen nicht erlaubten Zeichen
+ allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"
+ sanitized = "".join(c if c in allowed else "_" for c in sanitized)
+
+ # Darf nicht mit Zahl beginnen
+ if sanitized and sanitized[0].isdigit():
+ sanitized = f"_{sanitized}"
+
+ return sanitized
+
+
+def create_wix_fragment(dist_dir: Path) -> ET.Element:
+ """
+ Erstellt ein WiX Fragment mit allen Dateien aus dem dist Verzeichnis.
+
+ Args:
+ dist_dir: Pfad zum dist/DocuMentor Verzeichnis
+
+ Returns:
+ ET.Element: Wix Root-Element mit Fragment
+ """
+ # Namespace (WiX v4+)
+ ns = "http://wixtoolset.org/schemas/v4/wxs"
+ ET.register_namespace("", ns)
+
+ # Root Element
+ wix = ET.Element(f"{{{ns}}}Wix")
+ fragment = ET.SubElement(wix, f"{{{ns}}}Fragment")
+ component_group = ET.SubElement(
+ fragment, f"{{{ns}}}ComponentGroup", {"Id": "ProductComponents", "Directory": "INSTALLFOLDER"}
+ )
+
+ # Sammle alle Dateien
+ all_files = sorted(dist_dir.rglob("*"))
+ files_only = [f for f in all_files if f.is_file()]
+
+ print(f"Gefunden: {len(files_only)} Dateien")
+
+ # Gruppiere nach Verzeichnis
+ dirs_dict: dict[Path, list[Path]] = {}
+ for file in files_only:
+ rel_dir = file.parent.relative_to(dist_dir)
+ if rel_dir not in dirs_dict:
+ dirs_dict[rel_dir] = []
+ dirs_dict[rel_dir].append(file)
+
+ # Erstelle Directory Fragments
+ dir_fragment = ET.SubElement(wix, f"{{{ns}}}Fragment")
+
+ # INSTALLFOLDER ist bereits in DocuMentor.wxs definiert
+ directory_ref = ET.SubElement(dir_fragment, f"{{{ns}}}DirectoryRef", {"Id": "INSTALLFOLDER"})
+
+ # Erstelle Verzeichnisstruktur
+ created_dirs = {"INSTALLFOLDER": directory_ref}
+
+ for rel_dir in sorted(dirs_dict.keys()):
+ if rel_dir == Path("."):
+ continue
+
+ parts = rel_dir.parts
+ parent_id = "INSTALLFOLDER"
+
+ for i, part in enumerate(parts):
+ current_path = Path(*parts[: i + 1])
+ dir_id = f"Dir_{sanitize_id(current_path.as_posix().replace('/', '_'))}"
+
+ if dir_id not in created_dirs:
+ parent_elem = created_dirs[parent_id]
+ new_dir = ET.SubElement(parent_elem, f"{{{ns}}}Directory", {"Id": dir_id, "Name": part})
+ created_dirs[dir_id] = new_dir
+ parent_id = dir_id
+ else:
+ parent_id = dir_id
+
+ # Füge Komponenten hinzu
+ component_counter = 0
+
+ for rel_dir, files in sorted(dirs_dict.items()):
+ if rel_dir == Path("."):
+ dir_id = "INSTALLFOLDER"
+ else:
+ dir_id = f"Dir_{sanitize_id(rel_dir.as_posix().replace('/', '_'))}"
+
+ # Erstelle eine Komponente pro Verzeichnis
+ component_id = f"Component_{sanitize_id(dir_id)}"
+ component_counter += 1
+
+ component = ET.SubElement(
+ component_group, f"{{{ns}}}Component", {"Id": component_id, "Directory": dir_id, "Guid": generate_guid()}
+ )
+
+ # Füge alle Dateien des Verzeichnisses hinzu
+ for idx, file in enumerate(files):
+ file_id = f"File_{sanitize_id(component_id)}_{idx}"
+ # Absoluter Pfad für WiX
+ source_path = str(file).replace("/", "\\")
+
+ file_attribs = {"Id": file_id, "Source": source_path, "Name": file.name}
+
+ # Erste Datei ist KeyPath
+ if idx == 0:
+ file_attribs["KeyPath"] = "yes"
+
+ ET.SubElement(component, f"{{{ns}}}File", file_attribs)
+
+ print(f"Erstellt: {component_counter} Komponenten")
+
+ return wix
+
+
+def format_xml(element: ET.Element, level: int = 0) -> None:
+ """Formatiert XML mit Einrückungen (in-place)."""
+ indent = " "
+ i = f"\n{indent * level}"
+
+ if len(element):
+ if not element.text or not element.text.strip():
+ element.text = i + indent
+ if not element.tail or not element.tail.strip():
+ element.tail = i
+ last_child = None
+ for child in element:
+ format_xml(child, level + 1)
+ last_child = child
+ if last_child is not None and (not last_child.tail or not last_child.tail.strip()):
+ last_child.tail = i
+ else:
+ if level and (not element.tail or not element.tail.strip()):
+ element.tail = i
+
+
+def main():
+ """Hauptfunktion."""
+ dist_dir = Path("dist/DocuMentor")
+ output_file = Path("ProductFiles.wxs")
+
+ if not dist_dir.exists():
+ print(f"FEHLER: {dist_dir} existiert nicht!")
+ print("Führe zuerst 'uv run pyinstaller DocuMentor.spec' aus.")
+ return
+
+ print(f"Generiere ProductFiles.wxs aus {dist_dir}...")
+
+ wix_root = create_wix_fragment(dist_dir)
+ format_xml(wix_root)
+
+ # Schreibe XML
+ tree = ET.ElementTree(wix_root)
+ tree.write(output_file, encoding="utf-8", xml_declaration=True)
+
+ print(f"[OK] {output_file} erfolgreich erstellt!")
+ print(f"\nNaechste Schritte:")
+ print(f"1. wix build DocuMentor.wxs ProductFiles.wxs -o DocuMentor.msi")
+
+
+if __name__ == "__main__":
+ main()