Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d801678376 | ||
|
|
2060ad088d | ||
|
|
67c0c00fc3 | ||
|
|
e655fb4f71 | ||
|
|
48e191a705 | ||
|
|
a7d708c8ca | ||
|
|
b27f877f8b | ||
|
|
b21bb94e8d |
41 changed files with 307 additions and 1610 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,8 +0,0 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
*.log
|
||||
lib/erminig.db
|
||||
0
.gitmodules
vendored
0
.gitmodules
vendored
52
Makefile
52
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
|
||||
|
|
|
|||
100
data/config.yaml
100
data/config.yaml
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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("<urllib3.connection.HTTPSConnection object at 0x7fc45e207ed0>: Failed to resolve 'ftp.mpi-inf.mpg.de' ([Errno -5] No address associated with hostname)"))
|
||||
[gcc] Aucune version détectée.
|
||||
-----
|
||||
|
||||
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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:<name>()`
|
||||
`pre_install:<name>()`
|
||||
`post_install:<name>()`
|
||||
`pre_upgrade:<name>()`
|
||||
`post_upgrade:<name>()`
|
||||
`pre_remove:<name>()`
|
||||
`post_remove:<name>()`
|
||||
|
||||
### Macros disponibles
|
||||
|
||||
| Macro | Description |
|
||||
| :---- | :---------- |
|
||||
| `$PAK` | Dossier global d'installation |
|
||||
| `$SRC` | Dossier d'extraction des sources |
|
||||
| `$TMP` | Dossier temporaire pour chaque `pak:<name>` |
|
||||
| `name, $version, $revision, $basename` | Metadonnées du paquet principal |
|
||||
| `pakname` | nom du paquet courant dans un `pak:<name>` |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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]}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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"<Pakva {self.name}-{self.version}>"
|
||||
|
||||
@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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
91
schema.sql
91
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 (
|
||||
CREATE TABLE dependencies (
|
||||
package_name TEXT,
|
||||
dependency TEXT,
|
||||
type TEXT, -- build, runtime, check
|
||||
FOREIGN KEY (package_name) REFERENCES packages(name)
|
||||
);
|
||||
|
||||
CREATE TABLE builds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
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)
|
||||
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 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);
|
||||
25
src/erminig/common/db.py
Normal file
25
src/erminig/common/db.py
Normal file
|
|
@ -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
|
||||
224
src/erminig/common/pakfile.py
Normal file
224
src/erminig/common/pakfile.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
|
|
@ -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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue