[ADD] script forgejo: installer une forge git en option cochable
Une case au déploiement, et une forge git auto-hébergée répond sur le port 3000, git par SSH sur 2222. Le travail vit dans un script dédié, appelable seul sur une machine existante : une seule autorité pour les deux usages. Le binaire officiel est statique, donc le même fichier sert apt, dnf, pacman et zypper — c'est ce qui rend l'option portable sans une branche par distribution. Les architectures suivent l'amont (amd64, arm64, arm-6) ; la case se grise sur s390x plutôt que de poser un binaire inexécutable. Les quatre secrets sont écrits par le script : sans oauth2.JWT_SECRET, Forgejo tente de les persister lui-même et boucle sur un app.ini qu'il n'a pas le droit d'écrire. Vérifié sur une VM : somme de contrôle validée, service actif, API qui répond, dépôt créé puis cloné par git, et relance en 1,5 s sans rien réécrire. --- EN --- One checkbox at deploy time, and a self-hosted git forge answers on port 3000, git over SSH on 2222. The work lives in a dedicated script, callable on its own for an existing machine: one authority for both uses. The official binary is static, so the same file serves apt, dnf, pacman and zypper — that is what makes the option portable without a branch per distribution. Architectures follow upstream (amd64, arm64, arm-6); the checkbox greys out on s390x rather than dropping a binary that cannot run. The script writes all four secrets itself: without oauth2.JWT_SECRET, Forgejo tries to persist them and loops on an app.ini it is not allowed to write. Verified on a VM: checksum validated, service active, API answering, a repo created then cloned over git, and a replay in 1.5 s rewriting nothing. Assisted-by: Claude Opus 5
This commit is contained in:
parent
69431e17c9
commit
952c92b52a
8 changed files with 728 additions and 17 deletions
301
script/forgejo/install_forgejo.sh
Executable file
301
script/forgejo/install_forgejo.sh
Executable file
|
|
@ -0,0 +1,301 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
#
|
||||||
|
# Installe Forgejo — https://forgejo.org — depuis le binaire statique officiel
|
||||||
|
# publié sur https://codeberg.org/forgejo/forgejo.
|
||||||
|
#
|
||||||
|
# Le binaire est statique et sans dépendance : le même fichier sert Debian,
|
||||||
|
# Ubuntu, AlmaLinux, Rocky, openSUSE et Arch. Ce script ne touche donc à AUCUN
|
||||||
|
# gestionnaire de paquets — c'est ce qui le rend portable sur les plateformes
|
||||||
|
# ERPLibre sans une branche par distribution.
|
||||||
|
#
|
||||||
|
# Il n'appelle PAS env_var.sh, à la différence des scripts d'installation
|
||||||
|
# ERPLibre : Forgejo ne dépend ni du dépôt ni de son venv, et le script doit
|
||||||
|
# rester utilisable hors d'un checkout.
|
||||||
|
#
|
||||||
|
# Réglages, tous par variables d'environnement :
|
||||||
|
# FORGEJO_VERSION version à poser (défaut : la dernière publiée)
|
||||||
|
# FORGEJO_HTTP_PORT port web (défaut : 3000)
|
||||||
|
# FORGEJO_SSH_PORT port SSH interne de Forgejo (défaut : 2222)
|
||||||
|
# FORGEJO_ADMIN_USER compte administrateur créé (défaut : erplibre)
|
||||||
|
# FORGEJO_ADMIN_PASSWORD son mot de passe (défaut : erplibre)
|
||||||
|
# FORGEJO_ADMIN_EMAIL son courriel (défaut : admin@erplibre.local)
|
||||||
|
# FORGEJO_USER compte système propriétaire (défaut : git)
|
||||||
|
# FORGEJO_SKIP_ADMIN à 1, ne crée aucun compte (installateur web)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
Red='\033[0;31m'
|
||||||
|
Green='\033[0;32m'
|
||||||
|
Yellow='\033[0;33m'
|
||||||
|
Color_Off='\033[0m'
|
||||||
|
|
||||||
|
VERSION="${FORGEJO_VERSION:-}"
|
||||||
|
HTTP_PORT="${FORGEJO_HTTP_PORT:-3000}"
|
||||||
|
SSH_PORT="${FORGEJO_SSH_PORT:-2222}"
|
||||||
|
# « admin » est REFUSÉ par Forgejo — « CreateUser: name is reserved », mesuré.
|
||||||
|
# La liste des noms réservés couvre aussi api, assets, avatars, explore, user…
|
||||||
|
ADMIN_USER="${FORGEJO_ADMIN_USER:-erplibre}"
|
||||||
|
ADMIN_PASSWORD="${FORGEJO_ADMIN_PASSWORD:-erplibre}"
|
||||||
|
ADMIN_EMAIL="${FORGEJO_ADMIN_EMAIL:-admin@erplibre.local}"
|
||||||
|
RUN_USER="${FORGEJO_USER:-git}"
|
||||||
|
SKIP_ADMIN="${FORGEJO_SKIP_ADMIN:-0}"
|
||||||
|
|
||||||
|
BIN=/usr/local/bin/forgejo
|
||||||
|
CONF_DIR=/etc/forgejo
|
||||||
|
CONF="$CONF_DIR/app.ini"
|
||||||
|
DATA=/var/lib/forgejo
|
||||||
|
UNIT=/etc/systemd/system/forgejo.service
|
||||||
|
API=https://codeberg.org/api/v1/repos/forgejo/forgejo/releases
|
||||||
|
DL=https://codeberg.org/forgejo/forgejo/releases/download
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '5,26p' "$0" | sed 's/^# \?//'
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
case "${1:-}" in
|
||||||
|
-h|--help) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
say() { echo -e " $*"; }
|
||||||
|
die() { echo -e " ${Red}✗ $*${Color_Off}" >&2; exit 1; }
|
||||||
|
|
||||||
|
# --- 1. Architecture -------------------------------------------------------
|
||||||
|
# Forgejo publie amd64, arm64 et arm-6. PAS de s390x : sur cette architecture
|
||||||
|
# il faudrait le bâtir depuis les sources en Go, ce que ce script ne fait pas —
|
||||||
|
# il le dit plutôt que de télécharger un binaire qui ne s'exécutera pas.
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64) ARCH=amd64 ;;
|
||||||
|
aarch64|arm64) ARCH=arm64 ;;
|
||||||
|
armv6l|armv7l) ARCH=arm-6 ;;
|
||||||
|
*) die "Forgejo ne publie pas de binaire pour $(uname -m)" \
|
||||||
|
"(amd64, arm64 et arm-6 seulement)." ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- 2. Version ------------------------------------------------------------
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
# La liste des versions est en JSON : on la lit avec python3, présent dans
|
||||||
|
# toutes les images cloud visées. Sans lui, on retombe sur grep — mieux
|
||||||
|
# qu'un abandon, et le motif est celui d'un champ JSON, pas d'une page web.
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
VERSION=$(curl -fsSL --max-time 30 "$API?limit=1" 2>/dev/null \
|
||||||
|
| python3 -c 'import json,sys
|
||||||
|
try:
|
||||||
|
print(json.load(sys.stdin)[0]["tag_name"].lstrip("v"))
|
||||||
|
except Exception:
|
||||||
|
pass' || true)
|
||||||
|
else
|
||||||
|
VERSION=$(curl -fsSL --max-time 30 "$API?limit=1" 2>/dev/null \
|
||||||
|
| grep -o '"tag_name":"v[^"]*"' | head -1 \
|
||||||
|
| sed 's/.*"v//;s/"//' || true)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
[ -n "$VERSION" ] || die "Version de Forgejo introuvable (réseau ? $API)"
|
||||||
|
say "Forgejo $VERSION pour $ARCH"
|
||||||
|
|
||||||
|
# --- 3. Déjà posé ? --------------------------------------------------------
|
||||||
|
# Rejouer une installation est le cas normal. Comparer la version évite de
|
||||||
|
# retélécharger 34 Mo pour rien, et de redémarrer un service qui va bien.
|
||||||
|
if [ -x "$BIN" ] && "$BIN" --version 2>/dev/null | grep -q "version $VERSION"; then
|
||||||
|
say "${Green}binaire déjà en version $VERSION, téléchargement évité${Color_Off}"
|
||||||
|
else
|
||||||
|
# L'archive .xz pèse 34 Mo contre 114 Mo pour le binaire nu. On la prend
|
||||||
|
# quand xz est là, sans jamais l'installer : le binaire nu est le repli.
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$tmp"' EXIT
|
||||||
|
if command -v xz >/dev/null 2>&1; then
|
||||||
|
asset="forgejo-$VERSION-linux-$ARCH.xz"
|
||||||
|
else
|
||||||
|
asset="forgejo-$VERSION-linux-$ARCH"
|
||||||
|
fi
|
||||||
|
say "téléchargement de $asset"
|
||||||
|
curl -fsSL --retry 3 --max-time 900 "$DL/v$VERSION/$asset" \
|
||||||
|
-o "$tmp/$asset" || die "téléchargement impossible : $DL/v$VERSION/$asset"
|
||||||
|
# Somme de contrôle publiée à côté du fichier : une archive tronquée par une
|
||||||
|
# coupure réseau donne un binaire qui ne s'exécute pas, et l'erreur arrive
|
||||||
|
# alors dix étapes plus loin.
|
||||||
|
if curl -fsSL --max-time 60 "$DL/v$VERSION/$asset.sha256" \
|
||||||
|
-o "$tmp/$asset.sha256" 2>/dev/null; then
|
||||||
|
(cd "$tmp" && sha256sum -c "$asset.sha256" >/dev/null) \
|
||||||
|
|| die "somme de contrôle invalide pour $asset"
|
||||||
|
say "somme de contrôle vérifiée"
|
||||||
|
else
|
||||||
|
say "${Yellow}⚠ somme de contrôle indisponible, non vérifiée${Color_Off}"
|
||||||
|
fi
|
||||||
|
case "$asset" in
|
||||||
|
*.xz) xz -d "$tmp/$asset"; src="$tmp/${asset%.xz}" ;;
|
||||||
|
*) src="$tmp/$asset" ;;
|
||||||
|
esac
|
||||||
|
chmod +x "$src"
|
||||||
|
sudo install -m 0755 "$src" "$BIN"
|
||||||
|
say "${Green}binaire posé : $BIN${Color_Off}"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
trap - EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 4. Compte système et répertoires --------------------------------------
|
||||||
|
if ! id "$RUN_USER" >/dev/null 2>&1; then
|
||||||
|
sudo useradd --system --create-home --home-dir "/home/$RUN_USER" \
|
||||||
|
--shell /bin/bash --comment "Forgejo" "$RUN_USER"
|
||||||
|
say "compte système créé : $RUN_USER"
|
||||||
|
fi
|
||||||
|
sudo mkdir -p "$DATA"/{custom,data,log} "$CONF_DIR"
|
||||||
|
sudo chown -R "$RUN_USER:$RUN_USER" "$DATA"
|
||||||
|
sudo chmod 750 "$DATA"
|
||||||
|
# Le fichier de configuration appartient à root et se LIT par le groupe : le
|
||||||
|
# service en a besoin, et Forgejo y écrit ses secrets au premier démarrage si
|
||||||
|
# on ne les pose pas soi-même — ce que fait l'étape suivante.
|
||||||
|
sudo chown root:"$RUN_USER" "$CONF_DIR"
|
||||||
|
sudo chmod 770 "$CONF_DIR"
|
||||||
|
|
||||||
|
# --- 5. Configuration ------------------------------------------------------
|
||||||
|
# JAMAIS réécrite si elle existe : elle porte les secrets, et un utilisateur a
|
||||||
|
# pu l'ajuster. C'est aussi ce qui rend ce script rejouable.
|
||||||
|
# « sudo test », et non « [ -f ] » : /etc/forgejo appartient à root:git en 770,
|
||||||
|
# donc l'utilisateur qui lance le script ne peut même pas y statuer un fichier.
|
||||||
|
# Le test échouait toujours, et CHAQUE passage réécrivait la configuration avec
|
||||||
|
# des secrets neufs — ce qui invalide les sessions et les jetons existants.
|
||||||
|
if sudo test -f "$CONF"; then
|
||||||
|
say "configuration conservée : $CONF"
|
||||||
|
else
|
||||||
|
host=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
|
[ -n "$host" ] || host=localhost
|
||||||
|
# Les QUATRE secrets, et pas seulement les deux évidents. Vécu : sans
|
||||||
|
# « oauth2.JWT_SECRET », Forgejo tente de l'écrire dans app.ini au
|
||||||
|
# démarrage, n'y arrive pas — le fichier appartient à root — et s'arrête
|
||||||
|
# sur « [F] save oauth2.JWT_SECRET failed ». Le service redémarrait en
|
||||||
|
# boucle, 25 fois, sans jamais écouter le port.
|
||||||
|
#
|
||||||
|
# Les poser ici garde app.ini NON inscriptible par le service : c'est la
|
||||||
|
# bonne posture, et ça évite un fichier de configuration qui se réécrit
|
||||||
|
# tout seul.
|
||||||
|
secret=$("$BIN" generate secret SECRET_KEY)
|
||||||
|
token=$("$BIN" generate secret INTERNAL_TOKEN)
|
||||||
|
jwt=$("$BIN" generate secret JWT_SECRET)
|
||||||
|
lfs_jwt=$("$BIN" generate secret JWT_SECRET)
|
||||||
|
# « INSTALL_LOCK = true » verrouille l'installateur web : la machine est
|
||||||
|
# utilisable sans passer par un formulaire, ce qui est tout l'intérêt d'une
|
||||||
|
# option cochée au déploiement. SQLite, pour ne pas disputer PostgreSQL à
|
||||||
|
# Odoo, qui vit sur la même VM.
|
||||||
|
sudo tee "$CONF" >/dev/null <<CONFEOF
|
||||||
|
APP_NAME = ERPLibre Forgejo
|
||||||
|
RUN_USER = $RUN_USER
|
||||||
|
RUN_MODE = prod
|
||||||
|
WORK_PATH = $DATA
|
||||||
|
|
||||||
|
[server]
|
||||||
|
PROTOCOL = http
|
||||||
|
DOMAIN = $host
|
||||||
|
HTTP_PORT = $HTTP_PORT
|
||||||
|
ROOT_URL = http://$host:$HTTP_PORT/
|
||||||
|
APP_DATA_PATH = $DATA/data
|
||||||
|
DISABLE_SSH = false
|
||||||
|
START_SSH_SERVER = true
|
||||||
|
SSH_DOMAIN = $host
|
||||||
|
SSH_PORT = $SSH_PORT
|
||||||
|
SSH_LISTEN_PORT = $SSH_PORT
|
||||||
|
LFS_START_SERVER = true
|
||||||
|
LFS_JWT_SECRET = $lfs_jwt
|
||||||
|
|
||||||
|
[database]
|
||||||
|
DB_TYPE = sqlite3
|
||||||
|
PATH = $DATA/data/forgejo.db
|
||||||
|
|
||||||
|
[repository]
|
||||||
|
ROOT = $DATA/data/forgejo-repositories
|
||||||
|
|
||||||
|
[security]
|
||||||
|
INSTALL_LOCK = true
|
||||||
|
PASSWORD_COMPLEXITY = off
|
||||||
|
SECRET_KEY = $secret
|
||||||
|
INTERNAL_TOKEN = $token
|
||||||
|
|
||||||
|
[oauth2]
|
||||||
|
JWT_SECRET = $jwt
|
||||||
|
|
||||||
|
[service]
|
||||||
|
DISABLE_REGISTRATION = false
|
||||||
|
REQUIRE_SIGNIN_VIEW = false
|
||||||
|
|
||||||
|
[lfs]
|
||||||
|
PATH = $DATA/data/lfs
|
||||||
|
|
||||||
|
[log]
|
||||||
|
ROOT_PATH = $DATA/log
|
||||||
|
LEVEL = info
|
||||||
|
CONFEOF
|
||||||
|
sudo chown root:"$RUN_USER" "$CONF"
|
||||||
|
sudo chmod 640 "$CONF"
|
||||||
|
say "${Green}configuration écrite : $CONF${Color_Off}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 6. Service ------------------------------------------------------------
|
||||||
|
sudo tee "$UNIT" >/dev/null <<UNITEOF
|
||||||
|
[Unit]
|
||||||
|
Description=Forgejo (Beyond coding. We forge.)
|
||||||
|
After=network.target network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=$RUN_USER
|
||||||
|
Group=$RUN_USER
|
||||||
|
WorkingDirectory=$DATA
|
||||||
|
ExecStart=$BIN web --config $CONF
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=USER=$RUN_USER HOME=/home/$RUN_USER GITEA_WORK_DIR=$DATA
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
UNITEOF
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now forgejo.service >/dev/null 2>&1 \
|
||||||
|
|| die "le service refuse de démarrer : sudo journalctl -u forgejo -n 40"
|
||||||
|
|
||||||
|
# --- 7. Attendre qu'il RÉPONDE --------------------------------------------
|
||||||
|
# Une requête HTTP, pas un « systemctl is-active » : le service est « active »
|
||||||
|
# bien avant d'écouter, et « activating » en boucle de redémarrage ressemble à
|
||||||
|
# un démarrage en cours. /api/v1/version prouve que l'application SERT — la
|
||||||
|
# création du compte administrateur qui suit a besoin de la base migrée.
|
||||||
|
#
|
||||||
|
# Et surtout pas « exec 3<>/dev/tcp/... » : « exec » est un builtin spécial, et
|
||||||
|
# une redirection qui échoue termine le shell. Le script mourait donc en
|
||||||
|
# silence, au premier tour de la boucle, code 1 sans un mot — vécu.
|
||||||
|
ready=0
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if curl -fsS -o /dev/null --max-time 3 \
|
||||||
|
"http://127.0.0.1:$HTTP_PORT/api/v1/version"; then
|
||||||
|
ready=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
[ "$ready" = 1 ] || die "aucune réponse sur le port $HTTP_PORT après 120 s" \
|
||||||
|
"(sudo journalctl -u forgejo -n 40)"
|
||||||
|
|
||||||
|
# --- 8. Compte administrateur ---------------------------------------------
|
||||||
|
# Créé seulement s'il n'y a AUCUN compte : rejouer le script ne doit pas
|
||||||
|
# échouer sur « user already exists », ni écraser un mot de passe choisi.
|
||||||
|
if [ "$SKIP_ADMIN" = 1 ]; then
|
||||||
|
say "aucun compte créé (FORGEJO_SKIP_ADMIN=1)"
|
||||||
|
elif sudo -u "$RUN_USER" "$BIN" admin user list --config "$CONF" 2>/dev/null \
|
||||||
|
| tail -n +2 | grep -q .; then
|
||||||
|
say "comptes déjà présents, administrateur non recréé"
|
||||||
|
else
|
||||||
|
sudo -u "$RUN_USER" "$BIN" admin user create --admin \
|
||||||
|
--username "$ADMIN_USER" --password "$ADMIN_PASSWORD" \
|
||||||
|
--email "$ADMIN_EMAIL" --must-change-password=false \
|
||||||
|
--config "$CONF" >/dev/null \
|
||||||
|
|| die "création de l'administrateur impossible"
|
||||||
|
say "${Green}administrateur créé : $ADMIN_USER / $ADMIN_PASSWORD${Color_Off}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 9. Résumé -------------------------------------------------------------
|
||||||
|
host=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
|
[ -n "$host" ] || host=localhost
|
||||||
|
version=$("$BIN" --version 2>/dev/null | head -1)
|
||||||
|
say "${Green}Forgejo prêt${Color_Off} : http://$host:$HTTP_PORT/"
|
||||||
|
say " $version"
|
||||||
|
say " git par SSH : port $SSH_PORT (serveur interne de Forgejo)"
|
||||||
|
say " service : sudo systemctl status forgejo"
|
||||||
|
say " journal : sudo journalctl -u forgejo -f"
|
||||||
|
|
@ -337,6 +337,25 @@ Rendering is `swangle` in the AVD's own `config.ini` — `auto` would open a
|
||||||
black screen, and `swiftshader_indirect` no longer exists, the emulator
|
black screen, and `swiftshader_indirect` no longer exists, the emulator
|
||||||
answering `Selected GPU option ... is not valid`.
|
answering `Selected GPU option ... is not valid`.
|
||||||
|
|
||||||
|
A sixth, **Forgejo**, installs a self-hosted git forge — the software behind
|
||||||
|
Codeberg — from the project's official static binary, and leaves it serving on
|
||||||
|
port 3000 with git-over-SSH on 2222. Like the mobile build it needs no desktop,
|
||||||
|
and unlike it no package family is excluded: the binary is static, so the same
|
||||||
|
file serves apt, dnf, pacman and zypper. That is what makes it portable across
|
||||||
|
the ERPLibre platforms without a branch per distribution. Architectures follow
|
||||||
|
upstream, which publishes amd64, arm64 and arm-6 — the checkbox greys out on
|
||||||
|
s390x rather than dropping a binary that cannot run.
|
||||||
|
|
||||||
|
The work lives in `script/forgejo/install_forgejo.sh`, callable on its own for
|
||||||
|
an existing machine: `./script/forgejo/install_forgejo.sh`. It verifies the
|
||||||
|
published checksum, writes all four secrets itself so the service never needs
|
||||||
|
to rewrite its own configuration, and stores its data in SQLite so it does not
|
||||||
|
dispute PostgreSQL with Odoo on the same VM. Replaying it is cheap and safe —
|
||||||
|
1.5 s measured with everything in place: it skips a binary already at the right
|
||||||
|
version, never overwrites an existing `app.ini`, and does not recreate the
|
||||||
|
administrator. `FORGEJO_VERSION`, `FORGEJO_HTTP_PORT`, `FORGEJO_ADMIN_USER` and
|
||||||
|
a few others tune it; `--help` lists them.
|
||||||
|
|
||||||
Each tool is filtered per VM — by architecture, desktop flavour and package
|
Each tool is filtered per VM — by architecture, desktop flavour and package
|
||||||
family — and its disk cost is added to the plan before anything is created.
|
family — and its disk cost is added to the plan before anything is created.
|
||||||
|
|
||||||
|
|
@ -483,6 +502,26 @@ Le rendu est « swangle » dans le `config.ini` de l'AVD — « auto » ouvrirai
|
||||||
écran noir, et « swiftshader_indirect » n'existe plus, l'émulateur répondant
|
écran noir, et « swiftshader_indirect » n'existe plus, l'émulateur répondant
|
||||||
`Selected GPU option ... is not valid`.
|
`Selected GPU option ... is not valid`.
|
||||||
|
|
||||||
|
Un sixième, **Forgejo**, installe une forge git auto-hébergée — le logiciel
|
||||||
|
derrière Codeberg — depuis le binaire statique officiel du projet, et la laisse
|
||||||
|
en service sur le port 3000, avec git par SSH sur 2222. Comme la compilation
|
||||||
|
mobile, elle n'a besoin d'aucun bureau ; contrairement à elle, aucune famille de
|
||||||
|
paquets n'est exclue : le binaire est statique, donc le même fichier sert apt,
|
||||||
|
dnf, pacman et zypper. C'est ce qui la rend portable sur les plateformes
|
||||||
|
ERPLibre sans une branche par distribution. Les architectures suivent l'amont,
|
||||||
|
qui publie amd64, arm64 et arm-6 — la case se grise sur s390x plutôt que de
|
||||||
|
poser un binaire qui ne s'exécutera pas.
|
||||||
|
|
||||||
|
Le travail vit dans `script/forgejo/install_forgejo.sh`, appelable seul sur une
|
||||||
|
machine existante : `./script/forgejo/install_forgejo.sh`. Il vérifie la somme
|
||||||
|
de contrôle publiée, écrit lui-même les quatre secrets pour que le service n'ait
|
||||||
|
jamais à réécrire sa propre configuration, et garde ses données en SQLite pour
|
||||||
|
ne pas disputer PostgreSQL à Odoo sur la même VM. Le rejouer est sans risque et
|
||||||
|
bon marché — 1,5 s mesuré, tout étant en place : il saute un binaire déjà à la
|
||||||
|
bonne version, ne réécrit jamais un `app.ini` existant et ne recrée pas
|
||||||
|
l'administrateur. `FORGEJO_VERSION`, `FORGEJO_HTTP_PORT`, `FORGEJO_ADMIN_USER`
|
||||||
|
et quelques autres le règlent ; `--help` les énumère.
|
||||||
|
|
||||||
Chaque outil est filtré VM par VM — architecture, saveur de bureau et famille
|
Chaque outil est filtré VM par VM — architecture, saveur de bureau et famille
|
||||||
de paquets — et sa place disque s'ajoute au plan avant que rien ne soit créé.
|
de paquets — et sa place disque s'ajoute au plan avant que rien ne soit créé.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,26 @@ Le rendu est « swangle » dans le `config.ini` de l'AVD — « auto » ouvrirai
|
||||||
écran noir, et « swiftshader_indirect » n'existe plus, l'émulateur répondant
|
écran noir, et « swiftshader_indirect » n'existe plus, l'émulateur répondant
|
||||||
`Selected GPU option ... is not valid`.
|
`Selected GPU option ... is not valid`.
|
||||||
|
|
||||||
|
Un sixième, **Forgejo**, installe une forge git auto-hébergée — le logiciel
|
||||||
|
derrière Codeberg — depuis le binaire statique officiel du projet, et la laisse
|
||||||
|
en service sur le port 3000, avec git par SSH sur 2222. Comme la compilation
|
||||||
|
mobile, elle n'a besoin d'aucun bureau ; contrairement à elle, aucune famille de
|
||||||
|
paquets n'est exclue : le binaire est statique, donc le même fichier sert apt,
|
||||||
|
dnf, pacman et zypper. C'est ce qui la rend portable sur les plateformes
|
||||||
|
ERPLibre sans une branche par distribution. Les architectures suivent l'amont,
|
||||||
|
qui publie amd64, arm64 et arm-6 — la case se grise sur s390x plutôt que de
|
||||||
|
poser un binaire qui ne s'exécutera pas.
|
||||||
|
|
||||||
|
Le travail vit dans `script/forgejo/install_forgejo.sh`, appelable seul sur une
|
||||||
|
machine existante : `./script/forgejo/install_forgejo.sh`. Il vérifie la somme
|
||||||
|
de contrôle publiée, écrit lui-même les quatre secrets pour que le service n'ait
|
||||||
|
jamais à réécrire sa propre configuration, et garde ses données en SQLite pour
|
||||||
|
ne pas disputer PostgreSQL à Odoo sur la même VM. Le rejouer est sans risque et
|
||||||
|
bon marché — 1,5 s mesuré, tout étant en place : il saute un binaire déjà à la
|
||||||
|
bonne version, ne réécrit jamais un `app.ini` existant et ne recrée pas
|
||||||
|
l'administrateur. `FORGEJO_VERSION`, `FORGEJO_HTTP_PORT`, `FORGEJO_ADMIN_USER`
|
||||||
|
et quelques autres le règlent ; `--help` les énumère.
|
||||||
|
|
||||||
Chaque outil est filtré VM par VM — architecture, saveur de bureau et famille
|
Chaque outil est filtré VM par VM — architecture, saveur de bureau et famille
|
||||||
de paquets — et sa place disque s'ajoute au plan avant que rien ne soit créé.
|
de paquets — et sa place disque s'ajoute au plan avant que rien ne soit créé.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,25 @@ Rendering is `swangle` in the AVD's own `config.ini` — `auto` would open a
|
||||||
black screen, and `swiftshader_indirect` no longer exists, the emulator
|
black screen, and `swiftshader_indirect` no longer exists, the emulator
|
||||||
answering `Selected GPU option ... is not valid`.
|
answering `Selected GPU option ... is not valid`.
|
||||||
|
|
||||||
|
A sixth, **Forgejo**, installs a self-hosted git forge — the software behind
|
||||||
|
Codeberg — from the project's official static binary, and leaves it serving on
|
||||||
|
port 3000 with git-over-SSH on 2222. Like the mobile build it needs no desktop,
|
||||||
|
and unlike it no package family is excluded: the binary is static, so the same
|
||||||
|
file serves apt, dnf, pacman and zypper. That is what makes it portable across
|
||||||
|
the ERPLibre platforms without a branch per distribution. Architectures follow
|
||||||
|
upstream, which publishes amd64, arm64 and arm-6 — the checkbox greys out on
|
||||||
|
s390x rather than dropping a binary that cannot run.
|
||||||
|
|
||||||
|
The work lives in `script/forgejo/install_forgejo.sh`, callable on its own for
|
||||||
|
an existing machine: `./script/forgejo/install_forgejo.sh`. It verifies the
|
||||||
|
published checksum, writes all four secrets itself so the service never needs
|
||||||
|
to rewrite its own configuration, and stores its data in SQLite so it does not
|
||||||
|
dispute PostgreSQL with Odoo on the same VM. Replaying it is cheap and safe —
|
||||||
|
1.5 s measured with everything in place: it skips a binary already at the right
|
||||||
|
version, never overwrites an existing `app.ini`, and does not recreate the
|
||||||
|
administrator. `FORGEJO_VERSION`, `FORGEJO_HTTP_PORT`, `FORGEJO_ADMIN_USER` and
|
||||||
|
a few others tune it; `--help` lists them.
|
||||||
|
|
||||||
Each tool is filtered per VM — by architecture, desktop flavour and package
|
Each tool is filtered per VM — by architecture, desktop flavour and package
|
||||||
family — and its disk cost is added to the plan before anything is created.
|
family — and its disk cost is added to the plan before anything is created.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5422,6 +5422,32 @@ class TODO:
|
||||||
# manifeste ajoute, et du venv d'outils pour le synchroniser.
|
# manifeste ajoute, et du venv d'outils pour le synchroniser.
|
||||||
"phase": "after",
|
"phase": "after",
|
||||||
},
|
},
|
||||||
|
# Forgejo est un SERVICE, pas un outil de bureau : une VM serveur le
|
||||||
|
# prend aussi bien qu'une VM graphique. Son binaire est STATIQUE — le
|
||||||
|
# même fichier sur apt, dnf, pacman et zypper — donc aucune famille de
|
||||||
|
# paquets n'est exclue, et c'est ce qui le rend portable sur toutes les
|
||||||
|
# plateformes ERPLibre sans une branche par distribution.
|
||||||
|
#
|
||||||
|
# Les architectures, elles, sont bornées par l'amont : Forgejo publie
|
||||||
|
# amd64, arm64 et arm-6, et RIEN pour s390x. Sur celle-là il faudrait le
|
||||||
|
# bâtir en Go ; la case se grise plutôt que de poser un binaire qui ne
|
||||||
|
# s'exécute pas.
|
||||||
|
#
|
||||||
|
# Disque : ~115 Mo de binaire (34 Mo téléchargés en .xz), la base SQLite
|
||||||
|
# et les dépôts que l'utilisateur y poussera.
|
||||||
|
"forgejo": {
|
||||||
|
"label": "Forgejo (git forge)",
|
||||||
|
"hint": "self-hosted git forge on :3000, SQLite",
|
||||||
|
"disk_gb": 2,
|
||||||
|
"arches": ("amd64", "arm64"),
|
||||||
|
"desktops": (),
|
||||||
|
"needs_desktop": False,
|
||||||
|
"families": (),
|
||||||
|
# APRÈS l'installation : le script vit dans le dépôt, donc après le
|
||||||
|
# clone. Rien d'autre ne l'y oblige — Forgejo ne dépend ni du venv
|
||||||
|
# ni d'Odoo.
|
||||||
|
"phase": "after",
|
||||||
|
},
|
||||||
# L'émulateur n'a pas besoin de bureau DANS la VM : il s'affiche sur
|
# L'émulateur n'a pas besoin de bureau DANS la VM : il s'affiche sur
|
||||||
# l'écran de qui s'y connecte, par « ssh -X ». Il a besoin, lui, de KVM
|
# l'écran de qui s'y connecte, par « ssh -X ». Il a besoin, lui, de KVM
|
||||||
# dans la VM — donc de virtualisation imbriquée sur l'hôte, ce que le
|
# dans la VM — donc de virtualisation imbriquée sur l'hôte, ce que le
|
||||||
|
|
@ -6474,6 +6500,25 @@ class TODO:
|
||||||
+ f'echo " {t("smoother, without X11:")} TODO > Execute > Deploy > QEMU/KVM > tunnel > 4"'
|
+ f'echo " {t("smoother, without X11:")} TODO > Execute > Deploy > QEMU/KVM > tunnel > 4"'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _qemu_forgejo_steps(self, el_dir):
|
||||||
|
"""Pose Forgejo dans la VM, par le script dédié du dépôt.
|
||||||
|
|
||||||
|
Tout le travail est DANS le script — architecture, version, somme de
|
||||||
|
contrôle, compte système, configuration, service, compte
|
||||||
|
administrateur. Ce bloc ne fait que l'appeler : une seule autorité, et
|
||||||
|
la même commande sert un déploiement de VM et une installation à la
|
||||||
|
main sur une machine existante.
|
||||||
|
|
||||||
|
Pas de garde, comme la compilation mobile : une VM dont la forge
|
||||||
|
demandée n'existe pas n'est pas la VM demandée. Le script, lui, est
|
||||||
|
rejouable — il ne retélécharge pas un binaire déjà en place et ne
|
||||||
|
réécrit jamais une configuration existante.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
f'echo "== {t("Forgejo (git forge)")} =="; '
|
||||||
|
f"{el_dir}/script/forgejo/install_forgejo.sh"
|
||||||
|
)
|
||||||
|
|
||||||
def _qemu_after_remote_cmd(self, tools, prod=False):
|
def _qemu_after_remote_cmd(self, tools, prod=False):
|
||||||
"""Phase d'APRÈS l'installation : prologue commun, SDK commun, puis ce
|
"""Phase d'APRÈS l'installation : prologue commun, SDK commun, puis ce
|
||||||
qui a été coché.
|
qui a été coché.
|
||||||
|
|
@ -6486,29 +6531,39 @@ class TODO:
|
||||||
émulateur créé avec succès effacerait le verdict de la compilation."""
|
émulateur créé avec succès effacerait le verdict de la compilation."""
|
||||||
picked = [
|
picked = [
|
||||||
k
|
k
|
||||||
for k in ("mobile", "avd")
|
for k in ("forgejo", "mobile", "avd")
|
||||||
if k in (tools or ()) and k in self._QEMU_VM_TOOLS
|
if k in (tools or ()) and k in self._QEMU_VM_TOOLS
|
||||||
]
|
]
|
||||||
if not picked:
|
if not picked:
|
||||||
return ""
|
return ""
|
||||||
el_dir = self._qemu_install_dir(prod)
|
el_dir = self._qemu_install_dir(prod)
|
||||||
|
parts = []
|
||||||
|
# Forgejo d'abord : une minute, contre une heure pour le SDK et l'APK.
|
||||||
|
# Un échec rapide se voit tôt plutôt qu'après le long.
|
||||||
|
if "forgejo" in picked:
|
||||||
|
parts.append(f"{{ {self._qemu_forgejo_steps(el_dir)}; }}")
|
||||||
groups = []
|
groups = []
|
||||||
if "mobile" in picked:
|
if "mobile" in picked:
|
||||||
groups.append(self._qemu_mobile_build_steps(el_dir))
|
groups.append(self._qemu_mobile_build_steps(el_dir))
|
||||||
if "avd" in picked:
|
if "avd" in picked:
|
||||||
groups.append(self._qemu_avd_steps(el_dir))
|
groups.append(self._qemu_avd_steps(el_dir))
|
||||||
return (
|
if groups:
|
||||||
f'echo "== {t("ERPLibre mobile, Android SDK (long)")} =="; '
|
# UN seul prologue et un seul SDK même quand les deux options le
|
||||||
+ self._qemu_android_prologue_cmd()
|
# sont : deux prologues, et le second tronquerait le journal
|
||||||
+ self._qemu_android_sdk_steps(el_dir)
|
# détaillé du premier.
|
||||||
# Chaque groupe entre ACCOLADES. Sans elles, « && » ne lie que la
|
parts.append(
|
||||||
# première commande du groupe suivant : mesuré, un APK manquant
|
"{ "
|
||||||
# laissait tourner l'émulateur puis rendait 0 — la VM repassait au
|
+ f'echo "== {t("ERPLibre mobile, Android SDK (long)")} =="; '
|
||||||
# vert alors que rien n'avait compilé. C'est le même piège que le
|
+ self._qemu_android_prologue_cmd()
|
||||||
# bloc de service systemd, quelques centaines de lignes plus haut.
|
+ self._qemu_android_sdk_steps(el_dir)
|
||||||
+ " && ".join(f"{{ {g}; }}" for g in groups)
|
# Chaque groupe entre ACCOLADES. Sans elles, « && » ne lie que
|
||||||
+ "; "
|
# la première commande du groupe suivant : mesuré, un APK
|
||||||
)
|
# manquant laissait tourner l'émulateur puis rendait 0 — la VM
|
||||||
|
# repassait au vert alors que rien n'avait compilé.
|
||||||
|
+ " && ".join(f"{{ {g}; }}" for g in groups)
|
||||||
|
+ "; }"
|
||||||
|
)
|
||||||
|
return " && ".join(parts) + "; "
|
||||||
|
|
||||||
def _qemu_mobile_remote_cmd(self, prod=False):
|
def _qemu_mobile_remote_cmd(self, prod=False):
|
||||||
"""Compilation mobile seule — la forme que testent les tests."""
|
"""Compilation mobile seule — la forme que testent les tests."""
|
||||||
|
|
|
||||||
|
|
@ -2260,6 +2260,14 @@ TRANSLATIONS = {
|
||||||
"line": {"fr": "ligne", "en": "line"},
|
"line": {"fr": "ligne", "en": "line"},
|
||||||
"free space": {"fr": "libre", "en": "free"},
|
"free space": {"fr": "libre", "en": "free"},
|
||||||
"silent": {"fr": "silence", "en": "silent"},
|
"silent": {"fr": "silence", "en": "silent"},
|
||||||
|
"Forgejo (git forge)": {
|
||||||
|
"fr": "Forgejo (forge git)",
|
||||||
|
"en": "Forgejo (git forge)",
|
||||||
|
},
|
||||||
|
"self-hosted git forge on :3000, SQLite": {
|
||||||
|
"fr": "forge git auto-hébergée sur :3000, SQLite",
|
||||||
|
"en": "self-hosted git forge on :3000, SQLite",
|
||||||
|
},
|
||||||
"no project yet, second try on the warm caches": {
|
"no project yet, second try on the warm caches": {
|
||||||
"fr": "pas de projet encore ; seconde tentative sur les caches chauds",
|
"fr": "pas de projet encore ; seconde tentative sur les caches chauds",
|
||||||
"en": "no project yet, second try on the warm caches",
|
"en": "no project yet, second try on the warm caches",
|
||||||
|
|
|
||||||
|
|
@ -99,10 +99,17 @@ class TestToolDisk(unittest.TestCase):
|
||||||
|
|
||||||
def test_a_server_only_pays_for_what_it_gets(self):
|
def test_a_server_only_pays_for_what_it_gets(self):
|
||||||
"""Un serveur ne porte aucun IDE, donc il n'en paie pas le disque —
|
"""Un serveur ne porte aucun IDE, donc il n'en paie pas le disque —
|
||||||
mais il paie bien ce qu'il reçoit : compilation mobile et émulateur."""
|
mais il paie bien ce qu'il reçoit : compilation mobile, émulateur et
|
||||||
|
forge. La somme est calculée depuis la table plutôt qu'écrite en
|
||||||
|
chiffre : ajouter un outil sans écran ne doit pas casser ce test, il
|
||||||
|
doit le suivre."""
|
||||||
|
expected = sum(
|
||||||
|
spec["disk_gb"]
|
||||||
|
for key, spec in TODO._QEMU_VM_TOOLS.items()
|
||||||
|
if not spec["needs_desktop"]
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
TODO._QEMU_VM_TOOLS["mobile"]["disk_gb"]
|
expected,
|
||||||
+ TODO._QEMU_VM_TOOLS["avd"]["disk_gb"],
|
|
||||||
self.todo._qemu_tools_disk_gb(self.all, "amd64", "", "ubuntu"),
|
self.todo._qemu_tools_disk_gb(self.all, "amd64", "", "ubuntu"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -278,7 +285,9 @@ class TestMobileBuild(unittest.TestCase):
|
||||||
"""Elle compile, elle n'affiche rien : un bureau serait du gaspillage.
|
"""Elle compile, elle n'affiche rien : un bureau serait du gaspillage.
|
||||||
L'émulateur non plus n'en a pas besoin — il s'affiche par ssh -X."""
|
L'émulateur non plus n'en a pas besoin — il s'affiche par ssh -X."""
|
||||||
got = self.todo._qemu_tools_for(self.all, "amd64", "", "ubuntu")
|
got = self.todo._qemu_tools_for(self.all, "amd64", "", "ubuntu")
|
||||||
self.assertEqual(["mobile", "avd"], got)
|
self.assertEqual(["mobile", "forgejo", "avd"], got)
|
||||||
|
# Forgejo est là pour la même raison que la compilation : un
|
||||||
|
# service ne demande pas d'écran.
|
||||||
|
|
||||||
def test_it_is_bounded_to_apt(self):
|
def test_it_is_bounded_to_apt(self):
|
||||||
"""install-android.sh du dépôt mobile commence par « sudo apt install
|
"""install-android.sh du dépôt mobile commence par « sudo apt install
|
||||||
|
|
|
||||||
260
test/test_qemu_forgejo.py
Normal file
260
test/test_qemu_forgejo.py
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
"""Option Forgejo : la case, son filtrage, et le script qui fait le travail.
|
||||||
|
|
||||||
|
Forgejo est un service, pas un outil de bureau : une VM serveur le prend comme
|
||||||
|
une VM graphique. Son binaire est statique, donc le même fichier sert apt, dnf,
|
||||||
|
pacman et zypper — c'est ce qui le rend portable sans une branche par
|
||||||
|
distribution. Les architectures, elles, sont bornées par l'amont : Forgejo
|
||||||
|
publie amd64, arm64 et arm-6, et rien pour s390x.
|
||||||
|
|
||||||
|
Le script est vérifié en l'EXÉCUTANT sur ses chemins de refus — architecture
|
||||||
|
inconnue, version introuvable — qui précèdent toute élévation de privilège et
|
||||||
|
ne touchent donc à rien.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.argv = ["todo.py"]
|
||||||
|
from script.todo.todo import TODO # noqa: E402
|
||||||
|
|
||||||
|
SCRIPT = (
|
||||||
|
pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
/ "script/forgejo/install_forgejo.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheCheckbox(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.todo = TODO.__new__(TODO)
|
||||||
|
self.spec = TODO._QEMU_VM_TOOLS["forgejo"]
|
||||||
|
|
||||||
|
def test_it_is_offered_in_the_form(self):
|
||||||
|
keys = [k for k, _l, _h in TODO._qemu_vm_tool_choices()]
|
||||||
|
self.assertIn("forgejo", keys)
|
||||||
|
|
||||||
|
def test_a_plain_server_gets_it(self):
|
||||||
|
"""Une forge n'affiche rien : elle n'a pas besoin de bureau."""
|
||||||
|
self.assertFalse(self.spec["needs_desktop"])
|
||||||
|
got = self.todo._qemu_tools_for(("forgejo",), "amd64", "", "ubuntu")
|
||||||
|
self.assertIn("forgejo", got)
|
||||||
|
|
||||||
|
def test_every_package_family_gets_it(self):
|
||||||
|
"""Le binaire est statique : aucune famille n'est exclue, à la
|
||||||
|
différence de la compilation mobile que son installateur borne à apt.
|
||||||
|
"""
|
||||||
|
self.assertEqual(self.spec["families"], ())
|
||||||
|
for distro in ("ubuntu", "debian", "almalinux", "opensuse", "arch"):
|
||||||
|
self.assertIn(
|
||||||
|
"forgejo",
|
||||||
|
self.todo._qemu_tools_for(("forgejo",), "amd64", "", distro),
|
||||||
|
distro,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_arm64_yes_s390x_no(self):
|
||||||
|
"""Forgejo publie amd64, arm64 et arm-6. Sur s390x il faudrait le bâtir
|
||||||
|
en Go : la case se grise plutôt que de poser un binaire inexécutable.
|
||||||
|
"""
|
||||||
|
for arch in ("amd64", "arm64"):
|
||||||
|
self.assertIn(
|
||||||
|
"forgejo",
|
||||||
|
self.todo._qemu_tools_for(("forgejo",), arch, "", "ubuntu"),
|
||||||
|
arch,
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
"forgejo",
|
||||||
|
self.todo._qemu_tools_for(("forgejo",), "s390x", "", "ubuntu"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_its_disk_cost_is_counted_in_the_plan(self):
|
||||||
|
self.assertGreater(
|
||||||
|
self.todo._qemu_tools_disk_gb(("forgejo",), "amd64", "", "ubuntu"),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheInstallBlock(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.todo = TODO.__new__(TODO)
|
||||||
|
|
||||||
|
def test_it_calls_the_dedicated_script(self):
|
||||||
|
"""Une seule autorité : la même commande sert le déploiement d'une VM et
|
||||||
|
une installation à la main."""
|
||||||
|
for prod, root in (
|
||||||
|
(False, "$HOME/git/erplibre"),
|
||||||
|
(True, "/opt/erplibre"),
|
||||||
|
):
|
||||||
|
block = self.todo._qemu_forgejo_steps(
|
||||||
|
self.todo._qemu_install_dir(prod)
|
||||||
|
)
|
||||||
|
self.assertIn(f"{root}/script/forgejo/install_forgejo.sh", block)
|
||||||
|
|
||||||
|
def test_alone_it_does_not_drag_the_android_prologue(self):
|
||||||
|
"""Cocher Forgejo seul ne doit pas installer un SDK Android."""
|
||||||
|
cmd = self.todo._qemu_after_remote_cmd(("forgejo",), False)
|
||||||
|
self.assertIn("install_forgejo.sh", cmd)
|
||||||
|
self.assertNotIn("sdkmanager", cmd)
|
||||||
|
self.assertNotIn("mstep", cmd)
|
||||||
|
|
||||||
|
def test_with_the_mobile_build_forgejo_comes_first(self):
|
||||||
|
"""Une minute contre une heure : un échec rapide se voit tôt."""
|
||||||
|
cmd = self.todo._qemu_after_remote_cmd(("forgejo", "mobile"), False)
|
||||||
|
self.assertLess(cmd.index("install_forgejo.sh"), cmd.index("gradlew"))
|
||||||
|
|
||||||
|
def test_a_forgejo_failure_still_fails_the_vm(self):
|
||||||
|
"""Même contrat que la compilation mobile : une VM dont la forge
|
||||||
|
demandée n'existe pas n'est pas la VM demandée. Les groupes sont donc
|
||||||
|
liés par « && », jamais par « ; »."""
|
||||||
|
cmd = self.todo._qemu_after_remote_cmd(("forgejo", "mobile"), False)
|
||||||
|
between = cmd[
|
||||||
|
cmd.index("install_forgejo.sh") : cmd.index("ERPLibre mobile")
|
||||||
|
]
|
||||||
|
self.assertIn("&&", between)
|
||||||
|
self.assertNotIn("|| true", between)
|
||||||
|
|
||||||
|
def test_valid_shell_in_every_combination(self):
|
||||||
|
for tools in (
|
||||||
|
("forgejo",),
|
||||||
|
("forgejo", "mobile"),
|
||||||
|
("forgejo", "avd"),
|
||||||
|
("forgejo", "mobile", "avd"),
|
||||||
|
):
|
||||||
|
cmd = self.todo._qemu_after_remote_cmd(tools, False)
|
||||||
|
res = subprocess.run(
|
||||||
|
["bash", "-n"],
|
||||||
|
input="mstep() { :; }; mdiag() { :; };\n" + cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(0, res.returncode, f"{tools}: {res.stderr}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheScript(unittest.TestCase):
|
||||||
|
"""Le script lui-même, exécuté sur ses chemins de refus."""
|
||||||
|
|
||||||
|
def test_it_is_executable_and_valid_shell(self):
|
||||||
|
self.assertTrue(os.access(SCRIPT, os.X_OK), "pas exécutable")
|
||||||
|
res = subprocess.run(
|
||||||
|
["bash", "-n", str(SCRIPT)], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
self.assertEqual(0, res.returncode, res.stderr)
|
||||||
|
|
||||||
|
def test_help_explains_the_knobs_and_exits_clean(self):
|
||||||
|
res = subprocess.run(
|
||||||
|
["bash", str(SCRIPT), "--help"], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
self.assertEqual(0, res.returncode, res.stderr)
|
||||||
|
for knob in (
|
||||||
|
"FORGEJO_VERSION",
|
||||||
|
"FORGEJO_HTTP_PORT",
|
||||||
|
"FORGEJO_ADMIN_USER",
|
||||||
|
):
|
||||||
|
self.assertIn(knob, res.stdout)
|
||||||
|
|
||||||
|
def _run_with_stubs(self, stubs, env=None):
|
||||||
|
"""Lance le script avec un PATH bouchonné. Les chemins testés ici
|
||||||
|
s'arrêtent AVANT tout sudo : rien n'est installé nulle part."""
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
bin_dir = pathlib.Path(tmp) / "bin"
|
||||||
|
bin_dir.mkdir()
|
||||||
|
for name, body in stubs.items():
|
||||||
|
(bin_dir / name).write_text(f"#!/bin/bash\n{body}\n")
|
||||||
|
(bin_dir / name).chmod(0o755)
|
||||||
|
return subprocess.run(
|
||||||
|
["bash", str(SCRIPT)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=dict(
|
||||||
|
os.environ,
|
||||||
|
PATH=f"{bin_dir}:/usr/bin:/bin",
|
||||||
|
**(env or {}),
|
||||||
|
),
|
||||||
|
timeout=120,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_an_unpublished_architecture_is_refused_by_name(self):
|
||||||
|
"""Sur s390x, Forgejo n'a pas de binaire. Le dire vaut mieux que
|
||||||
|
télécharger un fichier qui ne s'exécutera pas."""
|
||||||
|
res = self._run_with_stubs({"uname": "echo s390x"})
|
||||||
|
self.assertNotEqual(0, res.returncode)
|
||||||
|
self.assertIn("s390x", res.stdout + res.stderr)
|
||||||
|
|
||||||
|
def test_an_unreachable_release_feed_is_named(self):
|
||||||
|
res = self._run_with_stubs({"uname": "echo x86_64", "curl": "exit 7"})
|
||||||
|
self.assertNotEqual(0, res.returncode)
|
||||||
|
self.assertIn("Version", res.stdout + res.stderr)
|
||||||
|
|
||||||
|
def test_a_pinned_version_needs_no_feed(self):
|
||||||
|
"""FORGEJO_VERSION évite l'appel réseau : utile hors ligne, et c'est ce
|
||||||
|
qui rend ce test rapide."""
|
||||||
|
res = self._run_with_stubs(
|
||||||
|
{"uname": "echo x86_64", "curl": "exit 7", "sudo": "exit 0"},
|
||||||
|
env={"FORGEJO_VERSION": "9.9.9"},
|
||||||
|
)
|
||||||
|
out = res.stdout + res.stderr
|
||||||
|
self.assertIn("9.9.9", out)
|
||||||
|
# Il échoue plus loin (le téléchargement est bouchonné), pas sur la
|
||||||
|
# version : c'est bien le réseau du flux qui a été évité.
|
||||||
|
self.assertNotIn("Version de Forgejo introuvable", out)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTheScriptGuards(unittest.TestCase):
|
||||||
|
"""Quatre pièges rencontrés en le mettant au point, tous mesurés."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.body = SCRIPT.read_text()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_lines(self):
|
||||||
|
"""Les lignes de CODE : le piège est expliqué en commentaire, et un
|
||||||
|
test qui cherche dans les commentaires trébuche sur sa propre
|
||||||
|
documentation — vécu à l'écriture de ce fichier."""
|
||||||
|
return [
|
||||||
|
ln
|
||||||
|
for ln in self.body.splitlines()
|
||||||
|
if ln.strip() and not ln.lstrip().startswith("#")
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_it_never_probes_with_exec_on_dev_tcp(self):
|
||||||
|
"""« exec » est un builtin spécial : une redirection qui échoue termine
|
||||||
|
le shell. Le script mourait au premier tour de la boucle d'attente,
|
||||||
|
code 1 et pas un mot."""
|
||||||
|
guilty = [ln for ln in self.code_lines if "exec 3<>" in ln]
|
||||||
|
self.assertEqual([], guilty)
|
||||||
|
self.assertIn("/api/v1/version", self.body)
|
||||||
|
|
||||||
|
def test_the_config_test_goes_through_sudo(self):
|
||||||
|
"""/etc/forgejo est en 770 root:git : « [ -f ] » échouait toujours, et
|
||||||
|
chaque passage réécrivait la configuration avec des secrets neufs."""
|
||||||
|
self.assertIn("sudo test -f", self.body)
|
||||||
|
|
||||||
|
def test_all_four_secrets_are_written(self):
|
||||||
|
"""Sans oauth2.JWT_SECRET, Forgejo tente de l'écrire dans app.ini,
|
||||||
|
n'y arrive pas, et boucle sur « [F] save oauth2.JWT_SECRET failed »."""
|
||||||
|
for key in (
|
||||||
|
"SECRET_KEY",
|
||||||
|
"INTERNAL_TOKEN",
|
||||||
|
"JWT_SECRET",
|
||||||
|
"LFS_JWT_SECRET",
|
||||||
|
):
|
||||||
|
self.assertIn(key, self.body, key)
|
||||||
|
|
||||||
|
def test_the_default_admin_name_is_not_reserved(self):
|
||||||
|
"""Forgejo refuse « admin » : « CreateUser: name is reserved »."""
|
||||||
|
self.assertIn("FORGEJO_ADMIN_USER:-erplibre", self.body)
|
||||||
|
self.assertNotIn("FORGEJO_ADMIN_USER:-admin}", self.body)
|
||||||
|
|
||||||
|
def test_it_touches_no_package_manager(self):
|
||||||
|
"""C'est ce qui le rend portable : le binaire est statique."""
|
||||||
|
for pm in ("apt-get install", "dnf install", "pacman -S", "zypper"):
|
||||||
|
self.assertNotIn(pm, self.body, pm)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in a new issue