Files
Projektwissen/scripts/link_qgis_profile.py
T
Michael Otto 85a34e4d1d Lokal abweichenden Ablageort der Plugin-Klone unterstuetzen (#6)
hallo.py, setup_workspace.py und link_qgis_profile.py gingen bisher fest
davon aus, dass die Repos aus workspace.toml direkte Unterordner von
Projektwissen sind. Eine optionale, nicht versionierte workspace.local.toml
mit [workspace] repos_root = "..." kann das jetzt lokal ueberschreiben, z. B.
wenn die Klone zusaetzlich unter einem separaten src-Ordner liegen und von
dort in mehrere QGIS-Profile verlinkt werden. Ohne diese Datei bleibt das
Verhalten fuer alle unveraendert.
2026-09-14 11:36:55 +02:00

171 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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]
LOKALE_ÜBERSCHREIBUNG = ROOT / "workspace.local.toml"
ROLLEN_MIT_PLUGIN = {"plattform", "fachplugin", "geruest"}
def repos_root() -> Path:
"""Ordner der Plugin-/Daten-Klone, siehe workspace.local.toml (nicht versioniert)."""
if not LOKALE_ÜBERSCHREIBUNG.is_file():
return ROOT
with LOKALE_ÜBERSCHREIBUNG.open("rb") as f:
lokal = tomllib.load(f)
pfad = lokal.get("workspace", {}).get("repos_root")
return Path(pfad).expanduser() if pfad else ROOT
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
rroot = repos_root()
for paket in gewuenscht:
quelle = rroot / 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())