Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56bd015690 | ||
|
|
6a6d439aa2 | ||
|
|
7d39cae011 | ||
|
|
d1bf50a840 | ||
|
|
fb8c7f1b86 | ||
|
|
db9b597878 | ||
|
|
c26fcdc1db | ||
|
|
865ec5def5 | ||
|
|
14a8f2b477 | ||
|
|
63fc1ffd0e | ||
|
|
f664d07c77 | ||
|
|
c63f62721b |
41 changed files with 1617 additions and 314 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
*.log
|
||||||
|
lib/erminig.db
|
||||||
0
src/erminig/common/__init__.py → .gitmodules
vendored
0
src/erminig/common/__init__.py → .gitmodules
vendored
52
Makefile
52
Makefile
|
|
@ -1,37 +1,29 @@
|
||||||
DB_PATH := /var/lib/erminig/erminig.db
|
.PHONY: install test fmt docker-build up rebuild clean
|
||||||
DB_DIR := /var/lib/erminig
|
|
||||||
PAK_USER := pak
|
|
||||||
|
|
||||||
all: prepare_env create_db create_pak_user
|
install:
|
||||||
|
pip install -e .[dev]
|
||||||
|
|
||||||
prepare_env:
|
test:
|
||||||
@echo "Création de l'arborescence pour Erminig..."
|
pytest -v
|
||||||
@mkdir -p $(DB_DIR)
|
|
||||||
@mkdir -p /var/govel
|
|
||||||
@chown -R $(PAK_USER):$(PAK_USER) /var/govel || true
|
|
||||||
|
|
||||||
create_db:
|
fmt:
|
||||||
@echo "Initialisation de la base SQLite Erminig..."
|
black erminig tests
|
||||||
@if [ ! -f "$(DB_PATH)" ]; then \
|
git add erminig tests
|
||||||
sqlite3 $(DB_PATH) < schema.sql; \
|
|
||||||
chown $(PAK_USER):$(PAK_USER) $(DB_PATH); \
|
|
||||||
echo "Base de données créée à $(DB_PATH)"; \
|
|
||||||
else \
|
|
||||||
echo "La base existe déjà, on touche pas."; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
create_pak_user:
|
docker-build:
|
||||||
@echo "Création de l'utilisateur '$(PAK_USER)'..."
|
podman-compose -f docker-compose.yml build
|
||||||
@if ! id -u $(PAK_USER) >/dev/null 2>&1; then \
|
|
||||||
useradd -r -m -d /var/govel -s /bin/bash $(PAK_USER); \
|
up:
|
||||||
echo "Utilisateur '$(PAK_USER)' créé."; \
|
podman-compose -f docker-compose.yml run erminig
|
||||||
else \
|
|
||||||
echo "L'utilisateur '$(PAK_USER)' existe déjà."; \
|
rebuild: docker-build up
|
||||||
fi
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Suppression de la base et de l'arborescence..."
|
sudo find . -name "*.pyc" -delete
|
||||||
@rm -f $(DB_PATH)
|
sudo find . -name "__pycache__" -type d -exec rm -rf {} +
|
||||||
@rm -rf /var/govel
|
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
|
||||||
|
|
||||||
|
|
||||||
.PHONY: all prepare_env create_db create_pak_user clean
|
|
||||||
|
|
|
||||||
100
data/config.yaml
Normal file
100
data/config.yaml
Normal file
|
|
@ -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
|
||||||
55
doc/architecture.md
Normal file
55
doc/architecture.md
Normal file
|
|
@ -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
|
||||||
6
doc/bugs.md
Normal file
6
doc/bugs.md
Normal file
|
|
@ -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("<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.
|
||||||
|
-----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
69
doc/roadmap.md
Normal file
69
doc/roadmap.md
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
78
doc/specs-pakva.md
Normal file
78
doc/specs-pakva.md
Normal file
|
|
@ -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:<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>` |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
0
erminig/__init__.py
Normal file
0
erminig/__init__.py
Normal file
0
erminig/cli/__init__.py
Normal file
0
erminig/cli/__init__.py
Normal file
63
erminig/cli/evezh.py
Normal file
63
erminig/cli/evezh.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
98
erminig/cli/govel.py
Normal file
98
erminig/cli/govel.py
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#
|
||||||
|
# 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()
|
||||||
69
erminig/cli/init.py
Normal file
69
erminig/cli/init.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#
|
||||||
|
# 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()
|
||||||
0
erminig/controllers/__init__.py
Normal file
0
erminig/controllers/__init__.py
Normal file
0
erminig/controllers/evezh/__init__.py
Normal file
0
erminig/controllers/evezh/__init__.py
Normal file
23
erminig/controllers/evezh/abstract.py
Normal file
23
erminig/controllers/evezh/abstract.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
133
erminig/controllers/evezh/check.py
Normal file
133
erminig/controllers/evezh/check.py
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
0
erminig/controllers/evezh/parsers/__init__.py
Normal file
0
erminig/controllers/evezh/parsers/__init__.py
Normal file
85
erminig/controllers/evezh/parsers/github.py
Normal file
85
erminig/controllers/evezh/parsers/github.py
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
54
erminig/controllers/evezh/parsers/http.py
Normal file
54
erminig/controllers/evezh/parsers/http.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
#
|
||||||
|
# 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))
|
||||||
42
erminig/controllers/evezh/parsers/sourceforge.py
Normal file
42
erminig/controllers/evezh/parsers/sourceforge.py
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#
|
||||||
|
# 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]}
|
||||||
0
erminig/controllers/govel/__init__.py
Normal file
0
erminig/controllers/govel/__init__.py
Normal file
63
erminig/controllers/govel/build.py
Normal file
63
erminig/controllers/govel/build.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#
|
||||||
|
# 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")
|
||||||
120
erminig/controllers/govel/pakva.py
Normal file
120
erminig/controllers/govel/pakva.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
#
|
||||||
|
# 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)
|
||||||
0
erminig/core/__init__.py
Normal file
0
erminig/core/__init__.py
Normal file
28
erminig/core/config.py
Normal file
28
erminig/core/config.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
105
erminig/core/package.py
Normal file
105
erminig/core/package.py
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
44
erminig/core/retry.py
Normal file
44
erminig/core/retry.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
69
erminig/core/security.py
Normal file
69
erminig/core/security.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
0
erminig/handlers/__init__.py
Normal file
0
erminig/handlers/__init__.py
Normal file
32
erminig/handlers/versions.py
Normal file
32
erminig/handlers/versions.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
0
erminig/models/__init__.py
Normal file
0
erminig/models/__init__.py
Normal file
43
erminig/models/db.py
Normal file
43
erminig/models/db.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#
|
||||||
|
# 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()
|
||||||
38
erminig/models/upstreams.py
Normal file
38
erminig/models/upstreams.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
34
erminig/models/versions.py
Normal file
34
erminig/models/versions.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#
|
||||||
|
# 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
|
||||||
27
pyproject.toml
Normal file
27
pyproject.toml
Normal file
|
|
@ -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"
|
||||||
101
schema.sql
101
schema.sql
|
|
@ -1,37 +1,68 @@
|
||||||
-- schema.sql - Structure initiale de la base Erminig
|
CREATE TABLE IF NOT EXISTS upstreams (
|
||||||
|
|
||||||
CREATE TABLE packages (
|
|
||||||
name TEXT PRIMARY KEY,
|
|
||||||
version TEXT NOT NULL,
|
|
||||||
revision INTEGER NOT NULL,
|
|
||||||
status TEXT NOT NULL, -- draft, built, failed, validated
|
|
||||||
blocked BOOLEAN NOT NULL DEFAULT 0,
|
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE sources (
|
|
||||||
package_name TEXT,
|
|
||||||
url TEXT,
|
|
||||||
hash TEXT,
|
|
||||||
FOREIGN KEY (package_name) REFERENCES packages(name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE dependencies (
|
|
||||||
package_name TEXT,
|
|
||||||
dependency TEXT,
|
|
||||||
type TEXT, -- build, runtime, check
|
|
||||||
FOREIGN KEY (package_name) REFERENCES packages(name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE builds (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
package_name TEXT,
|
name TEXT NOT NULL UNIQUE,
|
||||||
version TEXT,
|
type TEXT CHECK(type IN ('http', 'github', 'sourceforge')) NOT NULL,
|
||||||
revision INTEGER,
|
url TEXT, -- Base URL ou GitHub repo ou flux RSS
|
||||||
start_time TEXT DEFAULT CURRENT_TIMESTAMP,
|
pattern TEXT DEFAULT NULL, -- Regex pour HTTP ou pattern GitHub si besoin
|
||||||
end_time TEXT,
|
file TEXT, -- Optionnel : fichier final ex: gcc-${version}.tar.xz
|
||||||
status TEXT, -- success, failed
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
log TEXT,
|
|
||||||
FOREIGN KEY (package_name) REFERENCES packages(name)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
#
|
|
||||||
# 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
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
#
|
|
||||||
# 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()
|
|
||||||
14
tests/test_config.py
Normal file
14
tests/test_config.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#
|
||||||
|
# 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")
|
||||||
29
tests/test_pakva.py
Normal file
29
tests/test_pakva.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#
|
||||||
|
# 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