#!/usr/bin/env python3 """Verknüpft die Plugin-Klone des Workspace unter ihren Paketnamen im Plugin-Ordner eines QGIS-Profils. QGIS lädt die Plugins dann direkt aus der Arbeitskopie – ohne Release, ohne Kopieren. Eine Änderung im Klon ist nach „Plugin neu laden“ (Plugin „Plugin Reloader“) oder einem QGIS-Neustart aktiv. Aufrufe (aus dem Workspace-Root): python scripts/link_qgis_profile.py --profil dev # alle Plugins (plattform, fachplugin, geruest) python scripts/link_qgis_profile.py --profil dev sn_basis sn_plan41 # nur bestimmte Pakete python scripts/link_qgis_profile.py --pfad "D:/QGIS/profiles/dev/python/plugins" # Plugin-Ordner direkt angeben python scripts/link_qgis_profile.py --profil dev --status # nur anzeigen python scripts/link_qgis_profile.py --profil dev --entfernen # Verknüpfungen wieder entfernen Empfehlung: eigenes QGIS-Profil (z. B. „dev“) verwenden, in dem die Plugins NICHT aus dem Release-Feed installiert sind. Unter Windows werden Junctions angelegt (keine Administratorrechte nötig), sonst Symlinks. """ from __future__ import annotations import argparse import os import sys import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parents[1] ROLLEN_MIT_PLUGIN = {"plattform", "fachplugin", "geruest"} def profil_plugin_ordner(profil: str) -> Path: if sys.platform.startswith("win"): basis = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming")) elif sys.platform == "darwin": basis = Path.home() / "Library" / "Application Support" else: basis = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")) return basis / "QGIS" / "QGIS3" / "profiles" / profil / "python" / "plugins" def ist_link(p: Path) -> bool: if p.is_symlink(): return True if os.name == "nt" and p.is_dir(): try: return bool(os.stat(p, follow_symlinks=False).st_file_attributes & 0x400) # REPARSE_POINT (Junction) except (AttributeError, OSError): return False return False def link_anlegen(ziel: Path, quelle: Path) -> str: try: os.symlink(quelle, ziel, target_is_directory=True) return "Symlink" except (OSError, NotImplementedError): if os.name == "nt": import _winapi # type: ignore[attr-defined] _winapi.CreateJunction(str(quelle), str(ziel)) return "Junction" raise def link_entfernen(p: Path) -> None: if p.is_symlink(): p.unlink() else: os.rmdir(p) # Junction: rmdir entfernt nur den Verweis def main() -> int: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("pakete", nargs="*", help="Paketnamen (Standard: alle Plugins laut workspace.toml)") ap.add_argument("--profil", default=None, help="Name des QGIS-Profils, z. B. dev") ap.add_argument("--pfad", type=Path, default=None, help="Plugin-Ordner direkt (überschreibt --profil)") ap.add_argument("--status", action="store_true", help="nur anzeigen, nichts ändern") ap.add_argument("--entfernen", action="store_true", help="Verknüpfungen entfernen") ap.add_argument("--ersetzen", action="store_true", help="vorhandenen echten Plugin-Ordner (z. B. Release-Installation) nach .vor_link umbenennen und verknüpfen") args = ap.parse_args() if args.pfad is None and args.profil is None: ap.error("--profil oder --pfad angeben") plugin_ordner = args.pfad or profil_plugin_ordner(args.profil) with (ROOT / "workspace.toml").open("rb") as f: manifest = tomllib.load(f) repos = {r["paket"]: r for r in manifest["repos"] if r.get("paket") and r.get("rolle") in ROLLEN_MIT_PLUGIN} gewuenscht = args.pakete or list(repos) unbekannt = [p for p in gewuenscht if p not in repos] if unbekannt: print(f"Unbekannte oder nicht verknüpfbare Pakete: {unbekannt}. Möglich: {sorted(repos)}") return 2 print(f"QGIS-Plugin-Ordner: {plugin_ordner}") if not args.status and not args.entfernen: plugin_ordner.mkdir(parents=True, exist_ok=True) elif not plugin_ordner.exists(): print(" (existiert noch nicht – QGIS mit diesem Profil einmal starten oder --pfad prüfen)") rc = 0 for paket in gewuenscht: quelle = ROOT / repos[paket]["name"] ziel = plugin_ordner / paket if not (quelle / "__init__.py").is_file(): print(f"[{paket}] übersprungen: {quelle.name} enthält kein Plugin (__init__.py fehlt)") continue if args.status: if ist_link(ziel): print(f"[{paket}] verknüpft -> {os.path.realpath(ziel)}") elif ziel.exists(): print(f"[{paket}] echter Ordner vorhanden (Release-Installation?) – nicht verknüpft") else: print(f"[{paket}] nicht vorhanden") continue if args.entfernen: if ist_link(ziel): link_entfernen(ziel) print(f"[{paket}] Verknüpfung entfernt") elif ziel.exists(): print(f"[{paket}] ist ein echter Ordner – nicht entfernt") else: print(f"[{paket}] nichts zu tun") continue if ist_link(ziel): if os.path.realpath(ziel) == os.path.realpath(quelle): print(f"[{paket}] bereits verknüpft") continue link_entfernen(ziel) elif ziel.exists(): if not args.ersetzen: print(f"[{paket}] echter Ordner vorhanden: {ziel}\n" f" -> eigenes Profil verwenden oder mit --ersetzen nach {paket}.vor_link verschieben") rc = 1 continue sicherung = ziel.with_name(f"{paket}.vor_link") if sicherung.exists(): print(f"[{paket}] {sicherung.name} existiert bereits – bitte manuell aufräumen") rc = 1 continue ziel.rename(sicherung) print(f"[{paket}] vorhandenen Ordner nach {sicherung.name} verschoben") try: art = link_anlegen(ziel, quelle) print(f"[{paket}] {art} angelegt -> {quelle}") except OSError as e: print(f"[{paket}] FEHLER: {e}") rc = 1 if not args.status and not args.entfernen and rc == 0: print("\nIn QGIS: Erweiterungen -> Verwalten -> Einstellungen: „Auch experimentelle Erweiterungen anzeigen“ aktivieren,\n" "dann zuerst „LNO Sachsen | Plugin Basisfunktionen“ (sn_basis) einschalten, danach die Fachplugins.\n" "Nach Codeänderungen: Plugin „Plugin Reloader“ (offizielles Repository) oder QGIS neu starten.") return rc if __name__ == "__main__": raise SystemExit(main())