""" 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()