316 lines
7.0 KiB
Python
316 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import time
|
|
import subprocess
|
|
|
|
STATE_FILE = os.path.expanduser("~/.uni/state.json")
|
|
|
|
|
|
# -------------------------
|
|
# STATE
|
|
# -------------------------
|
|
def load_state():
|
|
if not os.path.exists(STATE_FILE):
|
|
return {"backends": {}, "packages": {}}
|
|
|
|
with open(STATE_FILE, "r") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def save_state(state):
|
|
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
|
with open(STATE_FILE, "w") as f:
|
|
json.dump(state, f, indent=2)
|
|
|
|
|
|
# -------------------------
|
|
# NORMALIZATION (FIX)
|
|
# -------------------------
|
|
def normalize_state(state):
|
|
fixed = {}
|
|
|
|
for pkg, entries in state.get("packages", {}).items():
|
|
|
|
# old format: "apt"
|
|
if isinstance(entries, str):
|
|
fixed[pkg] = [{"backend": entries}]
|
|
continue
|
|
|
|
# already correct list
|
|
if isinstance(entries, list):
|
|
clean = []
|
|
for e in entries:
|
|
if isinstance(e, dict) and "backend" in e:
|
|
clean.append(e)
|
|
elif isinstance(e, str):
|
|
clean.append({"backend": e})
|
|
fixed[pkg] = clean
|
|
continue
|
|
|
|
fixed[pkg] = []
|
|
|
|
state["packages"] = fixed
|
|
return state
|
|
|
|
|
|
# -------------------------
|
|
# BACKEND DETECTION
|
|
# -------------------------
|
|
def has(cmd):
|
|
return subprocess.call(
|
|
["which", cmd],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL
|
|
) == 0
|
|
|
|
|
|
def detect_backends(state):
|
|
state["backends"] = {
|
|
"apt": has("apt"),
|
|
"dnf": has("dnf"),
|
|
"pacman": has("pacman"),
|
|
"flatpak": has("flatpak")
|
|
}
|
|
return state["backends"]
|
|
|
|
|
|
# -------------------------
|
|
# RESOLVER
|
|
# -------------------------
|
|
def resolve_backend(pkg, state):
|
|
gui_apps = ["firefox", "discord", "spotify", "vscode"]
|
|
|
|
if pkg in gui_apps and state["backends"].get("flatpak"):
|
|
return "flatpak"
|
|
|
|
if state["backends"].get("apt"):
|
|
return "apt"
|
|
if state["backends"].get("dnf"):
|
|
return "dnf"
|
|
if state["backends"].get("pacman"):
|
|
return "pacman"
|
|
|
|
return None
|
|
|
|
|
|
# -------------------------
|
|
# CONFLICT CHECK
|
|
# -------------------------
|
|
def check_conflict(state, pkg, backend):
|
|
if pkg not in state["packages"]:
|
|
return False
|
|
|
|
for e in state["packages"][pkg]:
|
|
if e.get("backend") != backend:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
# -------------------------
|
|
# EXEC
|
|
# -------------------------
|
|
def run(cmd):
|
|
subprocess.run(cmd)
|
|
|
|
|
|
def install_pkg(pkg, backend):
|
|
if backend == "apt":
|
|
run(["sudo", "apt", "install", "-y", pkg])
|
|
elif backend == "dnf":
|
|
run(["sudo", "dnf", "install", "-y", pkg])
|
|
elif backend == "pacman":
|
|
run(["sudo", "pacman", "-S", "--noconfirm", pkg])
|
|
elif backend == "flatpak":
|
|
run(["flatpak", "install", "-y", pkg])
|
|
|
|
|
|
def remove_pkg(pkg, backend):
|
|
if backend == "apt":
|
|
run(["sudo", "apt", "remove", "-y", pkg])
|
|
elif backend == "dnf":
|
|
run(["sudo", "dnf", "remove", "-y", pkg])
|
|
elif backend == "pacman":
|
|
run(["sudo", "pacman", "-R", "--noconfirm", pkg])
|
|
elif backend == "flatpak":
|
|
run(["flatpak", "uninstall", "-y", pkg])
|
|
|
|
|
|
def update_all(state):
|
|
if state["backends"].get("apt"):
|
|
run(["sudo", "apt", "update"])
|
|
run(["sudo", "apt", "upgrade", "-y"])
|
|
|
|
if state["backends"].get("dnf"):
|
|
run(["sudo", "dnf", "upgrade", "-y"])
|
|
|
|
if state["backends"].get("pacman"):
|
|
run(["sudo", "pacman", "-Syu", "--noconfirm"])
|
|
|
|
if state["backends"].get("flatpak"):
|
|
run(["flatpak", "update", "-y"])
|
|
|
|
|
|
# -------------------------
|
|
# TRACKING
|
|
# -------------------------
|
|
def register_install(state, pkg, backend):
|
|
if pkg not in state["packages"]:
|
|
state["packages"][pkg] = []
|
|
|
|
state["packages"][pkg].append({
|
|
"backend": backend,
|
|
"time": int(time.time())
|
|
})
|
|
|
|
|
|
def register_remove(state, pkg, backend):
|
|
if pkg not in state["packages"]:
|
|
return
|
|
|
|
state["packages"][pkg] = [
|
|
e for e in state["packages"][pkg]
|
|
if e.get("backend") != backend
|
|
]
|
|
|
|
if not state["packages"][pkg]:
|
|
del state["packages"][pkg]
|
|
|
|
|
|
# -------------------------
|
|
# LIST
|
|
# -------------------------
|
|
def list_installed():
|
|
state = load_state()
|
|
state = normalize_state(state)
|
|
|
|
for pkg, entries in state["packages"].items():
|
|
backends = ", ".join([e["backend"] for e in entries])
|
|
print(f"{pkg} -> {backends}")
|
|
|
|
|
|
# -------------------------
|
|
# DOCTOR
|
|
# -------------------------
|
|
def doctor():
|
|
state = load_state()
|
|
state = normalize_state(state)
|
|
detect_backends(state)
|
|
save_state(state)
|
|
|
|
print("=== SYSTEM ===")
|
|
for k, v in state["backends"].items():
|
|
print(f"{k}: {'yes' if v else 'no'}")
|
|
|
|
print("\n=== HEALTH ===")
|
|
broken = []
|
|
|
|
for pkg, entries in state["packages"].items():
|
|
if not isinstance(entries, list):
|
|
broken.append(pkg)
|
|
continue
|
|
|
|
for e in entries:
|
|
if not isinstance(e, dict) or "backend" not in e:
|
|
broken.append(pkg)
|
|
|
|
print("OK" if not broken else f"broken: {len(broken)}")
|
|
|
|
print("\n=== CONFLICTS ===")
|
|
conflicts = {}
|
|
|
|
for pkg, entries in state["packages"].items():
|
|
backends = list(set([e["backend"] for e in entries]))
|
|
|
|
if len(backends) > 1:
|
|
conflicts[pkg] = backends
|
|
|
|
if not conflicts:
|
|
print("none")
|
|
else:
|
|
for k, v in conflicts.items():
|
|
print(f"{k}: {', '.join(v)}")
|
|
|
|
print("\n=== SUMMARY ===")
|
|
print(f"packages: {len(state['packages'])}")
|
|
print(f"conflicts: {len(conflicts)}")
|
|
print(f"corrupt: {len(broken)}")
|
|
|
|
|
|
# -------------------------
|
|
# INSTALL / REMOVE
|
|
# -------------------------
|
|
def install(pkg):
|
|
state = load_state()
|
|
state = normalize_state(state)
|
|
detect_backends(state)
|
|
|
|
backend = resolve_backend(pkg, state)
|
|
|
|
if not backend:
|
|
print("no backend")
|
|
return
|
|
|
|
if check_conflict(state, pkg, backend):
|
|
print("WARNING: backend mismatch")
|
|
|
|
install_pkg(pkg, backend)
|
|
register_install(state, pkg, backend)
|
|
save_state(state)
|
|
|
|
|
|
def remove(pkg):
|
|
state = load_state()
|
|
state = normalize_state(state)
|
|
|
|
if pkg not in state["packages"]:
|
|
print("not tracked")
|
|
return
|
|
|
|
for e in state["packages"][pkg]:
|
|
remove_pkg(pkg, e["backend"])
|
|
|
|
del state["packages"][pkg]
|
|
save_state(state)
|
|
|
|
|
|
def update():
|
|
state = load_state()
|
|
detect_backends(state)
|
|
update_all(state)
|
|
|
|
|
|
# -------------------------
|
|
# MAIN
|
|
# -------------------------
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("install|remove|update|list|doctor")
|
|
return
|
|
|
|
cmd = sys.argv[1]
|
|
|
|
if cmd == "install":
|
|
install(sys.argv[2])
|
|
|
|
elif cmd == "remove":
|
|
remove(sys.argv[2])
|
|
|
|
elif cmd == "update":
|
|
update()
|
|
|
|
elif cmd == "list":
|
|
list_installed()
|
|
|
|
elif cmd == "doctor":
|
|
doctor()
|
|
|
|
else:
|
|
print("unknown")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |