Files
Projektwissen/scripts/link_qgis_profile.py
T

159 lines
6.8 KiB
Python
Raw Normal View History

#!/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 <name>.vor_link umbenennen und verknüpfen")
args = ap.parse_args()
if args.pfad is None and args.profil is None:
ap.error("--profil <name> oder --pfad <ordner> 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())