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.
171 lines
7.3 KiB
Python
171 lines
7.3 KiB
Python
#!/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())
|