diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..afe0337 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +DB_PATH := /var/lib/erminig/erminig.db +DB_DIR := /var/lib/erminig +PAK_USER := pak + +all: prepare_env create_db create_pak_user + +prepare_env: + @echo "Création de l'arborescence pour Erminig..." + @mkdir -p $(DB_DIR) + @mkdir -p /var/govel + @chown -R $(PAK_USER):$(PAK_USER) /var/govel || true + +create_db: + @echo "Initialisation de la base SQLite Erminig..." + @if [ ! -f "$(DB_PATH)" ]; then \ + sqlite3 $(DB_PATH) < schema.sql; \ + chown $(PAK_USER):$(PAK_USER) $(DB_PATH); \ + echo "Base de données créée à $(DB_PATH)"; \ + else \ + echo "La base existe déjà, on touche pas."; \ + fi + +create_pak_user: + @echo "Création de l'utilisateur '$(PAK_USER)'..." + @if ! id -u $(PAK_USER) >/dev/null 2>&1; then \ + useradd -r -m -d /var/govel -s /bin/bash $(PAK_USER); \ + echo "Utilisateur '$(PAK_USER)' créé."; \ + else \ + echo "L'utilisateur '$(PAK_USER)' existe déjà."; \ + fi + +clean: + @echo "Suppression de la base et de l'arborescence..." + @rm -f $(DB_PATH) + @rm -rf /var/govel + +.PHONY: all prepare_env create_db create_pak_user clean diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..b10562e --- /dev/null +++ b/schema.sql @@ -0,0 +1,37 @@ +-- schema.sql - Structure initiale de la base Erminig + +CREATE TABLE packages ( + name TEXT PRIMARY KEY, + version TEXT NOT NULL, + revision INTEGER NOT NULL, + status TEXT NOT NULL, -- draft, built, failed, validated + blocked BOOLEAN NOT NULL DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE sources ( + package_name TEXT, + url TEXT, + hash TEXT, + FOREIGN KEY (package_name) REFERENCES packages(name) +); + +CREATE TABLE dependencies ( + package_name TEXT, + dependency TEXT, + type TEXT, -- build, runtime, check + FOREIGN KEY (package_name) REFERENCES packages(name) +); + +CREATE TABLE builds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + package_name TEXT, + version TEXT, + revision INTEGER, + start_time TEXT DEFAULT CURRENT_TIMESTAMP, + end_time TEXT, + status TEXT, -- success, failed + log TEXT, + FOREIGN KEY (package_name) REFERENCES packages(name) +); diff --git a/src/erminig/common/__init__.py b/src/erminig/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/erminig/common/db.py b/src/erminig/common/db.py new file mode 100644 index 0000000..e0a2ef1 --- /dev/null +++ b/src/erminig/common/db.py @@ -0,0 +1,25 @@ +# +# Erminig - Interface SQLite +# Copyright (C) 2025 L0m1g +# Sous licence DOUARN - Voir le fichier LICENCE pour les détails +# +# Ce fichier fait partie du projet Erminig. +# Libre comme l’air, stable comme un menhir, et salé comme le beurre. +# + +import sqlite3 +import os + +DB_PATH = "/var/lib/erminig/erminig.db" + +def get_db_connection(): + """ + Ouvre une connexion à la base SQLite d'Erminig. + Si la base n'existe pas, déclenche une exception et laisse Make gérer sa création. + """ + if not os.path.exists(DB_PATH): + raise FileNotFoundError(f"La base de données Erminig est introuvable. Exécutez 'make init-db' avant de continuer.") + + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn diff --git a/src/erminig/common/pakfile.py b/src/erminig/common/pakfile.py new file mode 100644 index 0000000..c453d27 --- /dev/null +++ b/src/erminig/common/pakfile.py @@ -0,0 +1,224 @@ +# +# Erminig - Librairie Pakfile +# Copyright (C) 2025 L0m1g +# Sous licence DOUARN - Voir le fichier LICENCE pour les détails +# +# Ce fichier fait partie du projet Erminig. +# Libre comme l’air, stable comme un menhir, et salé comme le beurre. +# + +import os +import toml + +from .db import get_db_connection + +class Pakfile: + """ + Représente un fichier de description de paquet (pakfile.toml) + dans le système de gestion de paquets Erminig. + + Cette classe offre les opérations essentielles pour manipuler, + vérifier et modifier les pakfiles. + """ + + BASE_PATH = "/var/govel" + + def __init__(self, package: str): + """ + Initialise la gestion du pakfile pour un paquet donné. + + :param package: Nom du paquet + """ + self.package = package + self.data = {} + + def _load(self): + """ + Charge le contenu du pakfile en mémoire + Appelé automatiquement à l'initialisation si le fichier existe + """ + package_dir, pakfile_path = self._get_paths(package) + if not os.path.exists(pakfile_path): + raise FileNotFoundError(f"Pakfile introuvable pour {self.package} dans {pakfile_path}") + + with open(pakfile_path, "r", encoding="utf-8") as f: + self.data = toml.load(f) + + def _get_paths(self, package: str) -> tuple[str, str]: + """ + Retourne le chemin du dossier du paquet et du pakfile associé. + """ + package_dir = f"{self.BASE_PATH}/{package}" + pakfile_path = f"{package_dir}/pakfile.toml" + return package_dir, pakfile_path + + def new(self, package: str): + """ + Crée un nouveau pakfile avec un squelette de base prêt à être édité. + Soulève une erreur si le pakfile existe déjà. + """ + package_dir, pakfile_path = self._get_paths(package) + + if os.path.exists(pakfile_path): + raise FileExistsError(f"Le pakfile existe déjà pour {package}") + + data = { + "header": { + "copyright": "Copyright (C) 2025 L0m1g", + "license": "Sous licence DOUARN - Voir le fichier LICENCE pour les détails", + "author": "L0m1g", + "maintainer": "L0m1g", + "description": "Description à compléter" + }, + "name": package, + "ver": "", + "rev": 1, + "src": [], + "build": "# Ajouter les commandes de construction ici", + "check": "# Ajouter les commandes check ou équivalentes ici", + "install": "# Ajouter les commandes d’installation ici" + } + + os.makedirs(package_dir, exist_ok=True) + + with open(pakfile_path, "w", encoding="utf-8") as f: + toml.dump(data, f) + + self.update_db() + + def set(self, key: str, value): + """ + Définit une valeur dans le pakfile et sauvegarde immédiatement + """ + self.data[key] = value + self._save() + self.update_db() + + def _save(self): + """ + Ecrit le contenu actuel de self.data dans le pakfile correspondant + Créé le répertoire s'il n'existe pas. + """ + package_dir, pakfile_path = self._get_paths(self.package) + + os.makedirs(package_dir, exist_ok=True) + with open(pakfile_path, "w", encoding="utf-8") as f: + toml.dump(self.data, f) + + def get(self, key: str): + """ + Récupère la valeur d'une clé dans le pakfile + Redirige vers de getters spécifiques + + :param key: Clé à récupérer + :return: Valeur associée ou None + """ + match key: + case "name" | "ver" | "rev": + return self._get(key) + case "src" | "deps" | "bdeps": + return self._get_list(key) + case _: + return None + + def _get(self, key: str) -> str | None : + """ + Getter générique pour les chaines + """ + return self.data.get(key) + + def _get_list(self, key: str) -> list[str]: + """ + Getter spécifique aux listes + """ + return self.data.get(key,[]) + + def delete(self, package: str) -> bool: + """ + Supprime un paquet non installé et son répertoire associé. + + :param package: Nom du paquet à supprimer + :return: True si la suppression a réussi, False sinon + """ + package_dir, pakfile_path = self._get_paths(package) + + if not os.path.exists(package_dir): + print(f"Le paquet '{package}' n'existe pas. Rien à supprimer.") + return False + + try: + # On vire tout le répertoire et son contenu + for root, dirs, files in os.walk(package_dir, topdown=False): + for file in files: + os.remove(os.path.join(root, file)) + for dir in dirs: + os.rmdir(os.path.join(root, dir)) + os.rmdir(package_dir) + + print(f"Paquet '{package}' supprimé avec succès.") + return True + + except Exception as e: + print(f"Erreur lors de la suppression de '{package}': {e}") + return False + + self.update_db() + + def check(self): + """ + Vérifie la validité de la structure du pakfile. + Contrôle la présence et la cohérence des clés obligatoires. + Ne vérifie PAS la disponibilité des sources ou la qualité du code, juste la structure. + """ + required_keys = {"name", "ver", "rev", "src"} + optional_keys = {"deps", "bdeps", "build", "make", "install"} + + all_keys = set(self.data.keys()) + unknown_keys = all_keys - required_keys - optional_keys + + missing_keys = required_keys - all_keys + + if missing_keys: + print(f"Clés manquantes dans {self.package}: {', '.join(missing_keys)}") + return False + + if unknown_keys: + print(f"Clés inconnues dans {self.package}: {', '.join(unknown_keys)}") + return False + + return True + + def update_db(self): + """ + Met à jour la base SQLite avec les informations actuelles du pakfile. + Si le paquet existe, il est mis à jour. Sinon, il est inséré. + """ + conn = get_db_connection() + cursor = conn.cursor() + + # Récupère les données à jour + name = self.get("name") + version = self.get("ver") + revision = self.get("rev") + sources = self.get("src") + deps = self.get("deps") + bdeps = self.get("bdeps") + + # Conversion des listes en string (on se complique pas la vie) + src_str = ",".join(sources) if sources else "" + deps_str = ",".join(deps) if deps else "" + bdeps_str = ",".join(bdeps) if bdeps else "" + + cursor.execute(""" + INSERT INTO paquets (name, version, revision, sources, deps, build_deps) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(name) DO UPDATE SET + version = excluded.version, + revision = excluded.revision, + sources = excluded.sources, + deps = excluded.deps, + build_deps = excluded.build_deps + """, (name, version, revision, src_str, deps_str, bdeps_str)) + + conn.commit() + conn.close()