From c63f62721b5802100be020ad718bb385f51d7eb6 Mon Sep 17 00:00:00 2001 From: L0m1g Date: Tue, 29 Apr 2025 17:15:19 +0200 Subject: [PATCH 01/12] =?UTF-8?q?Les=20choses=20s=C3=A9rieuses=20commencen?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 + Dockerfile | 10 ++ Makefile | 28 ++++ data/config.yaml | 100 ++++++++++++++ dev.sh | 4 + doc/architecture.md | 55 ++++++++ doc/bugs.md | 6 + doc/roadmap.md | 69 ++++++++++ doc/specs-pakva.md | 78 +++++++++++ docker-compose.yml | 10 ++ erminig/__init__.py | 0 erminig/cli/__init__.py | 0 erminig/cli/evezh.py | 54 ++++++++ erminig/cli/govel.py | 37 ++++++ erminig/cli/init.py | 56 ++++++++ erminig/config.py | 18 +++ erminig/controllers/__init__.py | 0 erminig/controllers/evezh/__init__.py | 0 erminig/controllers/evezh/abstract.py | 14 ++ erminig/controllers/evezh/check.py | 122 ++++++++++++++++++ erminig/controllers/evezh/parsers/__init__.py | 0 erminig/controllers/evezh/parsers/github.py | 76 +++++++++++ erminig/controllers/evezh/parsers/http.py | 45 +++++++ .../controllers/evezh/parsers/sourceforge.py | 33 +++++ erminig/controllers/govel/__init__.py | 0 erminig/controllers/govel/build.py | 33 +++++ erminig/controllers/govel/pakva.py | 103 +++++++++++++++ erminig/handlers/__init__.py | 0 erminig/handlers/versions.py | 23 ++++ erminig/models/__init__.py | 0 erminig/models/db.py | 26 ++++ erminig/models/upstreams.py | 29 +++++ erminig/models/versions.py | 25 ++++ erminig/system/__init__.py | 0 erminig/system/retry.py | 34 +++++ erminig/system/security.py | 55 ++++++++ lib/erminig.db | Bin 0 -> 49152 bytes pyproject.toml | 27 ++++ schema.sql | 68 ++++++++++ tests/test_config.py | 5 + tests/test_pakva.py | 20 +++ 41 files changed, 1270 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 data/config.yaml create mode 100755 dev.sh create mode 100644 doc/architecture.md create mode 100644 doc/bugs.md create mode 100644 doc/roadmap.md create mode 100644 doc/specs-pakva.md create mode 100644 docker-compose.yml create mode 100644 erminig/__init__.py create mode 100644 erminig/cli/__init__.py create mode 100644 erminig/cli/evezh.py create mode 100644 erminig/cli/govel.py create mode 100644 erminig/cli/init.py create mode 100644 erminig/config.py create mode 100644 erminig/controllers/__init__.py create mode 100644 erminig/controllers/evezh/__init__.py create mode 100644 erminig/controllers/evezh/abstract.py create mode 100644 erminig/controllers/evezh/check.py create mode 100644 erminig/controllers/evezh/parsers/__init__.py create mode 100644 erminig/controllers/evezh/parsers/github.py create mode 100644 erminig/controllers/evezh/parsers/http.py create mode 100644 erminig/controllers/evezh/parsers/sourceforge.py create mode 100644 erminig/controllers/govel/__init__.py create mode 100644 erminig/controllers/govel/build.py create mode 100644 erminig/controllers/govel/pakva.py create mode 100644 erminig/handlers/__init__.py create mode 100644 erminig/handlers/versions.py create mode 100644 erminig/models/__init__.py create mode 100644 erminig/models/db.py create mode 100644 erminig/models/upstreams.py create mode 100644 erminig/models/versions.py create mode 100644 erminig/system/__init__.py create mode 100644 erminig/system/retry.py create mode 100644 erminig/system/security.py create mode 100644 lib/erminig.db create mode 100644 pyproject.toml create mode 100644 schema.sql create mode 100644 tests/test_config.py create mode 100644 tests/test_pakva.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f81dd8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +*.egg-info/ +.pytest_cache/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..508e3c8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM fedora:42 + +RUN dnf -y update && \ + dnf -y install python3 python3-pip sqlite tar zstd git bash && \ + dnf clean all + +RUN mkdir -p /var/lib/erminig /var/cache/erminig /opt/erminig + +COPY . /opt/erminig + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4ec82b9 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +.PHONY: install test fmt docker-build up rebuild clean + +install: + pip install -e .[dev] + +test: + pytest -v + +fmt: + black erminig tests + +docker-build: + podman-compose -f docker-compose.yml build + +up: + podman-compose -f docker-compose.yml run erminig + +rebuild: docker-build up + +clean: + find . -name "*.pyc" -delete + find . -name "__pycache__" -type d -exec rm -rf {} + + find . -name "*.egg-info" -type d -exec rm -rf {} + + find . -name ".pytest_cache" -type d -exec rm -rf {} + + +build-all: fmt test docker-build + + diff --git a/data/config.yaml b/data/config.yaml new file mode 100644 index 0000000..9b836bf --- /dev/null +++ b/data/config.yaml @@ -0,0 +1,100 @@ +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/dev.sh b/dev.sh new file mode 100755 index 0000000..9162fd8 --- /dev/null +++ b/dev.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "🌊 [Erminig] Rebuild and launch dev env..." +podman-compose -f docker-compose.yml build && \ +podman-compose -f docker-compose.yml run erminig \ No newline at end of file diff --git a/doc/architecture.md b/doc/architecture.md new file mode 100644 index 0000000..5989d07 --- /dev/null +++ b/doc/architecture.md @@ -0,0 +1,55 @@ +# 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 new file mode 100644 index 0000000..fa845f8 --- /dev/null +++ b/doc/bugs.md @@ -0,0 +1,6 @@ +[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 new file mode 100644 index 0000000..cbe5f32 --- /dev/null +++ b/doc/roadmap.md @@ -0,0 +1,69 @@ +# 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 new file mode 100644 index 0000000..972d10f --- /dev/null +++ b/doc/specs-pakva.md @@ -0,0 +1,78 @@ +# 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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..afb26da --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.9' +services: + erminig: + build: + context: . + volumes: + - ./lib:/var/lib/erminig:z + - ./cache:/var/cache/erminig:z + working_dir: /opt/erminig + command: /bin/bash -c "pip install -e . && exec /bin/bash" diff --git a/erminig/__init__.py b/erminig/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/erminig/cli/__init__.py b/erminig/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/erminig/cli/evezh.py b/erminig/cli/evezh.py new file mode 100644 index 0000000..150014b --- /dev/null +++ b/erminig/cli/evezh.py @@ -0,0 +1,54 @@ +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 new file mode 100644 index 0000000..6a2956a --- /dev/null +++ b/erminig/cli/govel.py @@ -0,0 +1,37 @@ +import argparse +from erminig.config import Config +from erminig.controllers.govel.pakva import Pakva +from erminig.controllers.govel.build import run_build_function + + +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") + + args = parser.parse_args() + + if args.command == "build": + if args.name: + pakva = Pakva(name=args.name, version=None, archive=None) + pakva.read() + else: + if not Config.PAKVA_DIR.exists(): + print("[GOVEL] Erreur : Aucun Pakva trouvé ici.") + return + pakva = Pakva(name="local", version=None, archive=None) + pakva.path = Config.PAKVA_DIR + pakva.read() + + build_success = run_build_function(pakva.path) + + if build_success: + print(f"[GOVEL] Build réussi pour {pakva.name}") + else: + print(f"[GOVEL] Build échoué pour {pakva.name}") + + +if __name__ == "__main__": + main() diff --git a/erminig/cli/init.py b/erminig/cli/init.py new file mode 100644 index 0000000..7111dac --- /dev/null +++ b/erminig/cli/init.py @@ -0,0 +1,56 @@ +import os +import subprocess +import sys +from pathlib import Path +from erminig.system.security import check_root, check_user_exists +from erminig.config import Config # Voilà la différence clé ! + +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]: + 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] Environnement Erminig initialisé avec succès.") + + +if __name__ == "__main__": + main() diff --git a/erminig/config.py b/erminig/config.py new file mode 100644 index 0000000..1644f41 --- /dev/null +++ b/erminig/config.py @@ -0,0 +1,18 @@ +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" + 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/controllers/__init__.py b/erminig/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/erminig/controllers/evezh/__init__.py b/erminig/controllers/evezh/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/erminig/controllers/evezh/abstract.py b/erminig/controllers/evezh/abstract.py new file mode 100644 index 0000000..e86a03b --- /dev/null +++ b/erminig/controllers/evezh/abstract.py @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..b6ff1b8 --- /dev/null +++ b/erminig/controllers/evezh/check.py @@ -0,0 +1,122 @@ +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 + + +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)) + + +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 + + +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 new file mode 100644 index 0000000..e69de29 diff --git a/erminig/controllers/evezh/parsers/github.py b/erminig/controllers/evezh/parsers/github.py new file mode 100644 index 0000000..29c66bd --- /dev/null +++ b/erminig/controllers/evezh/parsers/github.py @@ -0,0 +1,76 @@ +import re +import requests +from erminig.config import Config +from erminig.controllers.evezh.abstract import UpstreamSource +from erminig.system.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 new file mode 100644 index 0000000..e7a85d5 --- /dev/null +++ b/erminig/controllers/evezh/parsers/http.py @@ -0,0 +1,45 @@ +import re +import requests +from erminig.controllers.evezh.abstract import UpstreamSource +from erminig.system.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 new file mode 100644 index 0000000..c89f8f8 --- /dev/null +++ b/erminig/controllers/evezh/parsers/sourceforge.py @@ -0,0 +1,33 @@ +import re +import requests +import xml.etree.ElementTree as ET +from erminig.controllers.evezh.abstract import UpstreamSource +from erminig.system.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 new file mode 100644 index 0000000..e69de29 diff --git a/erminig/controllers/govel/build.py b/erminig/controllers/govel/build.py new file mode 100644 index 0000000..2ba0b69 --- /dev/null +++ b/erminig/controllers/govel/build.py @@ -0,0 +1,33 @@ +import subprocess +from erminig.system.security import check_root, check_user_exists, run_as_user + +check_root +check_user_exists("pak") + + +@run_as_user("pak") +def run_build_function(pakva_path): + """ + Exécute la fonction build() du fichier Pakva donné. + """ + try: + result = subprocess.run( + f""" + set -e + source "{pakva_path}" + build + """, + shell=True, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + executable="/bin/bash", + text=True, + ) + print(f"[BUILD] Succès : {pakva_path.name}") + print(result.stdout) + return True + except subprocess.CalledProcessError as e: + print(f"[BUILD] Échec : {pakva_path.name}") + print(e.stderr) + return False diff --git a/erminig/controllers/govel/pakva.py b/erminig/controllers/govel/pakva.py new file mode 100644 index 0000000..a290711 --- /dev/null +++ b/erminig/controllers/govel/pakva.py @@ -0,0 +1,103 @@ +from pathlib import Path +from erminig.config import Config +from erminig.system.security import run_as_user + + +class Pakva: + + def __init__(self, name, version, archive): + 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"" diff --git a/erminig/handlers/__init__.py b/erminig/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/erminig/handlers/versions.py b/erminig/handlers/versions.py new file mode 100644 index 0000000..652a0ec --- /dev/null +++ b/erminig/handlers/versions.py @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..e69de29 diff --git a/erminig/models/db.py b/erminig/models/db.py new file mode 100644 index 0000000..02424d8 --- /dev/null +++ b/erminig/models/db.py @@ -0,0 +1,26 @@ +import sqlite3 +from erminig.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() + 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 new file mode 100644 index 0000000..511bc5c --- /dev/null +++ b/erminig/models/upstreams.py @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..39ac451 --- /dev/null +++ b/erminig/models/versions.py @@ -0,0 +1,25 @@ +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/erminig/system/__init__.py b/erminig/system/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/erminig/system/retry.py b/erminig/system/retry.py new file mode 100644 index 0000000..ba0f772 --- /dev/null +++ b/erminig/system/retry.py @@ -0,0 +1,34 @@ +import time +import requests +from erminig.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/system/security.py b/erminig/system/security.py new file mode 100644 index 0000000..c015c04 --- /dev/null +++ b/erminig/system/security.py @@ -0,0 +1,55 @@ +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 + _, status = os.waitpid(pid, 0) + return os.WEXITSTATUS(status) + + # 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) + + # Exécuter la fonction sous l'utilisateur demandé + result = func(*args, **kwargs) + sys.exit(0 if result is None else int(bool(result))) + + except OSError as e: + print(f"[SECURITY] Fork échoué : {e}") + sys.exit(1) + + return wrapper + + return decorator diff --git a/lib/erminig.db b/lib/erminig.db new file mode 100644 index 0000000000000000000000000000000000000000..25fdf4972ca3abcb1e03e9f4b74d49d1e963a6c0 GIT binary patch literal 49152 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 NOq?v7;+)`{0RfsTR1p9G literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bd115a2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[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 new file mode 100644 index 0000000..c745680 --- /dev/null +++ b/schema.sql @@ -0,0 +1,68 @@ +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 +); + +CREATE TABLE IF NOT EXISTS versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + upstream_id INTEGER NOT NULL, + version TEXT, + url TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (upstream_id) REFERENCES upstreams(id) +); + +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 IF NOT EXISTS depends ( + 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) +); + +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/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..185d021 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,5 @@ +from erminig.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 new file mode 100644 index 0000000..6f87bb0 --- /dev/null +++ b/tests/test_pakva.py @@ -0,0 +1,20 @@ +import pytest +from pathlib import Path +from unittest.mock import patch + + +# PATCH directement le décorateur run_as_user pour les tests +@patch("erminig.system.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" From f664d07c7743273c2433b63b8083beb368e06ed1 Mon Sep 17 00:00:00 2001 From: L0m1g Date: Tue, 29 Apr 2025 17:35:21 +0200 Subject: [PATCH 02/12] Add: Entete de licence dans les fichier .py --- erminig/cli/evezh.py | 9 +++++++++ erminig/cli/govel.py | 9 +++++++++ erminig/cli/init.py | 9 +++++++++ erminig/config.py | 9 +++++++++ erminig/controllers/evezh/abstract.py | 10 ++++++++++ erminig/controllers/evezh/check.py | 9 +++++++++ erminig/controllers/evezh/parsers/github.py | 9 +++++++++ erminig/controllers/evezh/parsers/http.py | 9 +++++++++ erminig/controllers/evezh/parsers/sourceforge.py | 9 +++++++++ erminig/controllers/govel/build.py | 9 +++++++++ erminig/controllers/govel/pakva.py | 9 +++++++++ erminig/handlers/versions.py | 9 +++++++++ erminig/models/db.py | 9 +++++++++ erminig/models/upstreams.py | 9 +++++++++ erminig/models/versions.py | 9 +++++++++ erminig/system/retry.py | 10 ++++++++++ erminig/system/security.py | 9 +++++++++ tests/test_config.py | 9 +++++++++ tests/test_pakva.py | 9 +++++++++ 19 files changed, 173 insertions(+) diff --git a/erminig/cli/evezh.py b/erminig/cli/evezh.py index 150014b..ac09787 100644 --- a/erminig/cli/evezh.py +++ b/erminig/cli/evezh.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/cli/govel.py b/erminig/cli/govel.py index 6a2956a..1ca0d32 100644 --- a/erminig/cli/govel.py +++ b/erminig/cli/govel.py @@ -1,3 +1,12 @@ +# +# 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 erminig.config import Config from erminig.controllers.govel.pakva import Pakva diff --git a/erminig/cli/init.py b/erminig/cli/init.py index 7111dac..0efa943 100644 --- a/erminig/cli/init.py +++ b/erminig/cli/init.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/config.py b/erminig/config.py index 1644f41..c6e4e7a 100644 --- a/erminig/config.py +++ b/erminig/config.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/controllers/evezh/abstract.py b/erminig/controllers/evezh/abstract.py index e86a03b..9c42a2c 100644 --- a/erminig/controllers/evezh/abstract.py +++ b/erminig/controllers/evezh/abstract.py @@ -1,3 +1,13 @@ +# +# 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 diff --git a/erminig/controllers/evezh/check.py b/erminig/controllers/evezh/check.py index b6ff1b8..65a9237 100644 --- a/erminig/controllers/evezh/check.py +++ b/erminig/controllers/evezh/check.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/controllers/evezh/parsers/github.py b/erminig/controllers/evezh/parsers/github.py index 29c66bd..5d29562 100644 --- a/erminig/controllers/evezh/parsers/github.py +++ b/erminig/controllers/evezh/parsers/github.py @@ -1,3 +1,12 @@ +# +# 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.config import Config diff --git a/erminig/controllers/evezh/parsers/http.py b/erminig/controllers/evezh/parsers/http.py index e7a85d5..774aaee 100644 --- a/erminig/controllers/evezh/parsers/http.py +++ b/erminig/controllers/evezh/parsers/http.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/controllers/evezh/parsers/sourceforge.py b/erminig/controllers/evezh/parsers/sourceforge.py index c89f8f8..29bcac8 100644 --- a/erminig/controllers/evezh/parsers/sourceforge.py +++ b/erminig/controllers/evezh/parsers/sourceforge.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/controllers/govel/build.py b/erminig/controllers/govel/build.py index 2ba0b69..bab7913 100644 --- a/erminig/controllers/govel/build.py +++ b/erminig/controllers/govel/build.py @@ -1,3 +1,12 @@ +# +# 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 subprocess from erminig.system.security import check_root, check_user_exists, run_as_user diff --git a/erminig/controllers/govel/pakva.py b/erminig/controllers/govel/pakva.py index a290711..89050c3 100644 --- a/erminig/controllers/govel/pakva.py +++ b/erminig/controllers/govel/pakva.py @@ -1,3 +1,12 @@ +# +# 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.config import Config from erminig.system.security import run_as_user diff --git a/erminig/handlers/versions.py b/erminig/handlers/versions.py index 652a0ec..8e53fd0 100644 --- a/erminig/handlers/versions.py +++ b/erminig/handlers/versions.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/models/db.py b/erminig/models/db.py index 02424d8..dde81c8 100644 --- a/erminig/models/db.py +++ b/erminig/models/db.py @@ -1,3 +1,12 @@ +# +# 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 sqlite3 from erminig.config import Config diff --git a/erminig/models/upstreams.py b/erminig/models/upstreams.py index 511bc5c..e013afb 100644 --- a/erminig/models/upstreams.py +++ b/erminig/models/upstreams.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/models/versions.py b/erminig/models/versions.py index 39ac451..07bf4e0 100644 --- a/erminig/models/versions.py +++ b/erminig/models/versions.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/erminig/system/retry.py b/erminig/system/retry.py index ba0f772..e3f2be8 100644 --- a/erminig/system/retry.py +++ b/erminig/system/retry.py @@ -1,3 +1,13 @@ +# +# 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.config import Config diff --git a/erminig/system/security.py b/erminig/system/security.py index c015c04..a2ff43d 100644 --- a/erminig/system/security.py +++ b/erminig/system/security.py @@ -1,3 +1,12 @@ +# +# 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 diff --git a/tests/test_config.py b/tests/test_config.py index 185d021..d38183c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,12 @@ +# +# 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.config import Config diff --git a/tests/test_pakva.py b/tests/test_pakva.py index 6f87bb0..09621b2 100644 --- a/tests/test_pakva.py +++ b/tests/test_pakva.py @@ -1,3 +1,12 @@ +# +# 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 From 63fc1ffd0e6050b8a7bc47776ed0513b15aaf7d5 Mon Sep 17 00:00:00 2001 From: L0m1g Date: Sat, 3 May 2025 16:11:28 +0200 Subject: [PATCH 03/12] Fix: unlock database --- .gitignore | 1 + Dockerfile | 4 +++- erminig/models/db.py | 8 ++++++++ erminig/system/security.py | 11 +++++------ lib/erminig.db | Bin 49152 -> 0 bytes 5 files changed, 17 insertions(+), 7 deletions(-) delete mode 100644 lib/erminig.db diff --git a/.gitignore b/.gitignore index 9f81dd8..cefb59e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ *.egg-info/ .pytest_cache/ *.log +lib/erminig.db diff --git a/Dockerfile b/Dockerfile index 508e3c8..6e6c1eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,9 @@ RUN dnf -y update && \ dnf -y install python3 python3-pip sqlite tar zstd git bash && \ dnf clean all -RUN mkdir -p /var/lib/erminig /var/cache/erminig /opt/erminig +RUN useradd -r -s /sbin/nologin -d /var/lib/erminig pak && \ + mkdir -p /var/lib/erminig /var/cache/erminig /opt/erminig && \ + chown -R pak:pak /var/lib/erminig /var/cache/erminig /opt/erminig COPY . /opt/erminig diff --git a/erminig/models/db.py b/erminig/models/db.py index dde81c8..3fd0cc9 100644 --- a/erminig/models/db.py +++ b/erminig/models/db.py @@ -7,6 +7,8 @@ # Libre comme l’air, stable comme un menhir, et salé comme le beurre. # +import os +import pwd import sqlite3 from erminig.config import Config @@ -20,6 +22,12 @@ def init_db(): 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.") diff --git a/erminig/system/security.py b/erminig/system/security.py index a2ff43d..98bc2f1 100644 --- a/erminig/system/security.py +++ b/erminig/system/security.py @@ -39,9 +39,9 @@ def run_as_user(username): try: pid = os.fork() if pid > 0: - # Parent + # Parent : attendre le child, ne pas exit, juste return proprement _, status = os.waitpid(pid, 0) - return os.WEXITSTATUS(status) + return status >> 8 # récupère le code retour du fils (comme exit code) # Child pw_record = pwd.getpwnam(username) @@ -51,14 +51,13 @@ def run_as_user(username): os.setgid(user_gid) os.setuid(user_uid) - # Exécuter la fonction sous l'utilisateur demandé result = func(*args, **kwargs) - sys.exit(0 if result is None else int(bool(result))) + os._exit(0 if result is None else int(bool(result))) except OSError as e: print(f"[SECURITY] Fork échoué : {e}") - sys.exit(1) + os._exit(1) return wrapper - return decorator + return decorator \ No newline at end of file diff --git a/lib/erminig.db b/lib/erminig.db deleted file mode 100644 index 25fdf4972ca3abcb1e03e9f4b74d49d1e963a6c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49152 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 NOq?v7;+)`{0RfsTR1p9G From 14a8f2b47757e3b8f0aceadbd0a93e9bb9030f4d Mon Sep 17 00:00:00 2001 From: L0m1g Date: Sat, 3 May 2025 16:19:45 +0200 Subject: [PATCH 04/12] Fix: black error on pre-commit --- Makefile | 1 + erminig/system/security.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4ec82b9..ef6e999 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ test: fmt: black erminig tests + git add erminig tests docker-build: podman-compose -f docker-compose.yml build diff --git a/erminig/system/security.py b/erminig/system/security.py index 98bc2f1..a1c420d 100644 --- a/erminig/system/security.py +++ b/erminig/system/security.py @@ -41,7 +41,9 @@ def run_as_user(username): 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) + return ( + status >> 8 + ) # récupère le code retour du fils (comme exit code) # Child pw_record = pwd.getpwnam(username) @@ -60,4 +62,4 @@ def run_as_user(username): return wrapper - return decorator \ No newline at end of file + return decorator From 865ec5def55310a1491c429c260eef6be051928d Mon Sep 17 00:00:00 2001 From: L0m1g Date: Sat, 3 May 2025 16:29:04 +0200 Subject: [PATCH 05/12] Add: erminig-pakeva as submodule --- .gitmodules | 3 +++ lib/govel | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 lib/govel diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..396cfde --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/govel"] + path = lib/govel + url = git@github.com:L0m1g/Erminig-pakva.git diff --git a/lib/govel b/lib/govel new file mode 160000 index 0000000..96f6148 --- /dev/null +++ b/lib/govel @@ -0,0 +1 @@ +Subproject commit 96f61487d5b82458346f6c62077d4de474a08db6 From c26fcdc1db50136e36e6afe78af3abed3a8a0e37 Mon Sep 17 00:00:00 2001 From: L0m1g Date: Sat, 3 May 2025 18:33:53 +0200 Subject: [PATCH 06/12] Add: Start govel build packages --- Dockerfile | 2 +- erminig/cli/govel.py | 17 ++++++++--------- erminig/cli/init.py | 4 ++-- erminig/controllers/evezh/abstract.py | 1 - erminig/controllers/evezh/check.py | 1 - erminig/controllers/evezh/parsers/github.py | 4 ++-- erminig/controllers/evezh/parsers/http.py | 2 +- .../controllers/evezh/parsers/sourceforge.py | 2 +- erminig/controllers/govel/build.py | 17 +++++++++++++++-- erminig/controllers/govel/pakva.py | 4 ++-- erminig/{system => core}/__init__.py | 0 erminig/{ => core}/config.py | 1 + erminig/core/package.py | 0 erminig/{system => core}/retry.py | 2 +- erminig/{system => core}/security.py | 0 erminig/models/db.py | 2 +- tests/test_config.py | 2 +- tests/test_pakva.py | 2 +- 18 files changed, 37 insertions(+), 26 deletions(-) rename erminig/{system => core}/__init__.py (100%) rename erminig/{ => core}/config.py (94%) create mode 100644 erminig/core/package.py rename erminig/{system => core}/retry.py (97%) rename erminig/{system => core}/security.py (100%) diff --git a/Dockerfile b/Dockerfile index 6e6c1eb..3e609a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM fedora:42 RUN dnf -y update && \ - dnf -y install python3 python3-pip sqlite tar zstd git bash && \ + dnf -y install python3 python3-pip sqlite tar zstd git bash vim && \ dnf clean all RUN useradd -r -s /sbin/nologin -d /var/lib/erminig pak && \ diff --git a/erminig/cli/govel.py b/erminig/cli/govel.py index 1ca0d32..b4dfed5 100644 --- a/erminig/cli/govel.py +++ b/erminig/cli/govel.py @@ -8,7 +8,8 @@ # import argparse -from erminig.config import Config +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 @@ -24,17 +25,15 @@ def main(): if args.command == "build": if args.name: - pakva = Pakva(name=args.name, version=None, archive=None) - pakva.read() + pakva = Pakva.load_from_name(args.name) else: - if not Config.PAKVA_DIR.exists(): - print("[GOVEL] Erreur : Aucun Pakva trouvé ici.") + pakva_path = Path.cwd() / "Pakva" + if not pakva_path.exists(): + print("[GOVEL] Erreur : Aucun Pakva trouvé dans le dossier courant.") return - pakva = Pakva(name="local", version=None, archive=None) - pakva.path = Config.PAKVA_DIR - pakva.read() + pakva = Pakva.read(pakva_path) - build_success = run_build_function(pakva.path) + build_success = run_build_function(pakva.path, pakva.name, pakva.version) if build_success: print(f"[GOVEL] Build réussi pour {pakva.name}") diff --git a/erminig/cli/init.py b/erminig/cli/init.py index 0efa943..5769cf7 100644 --- a/erminig/cli/init.py +++ b/erminig/cli/init.py @@ -11,8 +11,8 @@ import os import subprocess import sys from pathlib import Path -from erminig.system.security import check_root, check_user_exists -from erminig.config import Config # Voilà la différence clé ! +from erminig.core.security import check_root, check_user_exists +from erminig.core.config import Config PAK_USER = Config.PAK_USER diff --git a/erminig/controllers/evezh/abstract.py b/erminig/controllers/evezh/abstract.py index 9c42a2c..874c3d5 100644 --- a/erminig/controllers/evezh/abstract.py +++ b/erminig/controllers/evezh/abstract.py @@ -7,7 +7,6 @@ # Libre comme l’air, stable comme un menhir, et salé comme le beurre. # - from abc import ABC, abstractmethod diff --git a/erminig/controllers/evezh/check.py b/erminig/controllers/evezh/check.py index 65a9237..d187e92 100644 --- a/erminig/controllers/evezh/check.py +++ b/erminig/controllers/evezh/check.py @@ -11,7 +11,6 @@ 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 diff --git a/erminig/controllers/evezh/parsers/github.py b/erminig/controllers/evezh/parsers/github.py index 5d29562..e6e029f 100644 --- a/erminig/controllers/evezh/parsers/github.py +++ b/erminig/controllers/evezh/parsers/github.py @@ -9,9 +9,9 @@ import re import requests -from erminig.config import Config +from erminig.core.config import Config from erminig.controllers.evezh.abstract import UpstreamSource -from erminig.system.retry import retry_on_failure +from erminig.core.retry import retry_on_failure class GitHubSource(UpstreamSource): diff --git a/erminig/controllers/evezh/parsers/http.py b/erminig/controllers/evezh/parsers/http.py index 774aaee..ec13ba7 100644 --- a/erminig/controllers/evezh/parsers/http.py +++ b/erminig/controllers/evezh/parsers/http.py @@ -10,7 +10,7 @@ import re import requests from erminig.controllers.evezh.abstract import UpstreamSource -from erminig.system.retry import retry_on_failure +from erminig.core.retry import retry_on_failure class HttpSource(UpstreamSource): diff --git a/erminig/controllers/evezh/parsers/sourceforge.py b/erminig/controllers/evezh/parsers/sourceforge.py index 29bcac8..f711af9 100644 --- a/erminig/controllers/evezh/parsers/sourceforge.py +++ b/erminig/controllers/evezh/parsers/sourceforge.py @@ -11,7 +11,7 @@ import re import requests import xml.etree.ElementTree as ET from erminig.controllers.evezh.abstract import UpstreamSource -from erminig.system.retry import retry_on_failure +from erminig.core.retry import retry_on_failure class SourceForgeRSS(UpstreamSource): diff --git a/erminig/controllers/govel/build.py b/erminig/controllers/govel/build.py index bab7913..f45b5c2 100644 --- a/erminig/controllers/govel/build.py +++ b/erminig/controllers/govel/build.py @@ -7,18 +7,30 @@ # Libre comme l’air, stable comme un menhir, et salé comme le beurre. # +import os import subprocess -from erminig.system.security import check_root, check_user_exists, run_as_user +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") @run_as_user("pak") -def run_build_function(pakva_path): +def run_build_function(pakva_path, name, version): """ Exécute la fonction build() du fichier Pakva donné. """ + 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""" @@ -32,6 +44,7 @@ def run_build_function(pakva_path): stderr=subprocess.PIPE, executable="/bin/bash", text=True, + env=env, ) print(f"[BUILD] Succès : {pakva_path.name}") print(result.stdout) diff --git a/erminig/controllers/govel/pakva.py b/erminig/controllers/govel/pakva.py index 89050c3..ebcc869 100644 --- a/erminig/controllers/govel/pakva.py +++ b/erminig/controllers/govel/pakva.py @@ -8,8 +8,8 @@ # from pathlib import Path -from erminig.config import Config -from erminig.system.security import run_as_user +from erminig.core.config import Config +from erminig.core.security import run_as_user class Pakva: diff --git a/erminig/system/__init__.py b/erminig/core/__init__.py similarity index 100% rename from erminig/system/__init__.py rename to erminig/core/__init__.py diff --git a/erminig/config.py b/erminig/core/config.py similarity index 94% rename from erminig/config.py rename to erminig/core/config.py index c6e4e7a..0090cc4 100644 --- a/erminig/config.py +++ b/erminig/core/config.py @@ -19,6 +19,7 @@ class Config: 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") diff --git a/erminig/core/package.py b/erminig/core/package.py new file mode 100644 index 0000000..e69de29 diff --git a/erminig/system/retry.py b/erminig/core/retry.py similarity index 97% rename from erminig/system/retry.py rename to erminig/core/retry.py index e3f2be8..cc0c02b 100644 --- a/erminig/system/retry.py +++ b/erminig/core/retry.py @@ -10,7 +10,7 @@ import time import requests -from erminig.config import Config +from erminig.core.config import Config def retry_on_failure(): diff --git a/erminig/system/security.py b/erminig/core/security.py similarity index 100% rename from erminig/system/security.py rename to erminig/core/security.py diff --git a/erminig/models/db.py b/erminig/models/db.py index 3fd0cc9..1de4382 100644 --- a/erminig/models/db.py +++ b/erminig/models/db.py @@ -10,7 +10,7 @@ import os import pwd import sqlite3 -from erminig.config import Config +from erminig.core.config import Config def init_db(): diff --git a/tests/test_config.py b/tests/test_config.py index d38183c..06638dd 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,7 +7,7 @@ # Libre comme l’air, stable comme un menhir, et salé comme le beurre. # -from erminig.config import Config +from erminig.core.config import Config def test_db_path(): diff --git a/tests/test_pakva.py b/tests/test_pakva.py index 09621b2..1f7ab25 100644 --- a/tests/test_pakva.py +++ b/tests/test_pakva.py @@ -13,7 +13,7 @@ from unittest.mock import patch # PATCH directement le décorateur run_as_user pour les tests -@patch("erminig.system.security.run_as_user", lambda x=None: (lambda f: f)) +@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 ! From db9b5978785c6df8c16d921970ec4f56fcce2648 Mon Sep 17 00:00:00 2001 From: L0m1g Date: Mon, 5 May 2025 16:16:24 +0200 Subject: [PATCH 07/12] Add: First packages build --- erminig/cli/govel.py | 15 +++++- erminig/controllers/govel/build.py | 22 +++++--- erminig/core/package.py | 84 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/erminig/cli/govel.py b/erminig/cli/govel.py index b4dfed5..bad28a4 100644 --- a/erminig/cli/govel.py +++ b/erminig/cli/govel.py @@ -11,7 +11,8 @@ 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 +from erminig.controllers.govel.build import run_build_function, run_pak_function +from erminig.core.package import Package def main(): @@ -33,9 +34,19 @@ def main(): return pakva = Pakva.read(pakva_path) - build_success = run_build_function(pakva.path, pakva.name, pakva.version) + pak_success = False + 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}") diff --git a/erminig/controllers/govel/build.py b/erminig/controllers/govel/build.py index f45b5c2..a9ac854 100644 --- a/erminig/controllers/govel/build.py +++ b/erminig/controllers/govel/build.py @@ -16,27 +16,26 @@ check_root check_user_exists("pak") -@run_as_user("pak") -def run_build_function(pakva_path, name, version): +def run_pakva_function(pakva_path, name, version, func_name): """ - Exécute la fonction build() du fichier Pakva donné. + 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}" - build + {func_name} """, shell=True, check=True, @@ -46,10 +45,19 @@ def run_build_function(pakva_path, name, version): text=True, env=env, ) - print(f"[BUILD] Succès : {pakva_path.name}") + print(f"[{func_name.upper()}] Succès : {pakva_path.name}") print(result.stdout) return True except subprocess.CalledProcessError as e: - print(f"[BUILD] Échec : {pakva_path.name}") + 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/core/package.py b/erminig/core/package.py index e69de29..4145992 100644 --- a/erminig/core/package.py +++ b/erminig/core/package.py @@ -0,0 +1,84 @@ +import os +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("*"): + if path.is_file(): + rel_path = Path("/") / path.relative_to(install_root) + stat = path.stat() + md5 = hashlib.md5(path.read_bytes()).hexdigest() + user = pwd.getpwuid(stat.st_uid).pw_name + group = grp.getgrgid(stat.st_gid).gr_name + self.manifest.append( + { + "path": str(rel_path), + "md5": md5, + "mode": oct(stat.st_mode & 0o777), + "user": user, + "group": group, + } + ) + + 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['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 From fb8c7f1b865bc39dfcd8960da43c12a4834ed399 Mon Sep 17 00:00:00 2001 From: L0m1g Date: Mon, 5 May 2025 16:27:52 +0200 Subject: [PATCH 08/12] Fix: Load Pakva file if --name is used with govel --- erminig/controllers/govel/pakva.py | 8 ++++++++ erminig/core/package.py | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/erminig/controllers/govel/pakva.py b/erminig/controllers/govel/pakva.py index ebcc869..f6c87f0 100644 --- a/erminig/controllers/govel/pakva.py +++ b/erminig/controllers/govel/pakva.py @@ -110,3 +110,11 @@ pak() {{ 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/package.py b/erminig/core/package.py index 4145992..58a040b 100644 --- a/erminig/core/package.py +++ b/erminig/core/package.py @@ -1,4 +1,12 @@ -import os +# +# 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 From d1bf50a8400cc51f27b5ba2d0e38ce7a53733798 Mon Sep 17 00:00:00 2001 From: L0m1g Date: Thu, 8 May 2025 11:32:06 +0200 Subject: [PATCH 09/12] Fix: remove container and venv for development --- Dockerfile | 12 ------------ dev.sh | 4 ---- docker-compose.yml | 10 ---------- erminig/cli/init.py | 8 ++++++-- 4 files changed, 6 insertions(+), 28 deletions(-) delete mode 100644 Dockerfile delete mode 100755 dev.sh delete mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3e609a9..0000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM fedora:42 - -RUN dnf -y update && \ - dnf -y install python3 python3-pip sqlite tar zstd git bash vim && \ - dnf clean all - -RUN useradd -r -s /sbin/nologin -d /var/lib/erminig pak && \ - mkdir -p /var/lib/erminig /var/cache/erminig /opt/erminig && \ - chown -R pak:pak /var/lib/erminig /var/cache/erminig /opt/erminig - -COPY . /opt/erminig - diff --git a/dev.sh b/dev.sh deleted file mode 100755 index 9162fd8..0000000 --- a/dev.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -echo "🌊 [Erminig] Rebuild and launch dev env..." -podman-compose -f docker-compose.yml build && \ -podman-compose -f docker-compose.yml run erminig \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index afb26da..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: '3.9' -services: - erminig: - build: - context: . - volumes: - - ./lib:/var/lib/erminig:z - - ./cache:/var/cache/erminig:z - working_dir: /opt/erminig - command: /bin/bash -c "pip install -e . && exec /bin/bash" diff --git a/erminig/cli/init.py b/erminig/cli/init.py index 5769cf7..136a614 100644 --- a/erminig/cli/init.py +++ b/erminig/cli/init.py @@ -11,8 +11,9 @@ import os import subprocess import sys from pathlib import Path -from erminig.core.security import check_root, check_user_exists +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 @@ -41,7 +42,7 @@ def create_user_pak(): def setup_directories(): """Crée les dossiers nécessaires et assigne les permissions.""" - for directory in [Config.LIB_DIR, Config.CACHE_DIR]: + 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) @@ -58,6 +59,9 @@ def main(): create_user_pak() setup_directories() + print("[INIT] Initialisation de la base de données...") + init_db() + print("[INIT] Environnement Erminig initialisé avec succès.") From 7d39cae01171957e2fb7c276dc864ef547b5315b Mon Sep 17 00:00:00 2001 From: L0m1g Date: Thu, 8 May 2025 11:36:06 +0200 Subject: [PATCH 10/12] Fix: remove govel as submodule --- .gitmodules | 3 --- lib/govel | 1 - 2 files changed, 4 deletions(-) delete mode 160000 lib/govel diff --git a/.gitmodules b/.gitmodules index 396cfde..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "lib/govel"] - path = lib/govel - url = git@github.com:L0m1g/Erminig-pakva.git diff --git a/lib/govel b/lib/govel deleted file mode 160000 index 96f6148..0000000 --- a/lib/govel +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 96f61487d5b82458346f6c62077d4de474a08db6 From 6a6d439aa293143a58b760949eb5f0c6258b4e17 Mon Sep 17 00:00:00 2001 From: L0m1g Date: Thu, 8 May 2025 11:59:34 +0200 Subject: [PATCH 11/12] Fix: run functions as pak user --- erminig/controllers/evezh/check.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erminig/controllers/evezh/check.py b/erminig/controllers/evezh/check.py index d187e92..55bf25f 100644 --- a/erminig/controllers/evezh/check.py +++ b/erminig/controllers/evezh/check.py @@ -17,6 +17,7 @@ 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): @@ -29,6 +30,7 @@ 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) @@ -95,6 +97,7 @@ def get_config(path): return resolved +@run_as_user("pak") def check_versions(config_path, state=None): results = [] with ErminigDB() as db: From 56bd015690910900434f0e949a18c82a4c1b62ae Mon Sep 17 00:00:00 2001 From: L0m1g Date: Fri, 9 May 2025 11:47:48 +0200 Subject: [PATCH 12/12] Add: --force option to delete existing package --- Makefile | 8 +++--- erminig/cli/govel.py | 42 ++++++++++++++++++++++++++++++ erminig/controllers/govel/pakva.py | 2 +- erminig/core/package.py | 23 ++++++++++++---- erminig/core/security.py | 4 +++ 5 files changed, 69 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index ef6e999..b308180 100644 --- a/Makefile +++ b/Makefile @@ -19,10 +19,10 @@ up: rebuild: docker-build up clean: - find . -name "*.pyc" -delete - find . -name "__pycache__" -type d -exec rm -rf {} + - find . -name "*.egg-info" -type d -exec rm -rf {} + - find . -name ".pytest_cache" -type d -exec rm -rf {} + + 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 diff --git a/erminig/cli/govel.py b/erminig/cli/govel.py index bad28a4..2309422 100644 --- a/erminig/cli/govel.py +++ b/erminig/cli/govel.py @@ -13,6 +13,9 @@ 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(): @@ -21,6 +24,17 @@ def main(): 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() @@ -36,6 +50,19 @@ def main(): 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) @@ -51,6 +78,21 @@ def main(): 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/controllers/govel/pakva.py b/erminig/controllers/govel/pakva.py index f6c87f0..a4d02cb 100644 --- a/erminig/controllers/govel/pakva.py +++ b/erminig/controllers/govel/pakva.py @@ -14,7 +14,7 @@ from erminig.core.security import run_as_user class Pakva: - def __init__(self, name, version, archive): + def __init__(self, name, version=None, archive=None): self.name = name self.version = version self.archive = archive diff --git a/erminig/core/package.py b/erminig/core/package.py index 58a040b..7a45837 100644 --- a/erminig/core/package.py +++ b/erminig/core/package.py @@ -27,12 +27,13 @@ class Package: 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(): - rel_path = Path("/") / path.relative_to(install_root) - stat = path.stat() md5 = hashlib.md5(path.read_bytes()).hexdigest() - user = pwd.getpwuid(stat.st_uid).pw_name - group = grp.getgrgid(stat.st_gid).gr_name self.manifest.append( { "path": str(rel_path), @@ -40,6 +41,18 @@ class Package: "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", } ) @@ -48,7 +61,7 @@ class Package: with open(manifest_path, "w") as f: for entry in self.manifest: f.write( - f"{entry['path']} {entry['md5']} {entry['mode']} {entry['user']} {entry['group']}\n" + f"{entry['type']} {entry['path']} {entry['md5']} {entry['mode']} {entry['user']} {entry['group']}\n" ) def copy_pakva(self, pakva_path): diff --git a/erminig/core/security.py b/erminig/core/security.py index a1c420d..c5dd51a 100644 --- a/erminig/core/security.py +++ b/erminig/core/security.py @@ -53,6 +53,10 @@ def run_as_user(username): 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)))