diff --git a/.gitignore b/.gitignore deleted file mode 100644 index cefb59e..0000000 --- a/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -__pycache__/ -*.pyc -*.pyo -*.pyd -*.egg-info/ -.pytest_cache/ -*.log -lib/erminig.db diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29..0000000 diff --git a/Makefile b/Makefile index b308180..afe0337 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,37 @@ -.PHONY: install test fmt docker-build up rebuild clean +DB_PATH := /var/lib/erminig/erminig.db +DB_DIR := /var/lib/erminig +PAK_USER := pak -install: - pip install -e .[dev] +all: prepare_env create_db create_pak_user -test: - pytest -v +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 -fmt: - black erminig tests - git add erminig tests +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 -docker-build: - podman-compose -f docker-compose.yml build - -up: - podman-compose -f docker-compose.yml run erminig - -rebuild: docker-build up +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: - sudo find . -name "*.pyc" -delete - sudo find . -name "__pycache__" -type d -exec rm -rf {} + - sudo find . -name "*.egg-info" -type d -exec rm -rf {} + - sudo find . -name ".pytest_cache" -type d -exec rm -rf {} + - -build-all: fmt test docker-build - + @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/data/config.yaml b/data/config.yaml deleted file mode 100644 index 9b836bf..0000000 --- a/data/config.yaml +++ /dev/null @@ -1,100 +0,0 @@ -templates: - gnu: - url: "https://ftp.gnu.org/gnu/@NAME@/" - pattern: "@NAME@-[0-9]+\\.[0-9]+(?:\\.[0-9]+)?\\.tar\\.gz" - savannah: - url: "https://download.savannah.gnu.org/releases/@NAME@/" - pattern: '@NAME@-[0-9]+\.[0-9]+(?:\.[0-9]).tar.gz' - -projects: - - name: acl - template: savannah - - name: attr - template: savannah - - name: autoconf - template: gnu - - name: automake - template: gnu - - - name: bash - template: gnu - - name: bc - github: gavinhoward/bc - - name: binutils - template: gnu - - name: bison - template: gnu - - name: bzip2 - url: https://www.sourceware.org/pub/bzip2/ - pattern: bzip2-[0-9]+\.[0-9]+\.[0-9]+\.tar.gz - - - name: check - github: libcheck/check - - name: coreutils - template: gnu - - - name: dejagnu - template: gnu - - name: diffutils - template: gnu - - - name: e2fsprogs - url: https://www.kernel.org/pub/linux/kernel/people/tytso/e2fsprogs/ - pattern: v[0-9]+\.[0-9]+\.[0-9]/ - file: e2fsprogs-${version}.tar.gz - - name: elfutils - url: https://sourceware.org/pub/elfutils/ - pattern: "[0-9]+\\.[0-9]+/" - file: elfutils-${version}.tar.bz2 - - name: expat - github: libexpat/libexpat - - name: expect - sourceforge: https://sourceforge.net/projects/expect/rss?path=/Expect - - - name: file - url: https://astron.com/pub/file/ - pattern: file-[0-9]+\.[0-9]+\.tar.gz - - name: findutils - template: gnu - - name: flex - github: westes/flex - - name: flit - github: pypa/flit - - - name: gawk - template: gnu - - name: gcc - url: https://ftp.mpi-inf.mpg.de/mirrors/gnu/mirror/gcc.gnu.org/pub/gcc/releases/ - pattern: gcc-[0-9]+\.[0-9]+\.[0-9]/ - file: gcc-${version}.tar.gz - - name: gdbm - template: gnu - - name: gettext - template: gnu - - name: glibc - template: gnu - - name: gmp - template: gnu - - name: gperf - template: gnu - - name: grep - template: gnu - - name: groff - template : gnu - - name: grub - template: gnu - - name: gzip - template: gnu - - - name: iana-etc - github: Mic92/iana-etc - - name: inetutils - template: gnu - - - name: mpfr - url: https://www.mpfr.org/mpfr-current/ - pattern: mpfr-[0-9]+\.[0-9]+\.[0-9]+\.tar\.xz - - - name: linux-kernel - url: https://cdn.kernel.org/pub/linux/kernel/v6.x/ - pattern: linux-[0-9]+\.[0-9]+\.[0-9]+\.tar\.xz diff --git a/doc/architecture.md b/doc/architecture.md deleted file mode 100644 index 5989d07..0000000 --- a/doc/architecture.md +++ /dev/null @@ -1,55 +0,0 @@ -# Architecture Erminig - ---- - -## Vue Globale - -Erminig est une forge artisanale en 3 piliers : - -- **Evezh** : Veille logicielle, détection de nouvelles versions. -- **Govel** : Construction et maintenance des paquets à partir des fichiers Pakva. -- **Keo** : Gestion du dépôt de paquets et mise à disposition publique. - -Chaque module est indépendant, mais communique par base de données et sockets. - ---- - -## Détails - -| Module | Rôle | Langage | Communication | -|:-------|:-----|:--------|:---------------| -| Evezh | Check des versions upstream | Python 3.13 | SQLite | -| Govel | Build + révision + création Pakva | Python 3.13 | SQLite, fichiers système | -| Keo | Mirroir de paquets `.bzh` | À venir | SQLite, HTTP (futur) | - ---- - -## Dossiers critiques - -| Dossier | Contenu | -|:--------|:--------| -| `/var/lib/erminig` | Données persistantes (db, Pakva, builds, etc.) | -| `/var/cache/erminig` | Temporaire (compilations, archives, logs) | -| `/opt/erminig` | Sources du projet installées | - ---- - -## Principes - -- **KISS** : Keep it simple, stupid. -- **Séparation stricte** entre utilisateur système (`pak`) et root. -- **Aucune dépendance inutile.** -- **Logs clairs** pour tout ce qui est critique. -- **Architecture modulaire.** - ---- - -# Motto - -> **Un système simple. -> Une forge robuste. -> Un esprit libre.** - ---- - -# FIN diff --git a/doc/bugs.md b/doc/bugs.md deleted file mode 100644 index fa845f8..0000000 --- a/doc/bugs.md +++ /dev/null @@ -1,6 +0,0 @@ -[gcc] Erreur HTTP : HTTPSConnectionPool(host='ftp.mpi-inf.mpg.de', port=443): Max retries exceeded with url: /mirrors/gnu/mirror/gcc.gnu.org/pub/gcc/releases/ (Caused by NameResolutionError(": Failed to resolve 'ftp.mpi-inf.mpg.de' ([Errno -5] No address associated with hostname)")) -[gcc] Aucune version détectée. ------ - - - diff --git a/doc/roadmap.md b/doc/roadmap.md deleted file mode 100644 index cbe5f32..0000000 --- a/doc/roadmap.md +++ /dev/null @@ -1,69 +0,0 @@ -# Roadmap Erminig - ---- - -## 1. Stabilisation du Cœur - -- Finaliser `evezh` (veille versions) -- Finaliser `govel` (build) -- Finaliser `pakva` (formules) -- Finaliser `keo` (mirroir) -- Freeze code sauf bugfixes. - ---- - -## 2. Documentation Interne - -- `/doc/architecture.md` : Décrire les modules. -- `/doc/usage.md` : Exemples de commandes. -- `/doc/rules.md` : Bonnes pratiques de dev (ex : jamais builder en root). - ---- - -## 3. Automatisation - -- Script `evezh sync && govel build --all` -- Script `init-db.sh` pour première installation. - ---- - -## 4. Versionning - -- Commencer à versionner dès premier gel : - - `0.1.0` : Forge fonctionnelle (check + build ok) - - `0.2.0` : Ajout du mirroir Keo - - `0.3.0` : Ajout communication sockets entre modules - - `1.0.0` : Première release publique - ---- - -## 5. Tests Simples - -- Tests manuels à chaque changement critique. -- (Optionnel plus tard : sanity-checks automatiques sur `.Pakva`, db, build). - ---- - -## 6. Releases - -- Utiliser git tags : - - `v0.1.0`, `v0.2.0`, `v0.3.0`, `v1.0.0` -- Archiver les builds, backups réguliers de `/var/lib/erminig` et `/var/cache/erminig`. - ---- - -## 7. Écosystème Ouvert (Optionnel) - -- Documenter comment contribuer un `.Pakva` -- (Peut devenir communautaire si volonté future.) - ---- - -# Mantra - -> **Simple. Robuste. Artisan. Libre.** - ---- - -# Forge On. - diff --git a/doc/specs-pakva.md b/doc/specs-pakva.md deleted file mode 100644 index 972d10f..0000000 --- a/doc/specs-pakva.md +++ /dev/null @@ -1,78 +0,0 @@ -# Specifications du format PAKVA v0.1 - -Le fichier **PAKVA** est un fichier qui définit les spécifications d'un logiciel. Il est utilisé par **evezh** pour -chercher les dernières versions disponibles pour un logiciel et par **govel** pour construire les paquets. -La syntaxe des fichiers **PAKVA** est prévue pour etre lisible en python et en bash. - -Ce document décrit les différentes clés, fonctions et macros disponibles. - -## Structure générale -Les lignes commençant par # sont des commentaires - -la syntaxe est de type clé=valeur (avec ou sans guillemets) -les tableaux sont en style bash clé=(val1 val2 val3) -les fonction sont en style bash fonction(){} - -### Clés disponibles - -**PAKVA** dispose de nombreuses clé disponibles : - -| Clé | Type | Obligatoire | Description | -| :---| :--- | :---------- | :---------- | -| `name` | string | oui | Nom du paquet principal | -| `basename` | string | non | Nom utilisé pour les archives/source/builddir (défaut = $name) | -| `description` | string | non | Description du paquet | -| `packager` | string | non | Packager du paquet | -| `version` | string | oui | Version du logiciel | -| `revision` | int | non | Révision locale | -| `url` | string | non | Page d'accueil du logiciel | -| `source` | array | oui | Sources du paquets | -| `license` | string | non | Licence du logiciel | -| `depends` | array | oui | Dépendances d'utilisation | -| `make_depends` | array | non | Dépendances de construction | -| `check_depends` | array | non | Dépendances de test | - - -### Fonctions disponibles - -| Fonction | Obligatoire | Instructions | -| :------- | :---------- | :---------- | -| `build()` | oui | Compilation | -| `check()` | non | Tests | -| `pak()` | oui | Packaging | -| `pre_install()` | non | Avant installation | -| `post_install()` | non | Après installation | -| `pre_upgrade()` | non | Avant mise à jour | -| `post_upgrade()` | non | Après mise à jour | -| `pre_remove()` | non | Avant suppression | -| `post_remove()` | non | Après suppression | - -### Multi Packaging - -On peut packager plus d'un paquet à partir d'un seul fichier **PAKVA**, et c'est assez simple. - -la syntaxe devient : - -`pak:()` -`pre_install:()` -`post_install:()` -`pre_upgrade:()` -`post_upgrade:()` -`pre_remove:()` -`post_remove:()` - -### Macros disponibles - -| Macro | Description | -| :---- | :---------- | -| `$PAK` | Dossier global d'installation | -| `$SRC` | Dossier d'extraction des sources | -| `$TMP` | Dossier temporaire pour chaque `pak:` | -| `name, $version, $revision, $basename` | Metadonnées du paquet principal | -| `pakname` | nom du paquet courant dans un `pak:` | - - - - - - diff --git a/erminig/cli/__init__.py b/erminig/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/erminig/cli/evezh.py b/erminig/cli/evezh.py deleted file mode 100644 index ac09787..0000000 --- a/erminig/cli/evezh.py +++ /dev/null @@ -1,63 +0,0 @@ -# -# Erminig - Analyse d'arguments pour evezh -# 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 argparse -import json -from erminig.controllers.evezh import check -from erminig.models.db import init_db - - -def main(): - parser = argparse.ArgumentParser(description="Evezh – Veille logicielle artisanale") - subparsers = parser.add_subparsers(dest="command") - - init_parser = subparsers.add_parser("init", help="Initialiser la base de données") - - check_parser = subparsers.add_parser("check") - check_parser.add_argument("--config") - check_parser.add_argument("--output") - check_parser.add_argument("--stdout", action="store_true") - - sync_parser = subparsers.add_parser("sync") - sync_parser.add_argument("--config", required=True) - sync_parser.add_argument("--db", required=True) - - args = parser.parse_args() - - if args.command == "init": - init_db() - - if args.command == "check": - state = check.load_state(args.output) if args.output else {} - results = check.check_versions(args.config, state) - new_state = {} - updated = [] - - for r in results: - name, version, url = r["name"], r["version"], r["url"] - if state.get(name, {}).get("version") != version: - updated.append(r) - new_state[name] = {"version": version, "url": url} - - if args.output: - check.save_state(args.output, new_state) - - if updated: - print(f"\n{len(updated)} mise(s) à jour détectée(s) :") - for r in updated: - print(f" - {r['name']} → {r['version']}") - else: - print("Aucun changement détecté.") - - if args.stdout: - print("\n--- Résultat complet ---") - print(json.dumps(results, indent=2)) - - elif args.command == "sync": - check.sync_db(args.config) diff --git a/erminig/cli/govel.py b/erminig/cli/govel.py deleted file mode 100644 index 2309422..0000000 --- a/erminig/cli/govel.py +++ /dev/null @@ -1,98 +0,0 @@ -# -# Erminig - Analyse d'arguments pour govel -# 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 argparse -from pathlib import Path -from erminig.core.config import Config -from erminig.controllers.govel.pakva import Pakva -from erminig.controllers.govel.build import run_build_function, run_pak_function -from erminig.core.package import Package -from erminig.core.security import run_as_user -import os -import subprocess - - -def main(): - parser = argparse.ArgumentParser(description="Govel – Build artisanal Erminig") - subparsers = parser.add_subparsers(dest="command") - - build_parser = subparsers.add_parser("build") - build_parser.add_argument("--name", help="Nom du paquet à builder") - build_parser.add_argument( - "--force", - action="store_true", - help="Force le build même si le dossier existe déjà", - ) - - new_parser = subparsers.add_parser("new") - new_parser.add_argument("--name", required=True, help="Nom du paquet") - - edit_parser = subparsers.add_parser("edit") - edit_parser.add_argument("--name", required=True, help="Nom du paquet à éditer") - - args = parser.parse_args() - - if args.command == "build": - if args.name: - pakva = Pakva.load_from_name(args.name) - else: - pakva_path = Path.cwd() / "Pakva" - if not pakva_path.exists(): - print("[GOVEL] Erreur : Aucun Pakva trouvé dans le dossier courant.") - return - pakva = Pakva.read(pakva_path) - - pak_success = False - - tmp_path = Path(f"{Config.BUILD_DIR}/{pakva.name}-{pakva.version}") - if tmp_path.exists(): - if not args.force: - print( - f"[GOVEL] Erreur : {tmp_path} existe déjà. Utilisez --force pour écraser." - ) - return - else: - print(f"[GOVEL] Build forcé activé. Suppression de {tmp_path}…") - import shutil - - shutil.rmtree(tmp_path) - - build_success = run_build_function(pakva.path, pakva.name, pakva.version) - if build_success: - pak_success = run_pak_function(pakva.path, pakva.name, pakva.version) - - if pak_success: - tmp_path = f"{Config.BUILD_DIR}/{pakva.name}-{pakva.version}" - pkg = Package(pakva.name, pakva.version, tmp_path) - pkg.generate_manifest() - pkg.write_manifest() - pkg.copy_pakva(pakva.path) - pkg.build_archive() - print(f"[GOVEL] Build réussi pour {pakva.name}") - else: - print(f"[GOVEL] Build échoué pour {pakva.name}") - - elif args.command == "new": - if args.name: - pakva = Pakva(args.name) - pakva.save() - - elif args.command == "edit": - pakva = Pakva.load_from_name(args.name) - open_editor(pakva.path) - - -@run_as_user("pak") -def open_editor(path): - editor = os.getenv("EDITOR", "nvim") - subprocess.run([editor, str(path)]) - - -if __name__ == "__main__": - main() diff --git a/erminig/cli/init.py b/erminig/cli/init.py deleted file mode 100644 index 136a614..0000000 --- a/erminig/cli/init.py +++ /dev/null @@ -1,69 +0,0 @@ -# -# Erminig - Initialisation des utilisateurs et répertoires -# 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 subprocess -import sys -from pathlib import Path -from erminig.core.security import check_root, check_user_exists, run_as_user -from erminig.core.config import Config -from erminig.models.db import init_db - -PAK_USER = Config.PAK_USER - - -def create_user_pak(): - """Crée l'utilisateur pak si nécessaire.""" - if check_user_exists(PAK_USER): - print(f"[INIT] Utilisateur '{PAK_USER}' existe déjà.") - return - - print(f"[INIT] Création de l'utilisateur '{PAK_USER}'...") - subprocess.run( - [ - "useradd", - "-r", - "-d", - str(Config.LIB_DIR), - "-s", - "/usr/sbin/nologin", - PAK_USER, - ], - check=True, - ) - print(f"[INIT] Utilisateur '{PAK_USER}' créé.") - - -def setup_directories(): - """Crée les dossiers nécessaires et assigne les permissions.""" - for directory in [Config.LIB_DIR, Config.CACHE_DIR, Config.BASE_DIR]: - if not directory.exists(): - print(f"[INIT] Création du dossier {directory}...") - directory.mkdir(parents=True, exist_ok=True) - - print(f"[INIT] Attribution de {directory} à '{PAK_USER}'...") - subprocess.run( - ["chown", "-R", f"{PAK_USER}:{PAK_USER}", str(directory)], check=True - ) - - -def main(): - check_root() - - create_user_pak() - setup_directories() - - print("[INIT] Initialisation de la base de données...") - init_db() - - print("[INIT] Environnement Erminig initialisé avec succès.") - - -if __name__ == "__main__": - main() diff --git a/erminig/controllers/__init__.py b/erminig/controllers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/erminig/controllers/evezh/__init__.py b/erminig/controllers/evezh/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/erminig/controllers/evezh/abstract.py b/erminig/controllers/evezh/abstract.py deleted file mode 100644 index 874c3d5..0000000 --- a/erminig/controllers/evezh/abstract.py +++ /dev/null @@ -1,23 +0,0 @@ -# -# Erminig - Classe abstraite pour la récupération des nouvelles version de softs. -# 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. -# - -from abc import ABC, abstractmethod - - -class UpstreamSource(ABC): - MAX_RETRIES = 2 - RETRY_DELAY = 2 # secondes - - def __init__(self, name, config): - self.name = name - self.config = config - - @abstractmethod - def get_latest(self): - pass diff --git a/erminig/controllers/evezh/check.py b/erminig/controllers/evezh/check.py deleted file mode 100644 index 55bf25f..0000000 --- a/erminig/controllers/evezh/check.py +++ /dev/null @@ -1,133 +0,0 @@ -# -# Erminig - Classe globale pour la récupération des dernières versions de softs. -# 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 json -from pathlib import Path -import yaml - -from erminig.controllers.evezh.parsers.github import GitHubSource -from erminig.controllers.evezh.parsers.http import HttpSource -from erminig.controllers.evezh.parsers.sourceforge import SourceForgeRSS -from erminig.handlers.versions import handle_new_version -from erminig.models.db import ErminigDB -from erminig.models import upstreams, versions -from erminig.core.security import run_as_user - - -def load_state(path): - if Path(path).exists(): - return json.loads(Path(path).read_text()) - return {} - - -def save_state(path, state): - Path(path).write_text(json.dumps(state, indent=2)) - - -@run_as_user("pak") -def sync_db(config_path): - config = get_config(config_path) - print(config) - - with ErminigDB() as db: - for proj in config: - name = proj["name"] - type_ = "http" - if "github" in proj: - type_ = "github" - url = proj["github"] - elif "sourceforge" in proj: - type_ = "sourceforge" - url = proj["sourceforge"] - else: - url = proj.get("url", "") - - pattern = proj.get("pattern", "") - file = proj.get("file", None) - upstreams.upsert_upstream(db, name, type_, url, pattern, file) - - print("Synchronisation terminée.") - - -def make_source(name, config): - if "github" in config: - return GitHubSource(name, config) - elif "sourceforge" in config: - return SourceForgeRSS(name, config) - else: - return HttpSource(name, config) - - -def apply_template(name, template_name, templates): - tpl = templates.get(template_name) - if not tpl: - raise ValueError(f"Template '{template_name}' non trouvé.") - return { - "url": tpl["url"].replace("@NAME@", name), - "pattern": tpl["pattern"].replace("@NAME@", name), - } - - -def get_config(path): - with open(path) as f: - full = yaml.safe_load(f) - - templates = full.get("templates", {}) - projects = full.get("projects", []) - - resolved = [] - - for proj in projects: - name = proj["name"] - new_proj = proj.copy() - - if "template" in proj: - tpl = apply_template(name, proj["template"], templates) - new_proj["url"] = tpl["url"] - new_proj["pattern"] = tpl["pattern"] - - resolved.append(new_proj) - - return resolved - - -@run_as_user("pak") -def check_versions(config_path, state=None): - results = [] - with ErminigDB() as db: - for row in upstreams.get_all_upstreams(db): - proj = dict(row) - if proj["type"] == "github": - proj["github"] = proj["url"] - elif proj["type"] == "sourceforge": - proj["sourceforge"] = proj["url"] - else: - proj["url"] = proj["url"] - upstream_id = proj["id"] - name = proj["name"] - source = make_source(name, proj) - latest = source.get_latest() - if latest: - results.append(latest) - version = latest["version"] - url = latest["url"] - handle_new_version(db, upstream_id, name, version, url) - elif state and name in state: - print(f"[{name}] Serveur HS. On garde l’ancienne version.") - results.append( - { - "name": name, - "version": state[name]["version"], - "url": state[name]["url"], - } - ) - else: - print(f"[{name}] Aucune version détectée.") - - return results diff --git a/erminig/controllers/evezh/parsers/__init__.py b/erminig/controllers/evezh/parsers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/erminig/controllers/evezh/parsers/github.py b/erminig/controllers/evezh/parsers/github.py deleted file mode 100644 index e6e029f..0000000 --- a/erminig/controllers/evezh/parsers/github.py +++ /dev/null @@ -1,85 +0,0 @@ -# -# Erminig - Récupération de la dernière version d'un soft sur Github via son API -# 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 re -import requests -from erminig.core.config import Config -from erminig.controllers.evezh.abstract import UpstreamSource -from erminig.core.retry import retry_on_failure - - -class GitHubSource(UpstreamSource): - - @retry_on_failure() - def get_latest(self): - repo = self.config["github"] - file_template = self.config.get("file") - - latest = self._get_latest_release(repo) - if not latest: - latest = self._get_latest_tag(repo) - if not latest: - print(f"[{self.name}] Aucune version détectée.") - return None - - version = self.normalize_version(latest["tag"]) - url = latest.get("url") - - if not url and file_template: - filename = file_template.replace("${version}", version) - url = f"https://github.com/{repo}/releases/download/{latest['tag']}/{filename}" - print(f"[{self.name}] Fallback URL : {url}") - - print(url) - return {"name": self.name, "version": version, "url": url or ""} - - def _github_headers(self): - headers = {} - if Config.GITHUB_TOKEN: - headers["Authorization"] = f"token {Config.GITHUB_TOKEN}" - return headers - - def _get_latest_release(self, repo): - r = requests.get( - f"https://api.github.com/repos/{repo}/releases", - headers=self._github_headers(), - ) - r.raise_for_status() - data = r.json() - if not data: - return None - - release = data[0] - for asset in release.get("assets", []): - if asset["browser_download_url"].endswith(".tar.gz"): - return { - "tag": release["tag_name"], - "url": asset["browser_download_url"], - } - return {"tag": release["tag_name"]} - - def _get_latest_tag(self, repo): - r = requests.get( - f"https://api.github.com/repos/{repo}/tags", headers=self._github_headers() - ) - r.raise_for_status() - tags = r.json() - if not tags: - return None - url = f"https://github.com/{repo}/archive/refs/tags/{tags[0]["name"]}.tar.gz" - return {"tag": tags[0]["name"], "url": url} - - def normalize_version(self, tag): - # Exemples : v2.7.1 → 2.7.1, R_2_7_1 → 2.7.1, expat-2.7.1 → 2.7.1 - tag = tag.strip() - if tag.lower().startswith(("v", "r_", "r")): - tag = re.sub(r"^[vVrR_]+", "", tag) - tag = tag.replace("_", ".") - match = re.search(r"(\d+\.\d+(?:\.\d+)?)", tag) - return match.group(1) if match else tag diff --git a/erminig/controllers/evezh/parsers/http.py b/erminig/controllers/evezh/parsers/http.py deleted file mode 100644 index ec13ba7..0000000 --- a/erminig/controllers/evezh/parsers/http.py +++ /dev/null @@ -1,54 +0,0 @@ -# -# Erminig - Recupération de la dernière version d'un soft sur page html -# 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 re -import requests -from erminig.controllers.evezh.abstract import UpstreamSource -from erminig.core.retry import retry_on_failure - - -class HttpSource(UpstreamSource): - - @retry_on_failure() - def get_latest(self): - base_url = self.config["url"] - pattern = self.config["pattern"] - file_pattern = self.config.get("file") - - response = requests.get( - base_url, timeout=10 - ) # timeout pour éviter de bloquer éternellement - response.raise_for_status() - html = response.text - - matches = re.findall(pattern, html) - if not matches: - print(f"[{self.name}] Aucun match avec pattern : {pattern}") - return None - - latest_dir = sorted(set(matches), key=self.version_key)[-1] - version = self.extract_version(latest_dir) - - if file_pattern: - filename = file_pattern.replace("${version}", version) - url = f"{base_url}{latest_dir}{filename}" - else: - url = f"{base_url}{latest_dir}" - - print(url) - - return {"name": self.name, "version": version, "url": url} - - def extract_version(self, path): - match = re.search(r"([0-9]+\.[0-9]+(?:\.[0-9]+)?)", path) - return match.group(1) if match else path - - def version_key(self, ver): - nums = re.findall(r"\d+", ver) - return list(map(int, nums)) diff --git a/erminig/controllers/evezh/parsers/sourceforge.py b/erminig/controllers/evezh/parsers/sourceforge.py deleted file mode 100644 index f711af9..0000000 --- a/erminig/controllers/evezh/parsers/sourceforge.py +++ /dev/null @@ -1,42 +0,0 @@ -# -# Erminig - Recupération de la dernière version d'un soft sur page Sourceforge -# 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 re -import requests -import xml.etree.ElementTree as ET -from erminig.controllers.evezh.abstract import UpstreamSource -from erminig.core.retry import retry_on_failure - - -class SourceForgeRSS(UpstreamSource): - - @retry_on_failure() - def get_latest(self): - rss_url = self.config["sourceforge"] - r = requests.get(rss_url, timeout=10) - r.raise_for_status() - root = ET.fromstring(r.text) - items = root.findall(".//item") - - versions = [] - for item in items: - title = item.findtext("title") or "" - match = re.search(r"([0-9]+\.[0-9]+(?:\.[0-9]+)?)", title) - if match: - version = match.group(1) - link = item.findtext("link") - versions.append((version, link)) - - if not versions: - print(f"[{self.name}] Aucune version trouvée via RSS.") - return None - - latest = sorted(versions, key=lambda x: list(map(int, x[0].split("."))))[-1] - print(latest[1]) - return {"name": self.name, "version": latest[0], "url": latest[1]} diff --git a/erminig/controllers/govel/__init__.py b/erminig/controllers/govel/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/erminig/controllers/govel/build.py b/erminig/controllers/govel/build.py deleted file mode 100644 index a9ac854..0000000 --- a/erminig/controllers/govel/build.py +++ /dev/null @@ -1,63 +0,0 @@ -# -# Erminig - Lancement de la construction du paquet -# 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 subprocess -from erminig.core.config import Config -from erminig.core.security import check_root, check_user_exists, run_as_user - -check_root -check_user_exists("pak") - - -def run_pakva_function(pakva_path, name, version, func_name): - """ - Exécute une fonction d’un fichier Pakva donné (ex: build, pak). - """ - build_root = Config.BUILD_DIR / f"{name}-{version}" - src_dir = build_root / "src" - tmp_dir = build_root / "tmp" - os.makedirs(src_dir, exist_ok=True) - os.makedirs(tmp_dir, exist_ok=True) - - env = os.environ.copy() - env["SRC"] = str(src_dir) - env["TMP"] = str(tmp_dir) - - try: - result = subprocess.run( - f""" - set -e - source "{pakva_path}" - {func_name} - """, - shell=True, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - executable="/bin/bash", - text=True, - env=env, - ) - print(f"[{func_name.upper()}] Succès : {pakva_path.name}") - print(result.stdout) - return True - except subprocess.CalledProcessError as e: - print(f"[{func_name.upper()}] Échec : {pakva_path.name}") - print(e.stderr) - return False - - -@run_as_user("pak") -def run_build_function(pakva_path, name, version): - return run_pakva_function(pakva_path, name, version, "build") - - -def run_pak_function(pakva_path, name, version): - return run_pakva_function(pakva_path, name, version, "pak") diff --git a/erminig/controllers/govel/pakva.py b/erminig/controllers/govel/pakva.py deleted file mode 100644 index a4d02cb..0000000 --- a/erminig/controllers/govel/pakva.py +++ /dev/null @@ -1,120 +0,0 @@ -# -# Erminig - Création et mise à jour de la révision des fichiers pakva -# 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. -# - -from pathlib import Path -from erminig.core.config import Config -from erminig.core.security import run_as_user - - -class Pakva: - - def __init__(self, name, version=None, archive=None): - self.name = name - self.version = version - self.archive = archive - self.path = Config.GOVEL_DIR / name[0] / name / "Pakva" - - @run_as_user("pak") - def save(self): - self.path.parent.mkdir(parents=True, exist_ok=True) - with open(self.path, "w") as f: - f.write( - f"""\ -name="{self.name}" -version="{self.version}" -revision=1 -source=("{self.archive}") - -build() {{ - echo "Build function not implemented" -}} - -pak() {{ - echo "Packaging function not implemented" -}} -""" - ) - - @run_as_user("pak") - def update_version(self, version, archive, reset_revision=False): - if not self.path.exists(): - raise FileNotFoundError(f"Aucun fichier Pakva pour {self.name}") - - with open(self.path, "r") as f: - lines = f.readlines() - - new_lines = [] - updated_version = False - updated_source = False - - for line in lines: - if line.startswith("version=") and not updated_version: - new_lines.append(f'version="{version}"\n') - updated_version = True - elif line.startswith("source=") and not updated_source: - new_lines.append(f'source=("{archive}")\n') - updated_source = True - elif reset_revision and line.startswith("revision="): - new_lines.append("revision=1\n") - else: - new_lines.append(line) - - # Ajout si jamais les champs n'existaient pas - if not updated_version: - new_lines.insert(1, f'version="{version}"\n') - if not updated_source: - new_lines.append(f'source=("{archive}")\n') - if reset_revision and not any(line.startswith("revision=") for line in lines): - new_lines.insert(2, "revision=1\n") - - with open(self.path, "w") as f: - f.writelines(new_lines) - - self.version = version - self.archive = archive - print(f"[Pakva] Version mise à jour pour {self.name} -> {version}") - - @classmethod - def read(cls, path): - """ - Lit un fichier Pakva et retourne un objet Pakva. - """ - path = Path(path) - if not path.exists(): - raise FileNotFoundError(f"Fichier Pakva introuvable : {path}") - - with path.open() as f: - lines = f.readlines() - - name = None - version = None - archive = None - - for line in lines: - if line.startswith("name="): - name = line.split("=")[1].strip().strip('"') - elif line.startswith("version="): - version = line.split("=")[1].strip().strip('"') - elif line.startswith("source=("): - archive = line.split("(")[1].split(")")[0].strip().strip('"') - - obj = cls(name, version, archive) - obj.path = path - return obj - - def __repr__(self): - return f"" - - @classmethod - def load_from_name(cls, name): - """ - Charge un fichier Pakva depuis son nom uniquement. - """ - path = Config.GOVEL_DIR / name[0] / name / "Pakva" - return cls.read(path) diff --git a/erminig/core/__init__.py b/erminig/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/erminig/core/config.py b/erminig/core/config.py deleted file mode 100644 index 0090cc4..0000000 --- a/erminig/core/config.py +++ /dev/null @@ -1,28 +0,0 @@ -# -# Erminig - Configuration de l'application -# 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 -from pathlib import Path - - -class Config: - LIB_DIR = Path("/var/lib/erminig") - CACHE_DIR = Path("/var/cache/erminig") - BASE_DIR = Path("/opt/erminig") - DB_PATH = LIB_DIR / "erminig.db" - PAKVA_DIR = LIB_DIR / "pakva" - GOVEL_DIR = LIB_DIR / "govel" - REPO_DIR = LIB_DIR / "keo" - BUILD_DIR = Path("/tmp/erminig/build") - PAK_USER = "pak" - - GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") - - RETRY_MAX_ATTEMPTS = 2 # Pour toutes les opérations réseau - RETRY_DELAY_SECONDS = 2 diff --git a/erminig/core/package.py b/erminig/core/package.py deleted file mode 100644 index 7a45837..0000000 --- a/erminig/core/package.py +++ /dev/null @@ -1,105 +0,0 @@ -# -# Erminig - Fonctions de base pour les paquets -# 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 hashlib -import tarfile -import tempfile -import shutil -import pwd -import grp -from pathlib import Path -from erminig.core.config import Config - - -class Package: - def __init__(self, name, version, tmp_dir): - self.name = name - self.version = version - self.tmp_dir = Path(tmp_dir) - self.manifest = [] - - def generate_manifest(self): - install_root = self.tmp_dir / "tmp" - for path in install_root.rglob("*"): - rel_path = Path("/") / path.relative_to(install_root) - stat = path.lstat() - user = pwd.getpwuid(stat.st_uid).pw_name - group = grp.getgrgid(stat.st_gid).gr_name - - if path.is_file(): - md5 = hashlib.md5(path.read_bytes()).hexdigest() - self.manifest.append( - { - "path": str(rel_path), - "md5": md5, - "mode": oct(stat.st_mode & 0o777), - "user": user, - "group": group, - "type": "file", - } - ) - elif path.is_dir(): - self.manifest.append( - { - "path": str(rel_path), - "md5": "-", # pas de hash pour les dossiers - "mode": oct(stat.st_mode & 0o777), - "user": user, - "group": group, - "type": "dir", - } - ) - - def write_manifest(self): - manifest_path = self.tmp_dir / "MANIFEST" - with open(manifest_path, "w") as f: - for entry in self.manifest: - f.write( - f"{entry['type']} {entry['path']} {entry['md5']} {entry['mode']} {entry['user']} {entry['group']}\n" - ) - - def copy_pakva(self, pakva_path): - dest = self.tmp_dir / "Pakva" - content = Path(pakva_path).read_text() - dest.write_text(content) - - # Dans ta classe Package - - def build_archive(self): - temp_dir = tempfile.mkdtemp() - temp_path = Path(temp_dir) - - # Copie les fichiers Pakva et MANIFEST dans le répertoire temporaire - shutil.copy(self.tmp_dir / "Pakva", temp_path / "Pakva") - shutil.copy(self.tmp_dir / "MANIFEST", temp_path / "MANIFEST") - - # Crée le dossier Files et copie dedans UNIQUEMENT le contenu de $TMP - files_dir = temp_path / "Files" - files_dir.mkdir() - - for item in (self.tmp_dir).iterdir(): - if item.name in ["Pakva", "MANIFEST"]: - continue - install_dir = self.tmp_dir / "tmp" - if install_dir.exists(): - for item in install_dir.iterdir(): - dest = files_dir / item.name - if item.is_dir(): - shutil.copytree(item, dest) - else: - shutil.copy2(item, dest) - # Crée l'archive .bzh sans inclure les dossiers de travail - archive_path = Config.BUILD_DIR / f"{self.name}-{self.version}.bzh" - with tarfile.open(archive_path, "w|xz") as tar: - tar.add(temp_path / "Pakva", arcname="Pakva") - tar.add(temp_path / "MANIFEST", arcname="MANIFEST") - tar.add(files_dir, arcname="Files") - - shutil.rmtree(temp_path) - return archive_path diff --git a/erminig/core/retry.py b/erminig/core/retry.py deleted file mode 100644 index cc0c02b..0000000 --- a/erminig/core/retry.py +++ /dev/null @@ -1,44 +0,0 @@ -# -# Erminig - Décorateur pour relancer un téléchargement -# 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 time -import requests -from erminig.core.config import Config - - -def retry_on_failure(): - def decorator(func): - def wrapper(*args, **kwargs): - attempt = 1 - while attempt <= Config.RETRY_MAX_ATTEMPTS + 1: - try: - return func(*args, **kwargs) - except requests.exceptions.RequestException as e: - name = getattr(args[0], "name", "Unknown") - print( - f"[{name}] Erreur réseau tentative {attempt}: {e.__class__.__name__}" - ) - if attempt <= Config.RETRY_MAX_ATTEMPTS: - print( - f"[{name}] Nouvelle tentative dans {Config.RETRY_DELAY_SECONDS}s..." - ) - time.sleep(Config.RETRY_DELAY_SECONDS) - attempt += 1 - else: - print(f"[{name}] Abandon après {attempt} tentatives.") - return None - except Exception as e: - name = getattr(args[0], "name", "Unknown") - print(f"[{name}] Erreur inconnue dans {func.__name__}: {e}") - return None - - return wrapper - - return decorator diff --git a/erminig/core/security.py b/erminig/core/security.py deleted file mode 100644 index c5dd51a..0000000 --- a/erminig/core/security.py +++ /dev/null @@ -1,69 +0,0 @@ -# -# Erminig - Fonctions pour gérer les utilisateurs système. -# 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 pwd -import sys -import functools - - -def check_root(): - """Vérifie si on est root, sinon quitte.""" - if os.geteuid() != 0: - print("[SECURITY] Ce programme doit être exécuté en tant que root.") - sys.exit(1) - - -def check_user_exists(username): - """Vérifie si l'utilisateur spécifié existe.""" - try: - pwd.getpwnam(username) - return True - except KeyError: - print(f"[SECURITY] Utilisateur '{username}' introuvable.") - return False - - -def run_as_user(username): - """Décorateur : Fork et drop privileges pour exécuter une fonction sous un autre utilisateur.""" - - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - try: - pid = os.fork() - if pid > 0: - # Parent : attendre le child, ne pas exit, juste return proprement - _, status = os.waitpid(pid, 0) - return ( - status >> 8 - ) # récupère le code retour du fils (comme exit code) - - # Child - pw_record = pwd.getpwnam(username) - user_uid = pw_record.pw_uid - user_gid = pw_record.pw_gid - - os.setgid(user_gid) - os.setuid(user_uid) - - os.environ["HOME"] = pw_record.pw_dir - os.environ["LOGNAME"] = pw_record.pw_name - os.environ["USER"] = pw_record.pw_name - - result = func(*args, **kwargs) - os._exit(0 if result is None else int(bool(result))) - - except OSError as e: - print(f"[SECURITY] Fork échoué : {e}") - os._exit(1) - - return wrapper - - return decorator diff --git a/erminig/handlers/__init__.py b/erminig/handlers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/erminig/handlers/versions.py b/erminig/handlers/versions.py deleted file mode 100644 index 8e53fd0..0000000 --- a/erminig/handlers/versions.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Erminig - Rentre la nouvelle version d'un soft dans la base de données -# 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. -# - -from erminig.models import versions -from erminig.controllers.govel.pakva import Pakva - - -def handle_new_version(db, upstream_id, name, version, url): - """ - Gère l'arrivée d'une nouvelle version : - - Insère en base si absente - - Crée ou met à jour le fichier Pakva correspondant - """ - inserted = versions.insert_version_if_new(db, upstream_id, version, url) - if not inserted: - return False - - pakva = Pakva(name, version, url) - if pakva.path.exists(): - pakva.update_version(version, url, reset_revision=True) - print(f"[PAKVA] Mis à jour pour {name}") - else: - pakva.save() - print(f"[PAKVA] Créé pour {name}") - - return True diff --git a/erminig/models/__init__.py b/erminig/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/erminig/models/db.py b/erminig/models/db.py deleted file mode 100644 index 1de4382..0000000 --- a/erminig/models/db.py +++ /dev/null @@ -1,43 +0,0 @@ -# -# Erminig - initialise la base 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 os -import pwd -import sqlite3 -from erminig.core.config import Config - - -def init_db(): - if Config.DB_PATH.exists(): - return - - conn = sqlite3.connect(Config.DB_PATH) - with open(Config.BASE_DIR / "schema.sql", "r") as f: - conn.executescript(f.read()) - conn.commit() - conn.close() - # Attribution au user pak - pak_uid = pwd.getpwnam("pak").pw_uid - pak_gid = pwd.getpwnam("pak").pw_gid - os.chown(Config.DB_PATH, pak_uid, pak_gid) - os.chmod(Config.DB_PATH, 0o664) - - print("Base erminig.db initialisée avec succès.") - - -class ErminigDB: - def __enter__(self): - self.conn = sqlite3.connect(Config.DB_PATH) - self.conn.row_factory = sqlite3.Row - self.cursor = self.conn.cursor() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.conn.commit() - self.conn.close() diff --git a/erminig/models/upstreams.py b/erminig/models/upstreams.py deleted file mode 100644 index e013afb..0000000 --- a/erminig/models/upstreams.py +++ /dev/null @@ -1,38 +0,0 @@ -# -# Erminig - Fonctions relatives à la table upstreams -# 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. -# - -from erminig.models.db import ErminigDB - - -def upsert_upstream(db: ErminigDB, name, type_, url, pattern, file): - db.cursor.execute( - """ - INSERT INTO upstreams (name, type, url, pattern, file, created_at) - VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) - ON CONFLICT(name) DO UPDATE SET - type=excluded.type, - url=excluded.url, - pattern=excluded.pattern, - file=excluded.file - RETURNING id - """, - (name, type_, url, pattern, file), - ) - row = db.cursor.fetchone() - return row[0] if row else None - - -def get_all_upstreams(db: ErminigDB): - db.cursor.execute( - """ - SELECT id, name, type, url, pattern, file FROM upstreams - """ - ) - rows = db.cursor.fetchall() - return rows diff --git a/erminig/models/versions.py b/erminig/models/versions.py deleted file mode 100644 index 07bf4e0..0000000 --- a/erminig/models/versions.py +++ /dev/null @@ -1,34 +0,0 @@ -# -# Erminig - Fonctions relatives à la table versions -# 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. -# - -from erminig.models.db import ErminigDB - - -def insert_version_if_new(db, upstream_id, version, url): - db.cursor.execute( - """ - SELECT id FROM versions - WHERE upstream_id = ? AND version = ? - """, - (upstream_id, version), - ) - - if db.cursor.fetchone(): - return False # déjà en base - - db.cursor.execute( - """ - INSERT INTO versions (upstream_id, version, url) - VALUES (?, ?, ?) - """, - (upstream_id, version, url), - ) - - print(f"[DB] Nouvelle version insérée pour {upstream_id} : {version}") - return True diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index bd115a2..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,27 +0,0 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "erminig" -version = "0.0.1" -description = "Erminig: Une forge logicielle bretonne artisanale" -authors = [{ name = "Lomig", email = "lomig@example.com" }] -readme = "README.md" -license = "MIT" -requires-python = ">=3.13" -dependencies = ["requests", "PyYAML", "pytest"] - -[tool.setuptools] -packages = [ - "erminig", - "erminig.cli", - "erminig.controllers.evezh", - "erminig.controllers.evezh.parsers", - "erminig.models", -] - -[project.scripts] -erminit-init = "erminig.cli.init:main" -evezh = "erminig.cli.evezh:main" -govel = "erminig.cli.govel:main" diff --git a/schema.sql b/schema.sql index c745680..b10562e 100644 --- a/schema.sql +++ b/schema.sql @@ -1,68 +1,37 @@ -CREATE TABLE IF NOT EXISTS upstreams ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - type TEXT CHECK(type IN ('http', 'github', 'sourceforge')) NOT NULL, - url TEXT, -- Base URL ou GitHub repo ou flux RSS - pattern TEXT DEFAULT NULL, -- Regex pour HTTP ou pattern GitHub si besoin - file TEXT, -- Optionnel : fichier final ex: gcc-${version}.tar.xz - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +-- 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 IF NOT EXISTS versions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - upstream_id INTEGER NOT NULL, - version TEXT, +CREATE TABLE sources ( + package_name TEXT, url TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (upstream_id) REFERENCES upstreams(id) + hash TEXT, + FOREIGN KEY (package_name) REFERENCES packages(name) ); -CREATE TABLE IF NOT EXISTS packages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE, - upstream_id INTEGER, - current_version_id INTEGER, - FOREIGN KEY (upstream_id) REFERENCES upstreams(id), - FOREIGN KEY (current_version_id) REFERENCES versions(id) +CREATE TABLE dependencies ( + package_name TEXT, + dependency TEXT, + type TEXT, -- build, runtime, check + FOREIGN KEY (package_name) REFERENCES packages(name) ); -CREATE TABLE IF NOT EXISTS depends ( +CREATE TABLE builds ( id INTEGER PRIMARY KEY AUTOINCREMENT, - package_id INTEGER NOT NULL, - depends_on INTEGER NOT NULL, - type TEXT CHECK(type IN ('runtime', 'build', 'check')), - FOREIGN KEY (package_id) REFERENCES packages(id), - FOREIGN KEY (depends_on) REFERENCES packages(id) + 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) ); - -CREATE TABLE IF NOT EXISTS builds ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - package_id INTEGER NOT NULL, - version TEXT NOT NULL, - revision TEXT NOT NULL, - status TEXT CHECK(status IN ('success', 'fail', 'pending')), - built_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - log_path TEXT, - FOREIGN KEY (package_id) REFERENCES packages(id) -); - -CREATE TABLE IF NOT EXISTS repo ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - package_id INTEGER NOT NULL, - version TEXT NOT NULL, - path TEXT, - added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (package_id) REFERENCES packages(id) -); - -CREATE TABLE IF NOT EXISTS notifications ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - version TEXT NOT NULL, - url TEXT NOT NULL, - status TEXT DEFAULT 'pending', -- 'pending', 'sent', 'failed' - last_attempt TIMESTAMP, -- Pour savoir quand on a tenté la dernière fois - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE UNIQUE INDEX idx_upstream_version ON versions(upstream_id, version); \ No newline at end of file diff --git a/erminig/__init__.py b/src/erminig/common/__init__.py similarity index 100% rename from erminig/__init__.py rename to src/erminig/common/__init__.py 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() diff --git a/tests/test_config.py b/tests/test_config.py deleted file mode 100644 index 06638dd..0000000 --- a/tests/test_config.py +++ /dev/null @@ -1,14 +0,0 @@ -# -# Erminig - Tests relatifs à la bse de données -# 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. -# - -from erminig.core.config import Config - - -def test_db_path(): - assert str(Config.DB_PATH).endswith("erminig.db") diff --git a/tests/test_pakva.py b/tests/test_pakva.py deleted file mode 100644 index 1f7ab25..0000000 --- a/tests/test_pakva.py +++ /dev/null @@ -1,29 +0,0 @@ -# -# Erminig - Tests relatifs aux fichiers Pakva -# 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 pytest -from pathlib import Path -from unittest.mock import patch - - -# PATCH directement le décorateur run_as_user pour les tests -@patch("erminig.core.security.run_as_user", lambda x=None: (lambda f: f)) -def test_pakva_save_and_read(tmp_path): - from erminig.controllers.govel.pakva import Pakva # Importer après patch ! - - pakva_path = tmp_path / "Pakva" - - pakva = Pakva("testpkg", "1.0.0", "http://example.com/testpkg.tar.gz") - pakva.path = pakva_path - pakva.save() - - loaded = Pakva.read(pakva_path) - assert loaded.name == "testpkg" - assert loaded.version == "1.0.0" - assert loaded.archive == "http://example.com/testpkg.tar.gz"