Compare commits
18
Commits
main
..
81108d7879
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81108d7879 | ||
|
|
4a97b21ca3 | ||
|
|
a805721f04 | ||
|
|
2daacd847f | ||
|
|
c00abb4006 | ||
|
|
2858bd0755 | ||
|
|
2d98f5c987 | ||
|
|
712741a883 | ||
|
|
0c2e94431b | ||
|
|
3d4ccd32ed | ||
|
|
3fcb447bb8 | ||
|
|
6f58527206 | ||
|
|
54a22b1579 | ||
|
|
7ab423783f | ||
|
|
4e9bd8be71 | ||
|
|
33bab173bc | ||
|
|
ffc3db2dcc | ||
|
|
f942f41f34 |
@@ -1,6 +1,6 @@
|
|||||||
# Paquet YunoHost pour MaBibli
|
# Paquet YunoHost pour MaBibli
|
||||||
|
|
||||||
Paquet d'installation de [MaBibli](https://forge.example.org/mabibli), application de
|
Paquet d'installation de [MaBibli](https://git.akbar.nohost.me/mathieu/mabibli), application de
|
||||||
gestion de bibliothèque personnelle (catalogue, prêts, scan ISBN).
|
gestion de bibliothèque personnelle (catalogue, prêts, scan ISBN).
|
||||||
|
|
||||||
Ce dépôt **ne contient aucun code C#**. Il porte uniquement de quoi installer sur YunoHost une
|
Ce dépôt **ne contient aucun code C#**. Il porte uniquement de quoi installer sur YunoHost une
|
||||||
@@ -9,7 +9,7 @@ d'installation, et les scripts de publication.
|
|||||||
|
|
||||||
## 📖 Toute la documentation est dans le dépôt du code
|
## 📖 Toute la documentation est dans le dépôt du code
|
||||||
|
|
||||||
Elle a été regroupée dans **[le README de `mabibli`](https://forge.example.org/mabibli#readme)**,
|
Elle a été regroupée dans **[le README de `mabibli`](https://git.akbar.nohost.me/mathieu/mabibli#readme)**,
|
||||||
qui est le point d'entrée unique du projet : contenu et points de conception du paquet,
|
qui est le point d'entrée unique du projet : contenu et points de conception du paquet,
|
||||||
publication d'une version, mise en production pas à pas, montée de version, retour arrière,
|
publication d'une version, mise en production pas à pas, montée de version, retour arrière,
|
||||||
désinstallation, et contraintes permanentes.
|
désinstallation, et contraintes permanentes.
|
||||||
|
|||||||
Executable
+291
@@ -0,0 +1,291 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Produit l'archive de release de MaBibli et met à jour manifest.toml.
|
||||||
|
#
|
||||||
|
# Pourquoi ce script vit dans `mabibli_ynh` et non dans `mabibli` :
|
||||||
|
# sa sortie utile n'est pas l'archive, c'est le couple (url, sha256) que le
|
||||||
|
# manifeste doit porter. Le garder ici permet de compiler, d'archiver et de mettre à
|
||||||
|
# jour le manifeste en une commande, puis de committer le tout d'un bloc — le
|
||||||
|
# manifeste ne peut pas se désynchroniser de l'archive qu'il décrit.
|
||||||
|
#
|
||||||
|
# Réutilisable en CI (Gitea Actions) sans modification : aucune interaction, aucune
|
||||||
|
# variable propre à un environnement d'intégration, tout passe par des options ou des
|
||||||
|
# variables d'environnement, et le script échoue au premier problème.
|
||||||
|
#
|
||||||
|
# Exemple d'usage en CI :
|
||||||
|
# ./build/publier-release.sh \
|
||||||
|
# --source-dir "$GITHUB_WORKSPACE/mabibli" \
|
||||||
|
# --version 0.2.0 \
|
||||||
|
# --base-url "https://git.akbar.nohost.me/mathieu/mabibli/releases/download"
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
#=================================================
|
||||||
|
# VALEURS PAR DÉFAUT
|
||||||
|
#=================================================
|
||||||
|
|
||||||
|
racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
source_dir="${MABIBLI_SOURCE_DIR:-$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)}"
|
||||||
|
version="${MABIBLI_VERSION:-}"
|
||||||
|
base_url="${MABIBLI_BASE_URL:-https://git.akbar.nohost.me/mathieu/mabibli/releases/download}"
|
||||||
|
output_dir="${MABIBLI_OUTPUT_DIR:-$racine_paquet/build/dist}"
|
||||||
|
maj_manifeste=1
|
||||||
|
runtime="linux-x64"
|
||||||
|
projet="MaBibli.Api"
|
||||||
|
|
||||||
|
#=================================================
|
||||||
|
# OPTIONS
|
||||||
|
#=================================================
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'FIN'
|
||||||
|
Usage : publier-release.sh [options]
|
||||||
|
|
||||||
|
--source-dir CHEMIN Dépôt du code C# (défaut : ../mabibli)
|
||||||
|
--version X.Y.Z Version de la release (défaut : déduite de manifest.toml)
|
||||||
|
--base-url URL Racine des URL de release, sans le tag ni le fichier
|
||||||
|
--output-dir CHEMIN Où déposer l'archive (défaut : build/dist)
|
||||||
|
--no-manifest-update Ne pas réécrire manifest.toml
|
||||||
|
-h, --help Cette aide
|
||||||
|
|
||||||
|
Variables d'environnement équivalentes : MABIBLI_SOURCE_DIR, MABIBLI_VERSION,
|
||||||
|
MABIBLI_BASE_URL, MABIBLI_OUTPUT_DIR.
|
||||||
|
FIN
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--source-dir) source_dir="$2"; shift 2 ;;
|
||||||
|
--version) version="$2"; shift 2 ;;
|
||||||
|
--base-url) base_url="$2"; shift 2 ;;
|
||||||
|
--output-dir) output_dir="$2"; shift 2 ;;
|
||||||
|
--no-manifest-update) maj_manifeste=0; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
manifeste="$racine_paquet/manifest.toml"
|
||||||
|
|
||||||
|
#=================================================
|
||||||
|
# CONTRÔLES PRÉALABLES
|
||||||
|
#=================================================
|
||||||
|
|
||||||
|
echoerr() { echo "$@" >&2; }
|
||||||
|
|
||||||
|
if [ -z "$source_dir" ] || [ ! -d "$source_dir" ]; then
|
||||||
|
echoerr "Dépôt du code introuvable : '${source_dir:-<vide>}'"
|
||||||
|
echoerr "Précisez-le avec --source-dir."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
source_dir="$(cd "$source_dir" && pwd)"
|
||||||
|
|
||||||
|
if [ ! -d "$source_dir/$projet" ]; then
|
||||||
|
echoerr "'$source_dir' ne contient pas de projet $projet."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v dotnet >/dev/null; then
|
||||||
|
echoerr "Le SDK .NET est introuvable. Il n'est nécessaire QUE sur cette machine :"
|
||||||
|
echoerr "le serveur YunoHost reçoit un publish self-contained et n'a pas de SDK."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# La version par défaut se lit dans le manifeste, en retirant le suffixe ~ynhN :
|
||||||
|
# le tag et le nom de fichier portent la version applicative, pas celle du paquet.
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
version="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)"
|
||||||
|
if [ -z "$version" ]; then
|
||||||
|
echoerr "Impossible de déduire la version depuis $manifeste ; utilisez --version."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Version déduite de manifest.toml : $version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#=================================================
|
||||||
|
# COMPILATION
|
||||||
|
#=================================================
|
||||||
|
|
||||||
|
# Horodatage du build, injecté dans l'assembly à côté de la version (voir plus bas).
|
||||||
|
#
|
||||||
|
# ⚠️ Il sert de TÉMOIN : l'application ne montre sa version que si cet horodatage est présent.
|
||||||
|
# Sans lui, le numéro lu serait le « 1.0.0 » que le SDK pose par défaut, qui se lirait comme une
|
||||||
|
# vraie version alors qu'il ne désigne rien — et c'est précisément la valeur qu'on va chercher
|
||||||
|
# pour diagnostiquer un appareil dépareillé. Un binaire compilé à la main se déclare donc
|
||||||
|
# « version de développement », ce qui est vrai.
|
||||||
|
#
|
||||||
|
# `SOURCE_DATE_EPOCH` est respecté, comme pour l'archive : sans cela, deux compilations du même
|
||||||
|
# commit donneraient deux binaires — donc deux sha256 — pour la seule raison que l'heure a
|
||||||
|
# changé, et la reproductibilité visée plus bas ne vaudrait plus rien.
|
||||||
|
if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then
|
||||||
|
date_build="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
else
|
||||||
|
date_build="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
travail="$(mktemp -d)"
|
||||||
|
# shellcheck disable=SC2064
|
||||||
|
trap "rm -rf '$travail'" EXIT
|
||||||
|
|
||||||
|
publish="$travail/publish"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Compilation self-contained $runtime ==="
|
||||||
|
echo " source : $source_dir"
|
||||||
|
echo " version: $version"
|
||||||
|
echo " build : $date_build"
|
||||||
|
|
||||||
|
# `--self-contained` embarque le runtime .NET dans le dossier publié : c'est ce qui
|
||||||
|
# permet au paquet YunoHost de ne dépendre d'aucun `dotnet-runtime` côté serveur.
|
||||||
|
#
|
||||||
|
# `MaBibli.Api` référence `MaBibli.Client` : le client Blazor WebAssembly est compilé
|
||||||
|
# au passage et atterrit dans `wwwroot/`. Un seul publish suffit donc pour les trois
|
||||||
|
# projets.
|
||||||
|
dotnet publish "$source_dir/$projet" \
|
||||||
|
--configuration Release \
|
||||||
|
--runtime "$runtime" \
|
||||||
|
--self-contained \
|
||||||
|
--output "$publish" \
|
||||||
|
-p:Version="$version" \
|
||||||
|
-p:MaBibliDateBuild="$date_build"
|
||||||
|
|
||||||
|
#=================================================
|
||||||
|
# CONTRÔLES SUR LE RÉSULTAT
|
||||||
|
#=================================================
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Vérification du publish ==="
|
||||||
|
|
||||||
|
if [ ! -x "$publish/$projet" ]; then
|
||||||
|
echoerr "Binaire '$projet' absent ou non exécutable dans le publish."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sans `wwwroot`, le service démarrerait et répondrait — en ne servant aucune page.
|
||||||
|
# C'est le défaut qui est passé inaperçu pendant trois phases du projet, faute de
|
||||||
|
# regarder ailleurs que la racine. On le vérifie donc ici, à la source.
|
||||||
|
if [ ! -f "$publish/wwwroot/index.html" ]; then
|
||||||
|
echoerr "wwwroot/index.html absent : le client Blazor n'a pas été embarqué."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `index.html` doit référencer un script réellement présent. Un placeholder non
|
||||||
|
# substitué (`blazor.webassembly#[.{fingerprint}].js`) donnerait une page blanche
|
||||||
|
# alors que `/` répondrait 200.
|
||||||
|
if grep -q '#\[\.{fingerprint}\]' "$publish/wwwroot/index.html"; then
|
||||||
|
echoerr "index.html contient des placeholders d'empreinte non substitués."
|
||||||
|
echoerr "Voir CLAUDE.md, « Empreintes WASM désactivées »."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for ressource in $(grep -oE '(src|href)="[^"]+"' "$publish/wwwroot/index.html" \
|
||||||
|
| sed -E 's/^(src|href)="//; s/"$//' \
|
||||||
|
| grep -vE '^(https?:|data:|#|/$|\.$)'); do
|
||||||
|
if [ ! -f "$publish/wwwroot/$ressource" ]; then
|
||||||
|
echoerr "index.html référence '$ressource', absent du publish."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo " binaire, wwwroot et ressources d'index.html : OK"
|
||||||
|
|
||||||
|
#=================================================
|
||||||
|
# ARCHIVE
|
||||||
|
#=================================================
|
||||||
|
|
||||||
|
archive_nom="mabibli-$version-$runtime.tar.gz"
|
||||||
|
mkdir -p "$output_dir"
|
||||||
|
archive="$output_dir/$archive_nom"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Archive ==="
|
||||||
|
|
||||||
|
# Fichiers à la racine de l'archive, sans dossier intermédiaire : cohérent avec
|
||||||
|
# `in_subdir = false` dans manifest.toml.
|
||||||
|
#
|
||||||
|
# Options de reproductibilité : sans elles, deux compilations identiques donneraient
|
||||||
|
# deux sha256 différents (dates, ordre de parcours, uid/gid). En CI comme en local,
|
||||||
|
# on veut pouvoir vérifier qu'une archive correspond bien à un commit.
|
||||||
|
tar --create --gzip \
|
||||||
|
--file="$archive" \
|
||||||
|
--directory="$publish" \
|
||||||
|
--owner=0 --group=0 --numeric-owner \
|
||||||
|
--mtime="@${SOURCE_DATE_EPOCH:-0}" \
|
||||||
|
--sort=name \
|
||||||
|
.
|
||||||
|
|
||||||
|
somme="$(sha256sum "$archive" | cut -d' ' -f1)"
|
||||||
|
taille="$(du -h "$archive" | cut -f1)"
|
||||||
|
taille_octets="$(stat -c%s "$archive")"
|
||||||
|
|
||||||
|
echo " fichier : $archive"
|
||||||
|
echo " taille : $taille ($taille_octets octets)"
|
||||||
|
echo " sha256 : $somme"
|
||||||
|
|
||||||
|
#=================================================
|
||||||
|
# MISE À JOUR DU MANIFESTE
|
||||||
|
#=================================================
|
||||||
|
|
||||||
|
url="$base_url/v$version/$archive_nom"
|
||||||
|
|
||||||
|
if [ "$maj_manifeste" -eq 1 ]; then
|
||||||
|
echo
|
||||||
|
echo "=== Mise à jour de manifest.toml ==="
|
||||||
|
|
||||||
|
# Le suffixe ~ynhN est conservé s'il existe déjà pour cette version, sinon
|
||||||
|
# remis à ~ynh1 : un changement de version applicative repart toujours de 1.
|
||||||
|
version_actuelle="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$manifeste" | head -1)"
|
||||||
|
if [[ "$version_actuelle" == "$version~ynh"* ]]; then
|
||||||
|
version_paquet="$version_actuelle"
|
||||||
|
echo " version : $version_paquet (inchangée)"
|
||||||
|
else
|
||||||
|
version_paquet="$version~ynh1"
|
||||||
|
echo " version : $version_actuelle -> $version_paquet"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$manifeste" "$version_paquet" "$url" "$somme" <<'PY'
|
||||||
|
import re, sys
|
||||||
|
|
||||||
|
chemin, version, url, somme = sys.argv[1:5]
|
||||||
|
with open(chemin, encoding="utf-8") as f:
|
||||||
|
texte = f.read()
|
||||||
|
|
||||||
|
remplacements = [
|
||||||
|
(r'^version = ".*"$', f'version = "{version}"'),
|
||||||
|
(r'^ amd64\.url = ".*"$', f' amd64.url = "{url}"'),
|
||||||
|
(r'^ amd64\.sha256 = ".*"$', f' amd64.sha256 = "{somme}"'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for motif, valeur in remplacements:
|
||||||
|
texte, n = re.subn(motif, valeur, texte, count=1, flags=re.MULTILINE)
|
||||||
|
if n != 1:
|
||||||
|
sys.exit(f"Motif introuvable dans le manifeste : {motif}")
|
||||||
|
|
||||||
|
with open(chemin, "w", encoding="utf-8") as f:
|
||||||
|
f.write(texte)
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo " url : $url"
|
||||||
|
echo " sha256 : $somme"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#=================================================
|
||||||
|
# SUITE À DONNER
|
||||||
|
#=================================================
|
||||||
|
|
||||||
|
cat <<FIN
|
||||||
|
|
||||||
|
=== Terminé ===
|
||||||
|
|
||||||
|
Archive prête : $archive
|
||||||
|
|
||||||
|
Il reste à faire, à la main (le dépôt Gitea et la release ne sont pas automatisés) :
|
||||||
|
|
||||||
|
1. Créer le tag v$version sur le dépôt du code, puis la release correspondante.
|
||||||
|
2. Y téléverser $archive_nom.
|
||||||
|
3. Vérifier que l'URL du manifeste répond :
|
||||||
|
curl -fsSLI "$url"
|
||||||
|
4. Committer manifest.toml dans mabibli_ynh.
|
||||||
|
|
||||||
|
FIN
|
||||||
+51
-408
@@ -2,86 +2,37 @@
|
|||||||
#
|
#
|
||||||
# Publie une version de MaBibli de bout en bout, en une commande.
|
# Publie une version de MaBibli de bout en bout, en une commande.
|
||||||
#
|
#
|
||||||
# 1. choisir la version, à partir de celle déjà publiée
|
# Enchaîne ce qui était jusqu'ici tapé à la main, toujours dans le même ordre :
|
||||||
# 2. compiler self-contained POUR CHAQUE ARCHITECTURE, vérifier chaque publish,
|
|
||||||
# produire une archive par architecture
|
|
||||||
# 3. réécrire manifest.toml (version, url et sha256 de chaque architecture)
|
|
||||||
# 4. committer et pousser mabibli_ynh (le serveur lit Gitea, jamais le local)
|
|
||||||
# 5. taguer et pousser mabibli (le tag porte le code de la release)
|
|
||||||
#
|
#
|
||||||
# La seule chose qui reste manuelle est le dépôt des archives dans la release Gitea :
|
# 1. produire l'archive et mettre à jour manifest.toml (publier-release.sh)
|
||||||
# elles sont rappelées en fin de sortie, une par architecture.
|
# 2. committer et pousser mabibli_ynh (le serveur lit Gitea, pas le local)
|
||||||
|
# 3. taguer et pousser mabibli (le tag porte le code de la release)
|
||||||
#
|
#
|
||||||
# Le mode `--archive-seule` compile et archive sans toucher ni
|
# La version se choisit ici, à partir de celle déjà publiée : rien à aller chercher.
|
||||||
# au manifeste ni à git
|
# La seule chose qui reste manuelle est le dépôt de l'archive dans la release Gitea —
|
||||||
|
# l'URL est rappelée en fin de sortie.
|
||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
#=================================================
|
|
||||||
# VALEURS PAR DÉFAUT
|
|
||||||
#=================================================
|
|
||||||
|
|
||||||
racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
racine_paquet="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
manifeste="$racine_paquet/manifest.toml"
|
manifeste="$racine_paquet/manifest.toml"
|
||||||
|
|
||||||
# ⚠️ Chaque réglage a UNE seule façon de se donner : son option en ligne de commande.
|
source_dir="${MABIBLI_SOURCE_DIR:-$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)}"
|
||||||
# ⚠️ LA SEULE LIGNE À CHANGER pour héberger ce projet ailleurs.
|
base_url="${MABIBLI_BASE_URL:-https://git.akbar.nohost.me/mathieu/mabibli/releases/download}"
|
||||||
# Tout ce qui suit en dérive : URL de release, URL d'archive, message de fin.
|
url_releases="${MABIBLI_URL_RELEASES:-https://git.akbar.nohost.me/mathieu/mabibli/releases}"
|
||||||
#
|
|
||||||
# Une copie publique du projet porte ici `https://forge.example.org/mabibli`, injoignable
|
|
||||||
# par construction (example.org est réservé à la documentation par la RFC 2606). Le
|
|
||||||
# contrôle préalable plus bas REFUSE alors de publier, et refuse AVANT de compiler :
|
|
||||||
# celui qui reprend le projet doit renseigner cette ligne avant sa première release,
|
|
||||||
# plutôt que de découvrir le problème à l'installation, dans ynh_setup_source.
|
|
||||||
depot_code="https://git.akbar.nohost.me/mathieu/mabibli"
|
|
||||||
|
|
||||||
source_dir="$(cd "$racine_paquet/../mabibli" 2>/dev/null && pwd || true)"
|
|
||||||
base_url="$depot_code/releases/download"
|
|
||||||
url_releases="$depot_code/releases"
|
|
||||||
output_dir="$racine_paquet/build/dist"
|
|
||||||
|
|
||||||
version=""
|
version=""
|
||||||
increment=""
|
increment=""
|
||||||
sans_confirmation=0
|
sans_confirmation=0
|
||||||
archive_seule=0
|
|
||||||
projet="MaBibli.Api"
|
|
||||||
|
|
||||||
# ⚠️ Les DEUX architectures se compilent à chaque publication, et il n'y a pas d'option
|
|
||||||
# pour n'en faire qu'une. Le manifeste déclare `architectures = ["amd64", "arm64"]` : un
|
|
||||||
# serveur arm64 ira chercher `arm64.url`. Publier une seule archive laisserait le
|
|
||||||
# manifeste promettre une URL que personne n'a téléversée — et l'échec surviendrait chez
|
|
||||||
# celui qui installe, dans `ynh_setup_source`, sans rapport apparent avec la cause.
|
|
||||||
#
|
|
||||||
# ⚠️ Le RID désigne le SERVEUR, jamais la machine qui compile : le SDK télécharge le
|
|
||||||
# runtime pack de la cible, et un hôte arm64 produit un binaire x86-64 sans rien de plus.
|
|
||||||
runtimes=("linux-x64" "linux-arm64")
|
|
||||||
|
|
||||||
# Nom YunoHost de chaque RID .NET — c'est la clé du manifeste (`amd64.url`, `arm64.url`).
|
|
||||||
arch_ynh() {
|
|
||||||
case "$1" in
|
|
||||||
linux-x64) echo "amd64" ;;
|
|
||||||
linux-arm64) echo "arm64" ;;
|
|
||||||
*) echoerr "RID sans architecture YunoHost connue : $1"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'FIN'
|
cat <<'FIN'
|
||||||
Usage : publier.sh [options]
|
Usage : publier.sh [options]
|
||||||
|
|
||||||
--version X.Y.Z Version à publier (sinon : demandée, à partir de la précédente)
|
--version X.Y.Z Version à publier (sinon : demandée, à partir de la précédente)
|
||||||
--patch Incrémenter le dernier chiffre (1.0.0 -> 1.0.1)
|
--patch Incrémenter le dernier chiffre (0.3.0 -> 0.3.1)
|
||||||
--minor Incrémenter le chiffre du milieu (1.0.0 -> 1.1.0)
|
--minor Incrémenter le chiffre du milieu (0.3.0 -> 0.4.0)
|
||||||
--major Incrémenter le premier chiffre (1.0.0 -> 2.0.0)
|
--major Incrémenter le premier chiffre (0.3.0 -> 1.0.0)
|
||||||
--oui, -y Ne rien demander (pour une CI)
|
--oui Ne rien demander (pour une CI)
|
||||||
--archive-seule Compiler et archiver SEULEMENT : ni manifeste, ni commit, ni tag.
|
|
||||||
Pour REPRENDRE une publication à la main après un échec en cours
|
|
||||||
de route ; ne publie rien et ne modifie aucun dépôt. Les contrôles
|
|
||||||
restent les mêmes, mais avertissent au lieu de refuser.
|
|
||||||
--source-dir CHEMIN Dépôt du code C# (défaut : ../mabibli)
|
|
||||||
--depot-code URL Dépôt public du code, d'où dérivent les URL de release
|
|
||||||
--base-url URL Racine des URL de release, sans le tag ni le fichier
|
|
||||||
--output-dir CHEMIN Où déposer l'archive (défaut : build/dist)
|
|
||||||
-h, --help Cette aide
|
-h, --help Cette aide
|
||||||
FIN
|
FIN
|
||||||
}
|
}
|
||||||
@@ -91,11 +42,6 @@ while [ $# -gt 0 ]; do
|
|||||||
--version) version="$2"; shift 2 ;;
|
--version) version="$2"; shift 2 ;;
|
||||||
--patch|--minor|--major) increment="${1#--}"; shift ;;
|
--patch|--minor|--major) increment="${1#--}"; shift ;;
|
||||||
--oui|-y) sans_confirmation=1; shift ;;
|
--oui|-y) sans_confirmation=1; shift ;;
|
||||||
--archive-seule) archive_seule=1; shift ;;
|
|
||||||
--source-dir) source_dir="$2"; shift 2 ;;
|
|
||||||
--depot-code) depot_code="$2"; base_url="$2/releases/download"; url_releases="$2/releases"; shift 2 ;;
|
|
||||||
--base-url) base_url="$2"; shift 2 ;;
|
|
||||||
--output-dir) output_dir="$2"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;;
|
*) echo "Option inconnue : $1" >&2; usage >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
@@ -103,74 +49,13 @@ done
|
|||||||
|
|
||||||
echoerr() { echo "$@" >&2; }
|
echoerr() { echo "$@" >&2; }
|
||||||
|
|
||||||
# ⚠️ LE SEUL ENDROIT où `--archive-seule` assouplit quoi que ce soit.
|
if [ -z "$source_dir" ] || [ ! -d "$source_dir/.git" ]; then
|
||||||
#
|
echoerr "Dépôt du code introuvable : '${source_dir:-<vide>}' (MABIBLI_SOURCE_DIR)."
|
||||||
# Les garde-fous ci-dessous protègent la *publication* : ils empêchent qu'une release
|
|
||||||
# contienne autre chose que le code que son tag désigne. En `--archive-seule`, rien n'est
|
|
||||||
# publié — ni manifeste, ni commit, ni tag — donc rien ne peut mentir, et un refus n'a
|
|
||||||
# plus lieu d'être. Il reste un avertissement, car ce qu'il signale est toujours vrai.
|
|
||||||
#
|
|
||||||
# Chaque garde-fou s'écrit donc UNE fois, sans savoir dans quel mode il tourne. C'est ce
|
|
||||||
# qui empêche d'assouplir par inadvertance un contrôle qui devait rester dur.
|
|
||||||
# ⚠️ Le CONSTAT et le CONSEIL sont séparés, et ce n'est pas de la cosmétique : le constat
|
|
||||||
# reste vrai dans les deux modes, le conseil non. « Choisissez un numéro libre » est le
|
|
||||||
# bon conseil pour une publication, et un mauvais conseil en reprise — où le tag visé est
|
|
||||||
# justement celui qu'on veut retrouver.
|
|
||||||
refuser() { # refuser <conseil> <constat> [<constat>...]
|
|
||||||
local conseil="$1"; shift
|
|
||||||
if [ "$archive_seule" -eq 1 ]; then
|
|
||||||
printf ' ⚠ %s\n' "$@"
|
|
||||||
echo " (poursuite : --archive-seule ne publie rien)"
|
|
||||||
else
|
|
||||||
printf '%s\n' "$@" >&2
|
|
||||||
[ -n "$conseil" ] && echoerr "$conseil"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
#=================================================
|
|
||||||
# CONTRÔLES PRÉALABLES
|
|
||||||
#=================================================
|
|
||||||
|
|
||||||
if [ -z "$source_dir" ] || [ ! -d "$source_dir" ]; then
|
|
||||||
echoerr "Dépôt du code introuvable : '${source_dir:-<vide>}'"
|
|
||||||
echoerr "Précisez-le avec --source-dir."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
source_dir="$(cd "$source_dir" && pwd)"
|
|
||||||
|
|
||||||
if [ ! -d "$source_dir/$projet" ]; then
|
|
||||||
echoerr "'$source_dir' ne contient pas de projet $projet."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ⚠️ Exigé dans les DEUX modes. `--archive-seule` sert à reprendre une publication à la
|
|
||||||
# main, jamais à compiler un arbre quelconque : sans dépôt, il ne saurait ni proposer une
|
|
||||||
# version, ni dire si l'archive correspond au code d'un tag. L'exiger ici plutôt qu'à
|
|
||||||
# quatre endroits plus bas est ce qui rend le reste du script inconditionnel.
|
|
||||||
if [ ! -d "$source_dir/.git" ]; then
|
|
||||||
echoerr "'$source_dir' n'est pas un dépôt git."
|
|
||||||
echoerr "publier.sh compile ce qu'un commit désigne : sans dépôt, rien ne le désigne."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ⚠️ Refuser AVANT de compiler, comme les autres garde-fous. Publier avec le placeholder
|
|
||||||
# écrirait dans manifest.toml une `amd64.url` que personne ne peut télécharger — et
|
|
||||||
# `ynh_setup_source` échouerait à l'installation, loin d'ici et sans rapport apparent.
|
|
||||||
if [ "$archive_seule" -eq 0 ] && [[ "$base_url" == *example.org* ]]; then
|
|
||||||
echoerr "Le dépôt public n'est pas configuré : $depot_code"
|
|
||||||
echoerr "Changez « depot_code » en tête de ce script, ou passez --depot-code URL."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v dotnet >/dev/null; then
|
|
||||||
echoerr "Le SDK .NET est introuvable. Il n'est nécessaire QUE sur cette machine :"
|
|
||||||
echoerr "le serveur YunoHost reçoit un publish self-contained et n'a pas de SDK."
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#=================================================
|
#=================================================
|
||||||
# LA VERSION
|
# LA VERSION PRÉCÉDENTE, DEUX SOURCES QUI DOIVENT S'ACCORDER
|
||||||
#=================================================
|
#=================================================
|
||||||
|
|
||||||
# Le tag fait foi : c'est lui qui désigne le code publié. Le manifeste ne porte que la
|
# Le tag fait foi : c'est lui qui désigne le code publié. Le manifeste ne porte que la
|
||||||
@@ -180,7 +65,16 @@ version_tag="$(git -C "$source_dir" tag --list 'v[0-9]*' --sort=-v:refname | hea
|
|||||||
version_manifeste="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)"
|
version_manifeste="$(sed -n 's/^version *= *"\([^"~]*\).*"/\1/p' "$manifeste" | head -1)"
|
||||||
|
|
||||||
precedente="$(printf '%s\n%s\n' "$version_tag" "$version_manifeste" \
|
precedente="$(printf '%s\n%s\n' "$version_tag" "$version_manifeste" \
|
||||||
| grep -E '^[0-9]+(\.[0-9]+)*$' | sort -V | tail -1 || true)"
|
| grep -E '^[0-9]+(\.[0-9]+)*$' | sort -V | tail -1)"
|
||||||
|
|
||||||
|
if [ -z "$precedente" ]; then
|
||||||
|
echoerr "Aucune version précédente trouvée (ni tag, ni manifest.toml) ; utilisez --version."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Dernière version publiée : $precedente"
|
||||||
|
[ "$version_tag" != "$version_manifeste" ] &&
|
||||||
|
echo " ⚠ tag v${version_tag:-—} et manifest.toml ${version_manifeste:-—} divergent"
|
||||||
|
|
||||||
suivante() { # suivante <version> <patch|minor|major>
|
suivante() { # suivante <version> <patch|minor|major>
|
||||||
IFS=. read -r maj min cor <<<"$1"
|
IFS=. read -r maj min cor <<<"$1"
|
||||||
@@ -191,19 +85,6 @@ suivante() { # suivante <version> <patch|minor|major>
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# ⚠️ Le calcul de la version est le MÊME dans les deux modes, et c'est délibéré : une
|
|
||||||
# reprise après échec veut la version qu'on était en train de publier, c'est-à-dire ce
|
|
||||||
# que l'incrément propose. Faire retomber `--archive-seule` sur la version du manifeste
|
|
||||||
# reproduirait le numéro DÉJÀ publié tant qu'on ne passerait pas `--version`.
|
|
||||||
if [ -z "$precedente" ]; then
|
|
||||||
echoerr "Aucune version précédente trouvée (ni tag, ni manifest.toml) ; utilisez --version."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Dernière version publiée : $precedente"
|
|
||||||
[ "$version_tag" != "$version_manifeste" ] &&
|
|
||||||
echo " ⚠ tag v${version_tag:-—} et manifest.toml ${version_manifeste:-—} divergent"
|
|
||||||
|
|
||||||
if [ -z "$version" ] && [ -n "$increment" ]; then
|
if [ -z "$version" ] && [ -n "$increment" ]; then
|
||||||
version="$(suivante "$precedente" "$increment")"
|
version="$(suivante "$precedente" "$increment")"
|
||||||
fi
|
fi
|
||||||
@@ -234,43 +115,30 @@ if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then
|
||||||
|
echoerr "Le tag v$version existe déjà dans $source_dir."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
#=================================================
|
#=================================================
|
||||||
# CE QUI DOIT ÊTRE VRAI AVANT DE COMPILER
|
# CE QUI DOIT ÊTRE VRAI AVANT DE COMPILER
|
||||||
#=================================================
|
#=================================================
|
||||||
|
|
||||||
# ⚠️ Recompiler une version dont le tag existe
|
|
||||||
# déjà produit une archive qui ne correspond PAS au code que ce tag désigne : la release
|
|
||||||
# se met alors à mentir, et l'écran « À propos » avec elle, sans que rien ne le signale.
|
|
||||||
if git -C "$source_dir" rev-parse "v$version" >/dev/null 2>&1; then
|
|
||||||
commit_tag="$(git -C "$source_dir" rev-list -n1 "v$version")"
|
|
||||||
commit_head="$(git -C "$source_dir" rev-parse HEAD)"
|
|
||||||
if [ "$commit_tag" != "$commit_head" ]; then
|
|
||||||
refuser "Choisissez un numéro libre (--patch)." \
|
|
||||||
"Le tag v$version existe et désigne ${commit_tag:0:7} ; HEAD est ${commit_head:0:7}." \
|
|
||||||
"L'archive ne serait donc PAS le code que ce tag désigne."
|
|
||||||
else
|
|
||||||
refuser "Choisissez un numéro libre (--patch)." \
|
|
||||||
"Le tag v$version existe déjà (${commit_tag:0:7}) : ce numéro est pris."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Le tag va désigner le HEAD du dépôt du code : un travail non committé ne serait donc
|
# Le tag va désigner le HEAD du dépôt du code : un travail non committé ne serait donc
|
||||||
# pas dans la release, alors même que l'archive, elle, le contiendrait.
|
# pas dans la release, alors même que l'archive, elle, le contiendrait.
|
||||||
if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then
|
if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then
|
||||||
|
echoerr "Le dépôt du code a des modifications non committées :"
|
||||||
git -C "$source_dir" status --short >&2
|
git -C "$source_dir" status --short >&2
|
||||||
refuser "" \
|
exit 1
|
||||||
"Le dépôt du code a des modifications non committées (ci-dessus)." \
|
|
||||||
"Le tag désignera HEAD : ce travail ne serait pas dans la release."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=== À publier ==="
|
echo "=== À publier ==="
|
||||||
echo " version : ${precedente:-—} -> $version"
|
echo " version : $precedente -> $version"
|
||||||
branche="$(git -C "$source_dir" rev-parse --abbrev-ref HEAD)"
|
|
||||||
echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))"
|
echo " code : $source_dir ($branche, $(git -C "$source_dir" rev-parse --short HEAD))"
|
||||||
echo " paquet : $racine_paquet"
|
echo " paquet : $racine_paquet"
|
||||||
[ "$archive_seule" -eq 1 ] &&
|
|
||||||
echo " mode : --archive-seule (ni manifeste, ni commit, ni tag)"
|
|
||||||
|
|
||||||
if [ "$sans_confirmation" -ne 1 ]; then
|
if [ "$sans_confirmation" -ne 1 ]; then
|
||||||
read -rp "Continuer ? [O/n] " reponse
|
read -rp "Continuer ? [O/n] " reponse
|
||||||
@@ -281,242 +149,16 @@ if [ "$sans_confirmation" -ne 1 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
#=================================================
|
#=================================================
|
||||||
# COMPILATION
|
# 1. ARCHIVE + MANIFESTE
|
||||||
#=================================================
|
#=================================================
|
||||||
|
|
||||||
# Horodatage du build, injecté dans l'assembly à côté de la version.
|
"$racine_paquet/build/publier-release.sh" --version "$version" --source-dir "$source_dir" --base-url "$base_url"
|
||||||
#
|
|
||||||
# ⚠️ Il sert de TÉMOIN : l'application ne montre sa version que si cet horodatage est présent.
|
|
||||||
# Sans lui, le numéro lu serait le « 1.0.0 » que le SDK pose par défaut, qui se lirait comme une
|
|
||||||
# vraie version alors qu'il ne désigne rien — et c'est précisément la valeur qu'on va chercher
|
|
||||||
# pour diagnostiquer un appareil dépareillé. Un binaire compilé à la main se déclare donc
|
|
||||||
# « version de développement », ce qui est vrai.
|
|
||||||
#
|
|
||||||
# `SOURCE_DATE_EPOCH` est respecté, comme pour l'archive : sans cela, deux compilations du même
|
|
||||||
# commit donneraient deux binaires — donc deux sha256 — pour la seule raison que l'heure a
|
|
||||||
# changé, et la reproductibilité visée plus bas ne vaudrait plus rien.
|
|
||||||
if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then
|
|
||||||
date_build="$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y-%m-%dT%H:%M:%SZ)"
|
|
||||||
else
|
|
||||||
date_build="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
travail="$(mktemp -d)"
|
archive_nom="mabibli-$version-linux-x64.tar.gz"
|
||||||
# shellcheck disable=SC2064
|
archive="${MABIBLI_OUTPUT_DIR:-$racine_paquet/build/dist}/$archive_nom"
|
||||||
trap "rm -rf '$travail'" EXIT
|
|
||||||
|
|
||||||
# Une passe complète par architecture : compilation, contrôles, archive. Les trois
|
|
||||||
# blocs sont RIGOUREUSEMENT les mêmes d'une architecture à l'autre — c'est ce qui
|
|
||||||
# garantit qu'une archive arm64 est vérifiée aussi durement qu'une amd64.
|
|
||||||
declare -A sommes=()
|
|
||||||
declare -A urls=()
|
|
||||||
declare -A archives=()
|
|
||||||
|
|
||||||
for runtime in "${runtimes[@]}"; do
|
|
||||||
|
|
||||||
arch="$(arch_ynh "$runtime")"
|
|
||||||
publish="$travail/publish-$runtime"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== Compilation self-contained $runtime ($arch) ==="
|
|
||||||
echo " source : $source_dir"
|
|
||||||
echo " version: $version"
|
|
||||||
echo " build : $date_build"
|
|
||||||
|
|
||||||
# `--self-contained` embarque le runtime .NET dans le dossier publié : c'est ce qui
|
|
||||||
# permet au paquet YunoHost de ne dépendre d'aucun `dotnet-runtime` côté serveur.
|
|
||||||
#
|
|
||||||
# `MaBibli.Api` référence `MaBibli.Client` : le client Blazor WebAssembly est compilé
|
|
||||||
# au passage et atterrit dans `wwwroot/`. Un seul publish suffit donc pour les trois
|
|
||||||
# projets.
|
|
||||||
#
|
|
||||||
# ⚠️ Le client compilé est le MÊME quelle que soit l'architecture — c'est du WebAssembly,
|
|
||||||
# zbar compris. Seul l'hôte .NET change. On le recompile quand même par architecture
|
|
||||||
# plutôt que de recopier un `wwwroot/` d'une passe à l'autre : le publish est le seul
|
|
||||||
# geste dont on sait qu'il produit un ensemble cohérent, et les contrôles ci-dessous
|
|
||||||
# portent sur ce qu'on archive vraiment, pas sur ce qu'on croit y avoir mis.
|
|
||||||
dotnet publish "$source_dir/$projet" \
|
|
||||||
--configuration Release \
|
|
||||||
--runtime "$runtime" \
|
|
||||||
--self-contained \
|
|
||||||
--output "$publish" \
|
|
||||||
-p:Version="$version" \
|
|
||||||
-p:MaBibliDateBuild="$date_build"
|
|
||||||
|
|
||||||
#=================================================
|
#=================================================
|
||||||
# CONTRÔLES SUR LE RÉSULTAT
|
# 2. LE PAQUET (le serveur lit Gitea, jamais la copie locale)
|
||||||
#=================================================
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== Vérification du publish ($arch) ==="
|
|
||||||
|
|
||||||
if [ ! -x "$publish/$projet" ]; then
|
|
||||||
echoerr "Binaire '$projet' absent ou non exécutable dans le publish $runtime."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sans `wwwroot`, le service démarrerait et répondrait — en ne servant aucune page.
|
|
||||||
# C'est le défaut qui est passé inaperçu pendant trois phases du projet, faute de
|
|
||||||
# regarder ailleurs que la racine. On le vérifie donc ici, à la source.
|
|
||||||
if [ ! -f "$publish/wwwroot/index.html" ]; then
|
|
||||||
echoerr "wwwroot/index.html absent : le client Blazor n'a pas été embarqué ($runtime)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# `index.html` doit référencer un script réellement présent. Un placeholder non
|
|
||||||
# substitué (`blazor.webassembly#[.{fingerprint}].js`) donnerait une page blanche
|
|
||||||
# alors que `/` répondrait 200.
|
|
||||||
if grep -q '#\[\.{fingerprint}\]' "$publish/wwwroot/index.html"; then
|
|
||||||
echoerr "index.html contient des placeholders d'empreinte non substitués ($runtime)."
|
|
||||||
echoerr "Voir CLAUDE.md, « Les invariants à ne jamais casser »."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for ressource in $(grep -oE '(src|href)="[^"]+"' "$publish/wwwroot/index.html" \
|
|
||||||
| sed -E 's/^(src|href)="//; s/"$//' \
|
|
||||||
| grep -vE '^(https?:|data:|#|/$|\.$)'); do
|
|
||||||
if [ ! -f "$publish/wwwroot/$ressource" ]; then
|
|
||||||
echoerr "index.html référence '$ressource', absent du publish ($runtime)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# ⚠️ Le contrôle qui n'existait pas tant qu'il n'y avait qu'une architecture : rien
|
|
||||||
# n'empêche un publish de rendre un binaire de l'architecture de la MACHINE plutôt que
|
|
||||||
# de la cible. Le symptôme serait un service qui refuse de démarrer sur le serveur, avec
|
|
||||||
# un « Exec format error » qui ne désigne pas la publication.
|
|
||||||
attendu_elf="$(case "$runtime" in linux-x64) echo "x86-64" ;; linux-arm64) echo "aarch64" ;; esac)"
|
|
||||||
if ! file -b "$publish/$projet" | grep -q "$attendu_elf"; then
|
|
||||||
echoerr "Le binaire produit n'est pas du $attendu_elf :"
|
|
||||||
echoerr " $(file -b "$publish/$projet")"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " binaire ($attendu_elf), wwwroot et ressources d'index.html : OK"
|
|
||||||
|
|
||||||
#=================================================
|
|
||||||
# ARCHIVE
|
|
||||||
#=================================================
|
|
||||||
|
|
||||||
archive_nom="mabibli-$version-$runtime.tar.gz"
|
|
||||||
mkdir -p "$output_dir"
|
|
||||||
archive="$output_dir/$archive_nom"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== Archive ($arch) ==="
|
|
||||||
|
|
||||||
# Fichiers à la racine de l'archive, sans dossier intermédiaire : cohérent avec
|
|
||||||
# `in_subdir = false` dans manifest.toml.
|
|
||||||
#
|
|
||||||
# Options de reproductibilité : sans elles, deux compilations identiques donneraient
|
|
||||||
# deux sha256 différents (dates, ordre de parcours, uid/gid). En CI comme en local,
|
|
||||||
# on veut pouvoir vérifier qu'une archive correspond bien à un commit.
|
|
||||||
tar --create --gzip \
|
|
||||||
--file="$archive" \
|
|
||||||
--directory="$publish" \
|
|
||||||
--owner=0 --group=0 --numeric-owner \
|
|
||||||
--mtime="@${SOURCE_DATE_EPOCH:-0}" \
|
|
||||||
--sort=name \
|
|
||||||
.
|
|
||||||
|
|
||||||
somme="$(sha256sum "$archive" | cut -d' ' -f1)"
|
|
||||||
taille="$(du -h "$archive" | cut -f1)"
|
|
||||||
taille_octets="$(stat -c%s "$archive")"
|
|
||||||
|
|
||||||
echo " fichier : $archive"
|
|
||||||
echo " taille : $taille ($taille_octets octets)"
|
|
||||||
echo " sha256 : $somme"
|
|
||||||
|
|
||||||
sommes[$arch]="$somme"
|
|
||||||
archives[$arch]="$archive"
|
|
||||||
urls[$arch]="$base_url/v$version/$archive_nom"
|
|
||||||
|
|
||||||
# Le publish pèse ~150 Mo : le garder pendant la passe suivante doublerait l'occupation
|
|
||||||
# du disque pour rien, l'archive étant déjà produite.
|
|
||||||
rm -rf "$publish"
|
|
||||||
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$archive_seule" -eq 1 ]; then
|
|
||||||
echo
|
|
||||||
echo "=== Terminé (--archive-seule) ==="
|
|
||||||
echo
|
|
||||||
for runtime in "${runtimes[@]}"; do
|
|
||||||
arch="$(arch_ynh "$runtime")"
|
|
||||||
echo " $arch : ${archives[$arch]}"
|
|
||||||
echo " sha256 ${sommes[$arch]}"
|
|
||||||
done
|
|
||||||
cat <<FIN
|
|
||||||
|
|
||||||
Rien n'a été publié : manifest.toml est inchangé, aucun commit, aucun tag.
|
|
||||||
Pour publier réellement cette version, relancez sans --archive-seule.
|
|
||||||
|
|
||||||
FIN
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
#=================================================
|
|
||||||
# MANIFESTE
|
|
||||||
#=================================================
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "=== Mise à jour de manifest.toml ==="
|
|
||||||
|
|
||||||
# Le suffixe ~ynhN est conservé s'il existe déjà pour cette version, sinon
|
|
||||||
# remis à ~ynh1 : un changement de version applicative repart toujours de 1.
|
|
||||||
version_actuelle="$(sed -n 's/^version *= *"\([^"]*\)".*/\1/p' "$manifeste" | head -1)"
|
|
||||||
if [[ "$version_actuelle" == "$version~ynh"* ]]; then
|
|
||||||
version_paquet="$version_actuelle"
|
|
||||||
echo " version : $version_paquet (inchangée)"
|
|
||||||
else
|
|
||||||
version_paquet="$version~ynh1"
|
|
||||||
echo " version : $version_actuelle -> $version_paquet"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Les architectures sont passées à plat : arch, url, sha256, arch, url, sha256…
|
|
||||||
arguments=()
|
|
||||||
for runtime in "${runtimes[@]}"; do
|
|
||||||
arch="$(arch_ynh "$runtime")"
|
|
||||||
arguments+=("$arch" "${urls[$arch]}" "${sommes[$arch]}")
|
|
||||||
done
|
|
||||||
|
|
||||||
python3 - "$manifeste" "$version_paquet" "${arguments[@]}" <<'PY'
|
|
||||||
import re, sys
|
|
||||||
|
|
||||||
chemin, version = sys.argv[1:3]
|
|
||||||
reste = sys.argv[3:]
|
|
||||||
with open(chemin, encoding="utf-8") as f:
|
|
||||||
texte = f.read()
|
|
||||||
|
|
||||||
remplacements = [(r'^version = ".*"$', f'version = "{version}"')]
|
|
||||||
|
|
||||||
# ⚠️ Chaque motif DOIT correspondre exactement une fois. Une architecture déclarée au
|
|
||||||
# manifeste mais absente d'ici laisserait une URL périmée pointer sur la version
|
|
||||||
# précédente : le serveur téléchargerait une archive réelle, dont le sha256 ne
|
|
||||||
# correspondrait pas — ou pire, correspondrait, et installerait l'ancienne version.
|
|
||||||
for arch, url, somme in zip(reste[0::3], reste[1::3], reste[2::3]):
|
|
||||||
remplacements += [
|
|
||||||
(rf'^ {arch}\.url = ".*"$', f' {arch}.url = "{url}"'),
|
|
||||||
(rf'^ {arch}\.sha256 = ".*"$', f' {arch}.sha256 = "{somme}"'),
|
|
||||||
]
|
|
||||||
|
|
||||||
for motif, valeur in remplacements:
|
|
||||||
texte, n = re.subn(motif, valeur, texte, count=1, flags=re.MULTILINE)
|
|
||||||
if n != 1:
|
|
||||||
sys.exit(f"Motif introuvable dans le manifeste : {motif}")
|
|
||||||
|
|
||||||
with open(chemin, "w", encoding="utf-8") as f:
|
|
||||||
f.write(texte)
|
|
||||||
PY
|
|
||||||
|
|
||||||
for runtime in "${runtimes[@]}"; do
|
|
||||||
arch="$(arch_ynh "$runtime")"
|
|
||||||
echo " $arch : ${urls[$arch]}"
|
|
||||||
echo " sha256 ${sommes[$arch]}"
|
|
||||||
done
|
|
||||||
|
|
||||||
#=================================================
|
|
||||||
# LE PAQUET (le serveur lit Gitea, jamais la copie locale)
|
|
||||||
#=================================================
|
#=================================================
|
||||||
|
|
||||||
echo
|
echo
|
||||||
@@ -530,13 +172,13 @@ fi
|
|||||||
git -C "$racine_paquet" push
|
git -C "$racine_paquet" push
|
||||||
|
|
||||||
#=================================================
|
#=================================================
|
||||||
# LE CODE : tag et push
|
# 3. LE CODE : tag et push
|
||||||
#=================================================
|
#=================================================
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=== Code : tag v$version et push ==="
|
echo "=== Code : tag v$version et push ==="
|
||||||
git -C "$source_dir" tag "v$version"
|
git -C "$source_dir" tag "v$version"
|
||||||
git -C "$source_dir" push origin "$branche" "refs/tags/v$version"
|
git -C "$source_dir" push origin "$branche" --tags
|
||||||
|
|
||||||
# Un push qui échoue à demi ne se voit pas dans la sortie de git : on redemande au distant.
|
# Un push qui échoue à demi ne se voit pas dans la sortie de git : on redemande au distant.
|
||||||
verifier_pousse() { # verifier_pousse <dépôt> <ref>
|
verifier_pousse() { # verifier_pousse <dépôt> <ref>
|
||||||
@@ -557,24 +199,25 @@ verifier_pousse "$racine_paquet" "refs/heads/$(git -C "$racine_paquet" rev-parse
|
|||||||
# CE QUI RESTE À FAIRE À LA MAIN
|
# CE QUI RESTE À FAIRE À LA MAIN
|
||||||
#=================================================
|
#=================================================
|
||||||
|
|
||||||
|
url_archive="$base_url/v$version/$archive_nom"
|
||||||
|
|
||||||
cat <<FIN
|
cat <<FIN
|
||||||
|
|
||||||
=== Publié : $version ===
|
=== Publié : $version ===
|
||||||
|
|
||||||
Il ne reste qu'une chose, dans Gitea : créer la release v$version et y téléverser
|
Il ne reste qu'une chose, dans Gitea : créer la release v$version et y téléverser
|
||||||
LES DEUX archives. Une seule des deux téléversée, et le manifeste promet à l'autre
|
l'archive.
|
||||||
architecture une URL qui répondra 404 à l'installation.
|
|
||||||
|
|
||||||
Releases : $url_releases
|
Releases : $url_releases
|
||||||
$(for r in "${runtimes[@]}"; do a="$(arch_ynh "$r")"; echo " $a : ${archives[$a]}"; done)
|
Archive : $archive
|
||||||
|
|
||||||
Ensuite, vérifier que le manifeste pointe sur des fichiers réellement téléchargeables
|
Ensuite, vérifier que le manifeste pointe sur un fichier réellement téléchargeable
|
||||||
(sans être authentifié) :
|
(sans être authentifié) :
|
||||||
|
|
||||||
$(for r in "${runtimes[@]}"; do a="$(arch_ynh "$r")"; echo " curl -fsSLI \"${urls[$a]}\" | head -1"; done)
|
curl -fsSLI "$url_archive" | head -1
|
||||||
|
|
||||||
Puis, sur le serveur :
|
Puis, sur le serveur :
|
||||||
|
|
||||||
sudo yunohost app upgrade mabibli -u ${depot_code}_ynh
|
sudo yunohost app upgrade mabibli -u https://git.akbar.nohost.me/mathieu/mabibli_ynh
|
||||||
|
|
||||||
FIN
|
FIN
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=MaBibli — gestion de bibliothèque personnelle
|
Description=MaBibli — gestion de bibliothèque personnelle
|
||||||
Documentation=https://forge.example.org/mabibli
|
Documentation=https://git.akbar.nohost.me/mathieu/mabibli
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|||||||
+61
-47
@@ -1,68 +1,82 @@
|
|||||||
|
## Comment l'authentification fonctionne
|
||||||
|
|
||||||
|
MaBibli n'a **pas de connexion propre**. L'identité vient entièrement du portail YunoHost :
|
||||||
|
nginx authentifie le visiteur, puis SSOwat injecte dans la requête les en-têtes `YNH_USER`,
|
||||||
|
`YNH_USER_EMAIL` et `YNH_USER_FULLNAME`, que l'application se contente de lire.
|
||||||
|
|
||||||
|
Conséquences pratiques :
|
||||||
|
|
||||||
|
- **Qui peut voir la bibliothèque se règle dans les permissions YunoHost** (`mabibli.main`),
|
||||||
|
pas dans l'application.
|
||||||
|
- La collection est **commune** à toutes les personnes autorisées. Chaque livre garde une trace
|
||||||
|
de qui l'a saisi, mais personne n'est cloisonné : c'est une bibliothèque de foyer.
|
||||||
|
- Les **statuts de lecture sont personnels** (chacun sa progression), les **prêts sont communs**
|
||||||
|
(un livre absent l'est pour tout le monde).
|
||||||
|
- Se déconnecter du portail YunoHost ne déconnecte pas nécessairement des applications :
|
||||||
|
chacune garde sa propre session. C'est une limite connue de YunoHost, pas de MaBibli.
|
||||||
|
|
||||||
|
⚠️ **Ne pas exposer le port interne.** Le service écoute volontairement sur `127.0.0.1`
|
||||||
|
uniquement. L'application fait confiance à `YNH_USER` parce que SSOwat écrase cet en-tête à
|
||||||
|
chaque requête ; un service joignable directement permettrait à quiconque de forger
|
||||||
|
`YNH_USER` et de contourner le portail. Le port n'est pas ouvert au pare-feu, et
|
||||||
|
`ASPNETCORE_URLS` dans l'unité systemd ne doit jamais être élargi à `0.0.0.0` ou `*`.
|
||||||
|
|
||||||
|
## Domaine entier obligatoire
|
||||||
|
|
||||||
|
MaBibli s'installe **sur un domaine entier**, pas sous un sous-chemin (`/mabibli`).
|
||||||
|
|
||||||
|
Le client est une application Blazor WebAssembly : son chemin de base et les empreintes
|
||||||
|
d'intégrité de son service worker sont figés **à la compilation**. Comme le paquet installe une
|
||||||
|
archive déjà compilée — et ne compile jamais rien sur le serveur — il n'existe pas de moyen
|
||||||
|
propre de les réécrire à l'installation. YunoHost refusera donc un changement d'URL vers un
|
||||||
|
sous-chemin.
|
||||||
|
|
||||||
## Où vivent les données
|
## Où vivent les données
|
||||||
|
|
||||||
| Quoi | Où |
|
| Quoi | Où |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Binaires et client web | `/var/www/mabibli` |
|
| Binaires et client web | `/var/www/mabibli` (appartient à `root`, l'application ne peut pas s'y écrire) |
|
||||||
| Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` |
|
| Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` |
|
||||||
| Journaux | `journalctl -u mabibli` |
|
| Journaux | `journalctl -u mabibli` |
|
||||||
|
|
||||||
La base est séparée des binaires : une mise à jour remplace `/var/www/mabibli` sans toucher
|
La base est **délibérément séparée des binaires** : une mise à jour remplace intégralement
|
||||||
aux données, et le schéma est migré automatiquement au démarrage. Rien à lancer à la main.
|
`/var/www/mabibli` sans jamais toucher aux données. Le schéma est migré automatiquement au
|
||||||
|
démarrage, il n'y a aucune commande à lancer après une mise à jour.
|
||||||
|
|
||||||
## Qui a accès
|
## Sauvegarde
|
||||||
|
|
||||||
> ## ⚠️ MaBibli est une application **privée**
|
La base tourne en mode **WAL**. C'est important pour qui voudrait bricoler une sauvegarde à la
|
||||||
>
|
main : à un instant donné, l'essentiel des données peut se trouver dans `mabibli.db-wal` et
|
||||||
> Elle n'est **pas** faite pour être publique. L'installation la règle sur `all_users` —
|
**pas** dans `mabibli.db`. Mesuré sur une base fraîchement migrée, `mabibli.db` faisait 4 Ko —
|
||||||
> les comptes du serveur — et **ne pose pas la question**, parce qu'il n'y a pas de bonne
|
et ne contenait **aucune table** — pendant que le fichier `-wal` en portait 205 Ko.
|
||||||
> raison de l'ouvrir plus large.
|
|
||||||
>
|
|
||||||
> **N'ajoutez jamais le groupe `visitors` à la permission `mabibli.main`.**
|
|
||||||
>
|
|
||||||
> Deux raisons, et ce sont des propriétés du code, pas des préférences :
|
|
||||||
>
|
|
||||||
> 1. **L'application n'a aucune authentification à elle.** Elle lit l'en-tête `YNH_USER`
|
|
||||||
> que le portail YunoHost injecte à chaque requête, et lui fait confiance. Ouverte aux
|
|
||||||
> visiteurs, plus personne n'écrit cet en-tête : tout ce qui est **personnel** — statuts
|
|
||||||
> de lecture, liste d'envies — devient lisible et modifiable par n'importe qui.
|
|
||||||
> 2. **Toute personne autorisée peut tout modifier**, y compris **supprimer** des livres du
|
|
||||||
> catalogue commun. Il n'existe ni rôle, ni lecture seule, ni journal des suppressions.
|
|
||||||
> C'est assumé pour un foyer, dont les membres se connaissent.
|
|
||||||
>
|
|
||||||
> Restreindre plus (un groupe dédié) est parfaitement légitime, et se fait dans les
|
|
||||||
> permissions YunoHost (`mabibli.main`). C'est **l'élargissement** qui est un contresens.
|
|
||||||
|
|
||||||
MaBibli n'a pas de connexion propre : l'identité vient du portail YunoHost. **Réglez les accès
|
Le script de sauvegarde du paquet ne copie donc pas les fichiers tels quels : il demande à
|
||||||
dans les permissions YunoHost** (`mabibli.main`), pas dans l'application.
|
SQLite un instantané cohérent (`.backup`, l'API de sauvegarde en ligne), déposé à côté de la
|
||||||
|
base sous le nom `mabibli-instantane.db`. C'est ce fichier que la restauration remet en place,
|
||||||
|
en écartant au passage les `-wal` / `-shm` de l'archive, qui décrivaient l'état d'une autre
|
||||||
|
copie de la base.
|
||||||
|
|
||||||
La collection est **commune** à toutes les personnes autorisées. Les statuts de lecture sont
|
Le service **n'est pas arrêté** pendant la sauvegarde : l'API de sauvegarde en ligne garantit la
|
||||||
personnels, les prêts sont communs.
|
cohérence du fichier produit sans bloquer les lectures, et couper l'application à chaque
|
||||||
|
sauvegarde nocturne coûterait une indisponibilité pour rien.
|
||||||
|
|
||||||
⚠️ **Ne jamais élargir l'écoute du service.** Il écoute sur `127.0.0.1` uniquement, et c'est
|
Pour une sauvegarde manuelle, la bonne commande est donc :
|
||||||
une frontière de sécurité : l'application fait confiance à l'en-tête `YNH_USER` parce que
|
|
||||||
SSOwat l'écrase à chaque requête. Un service joignable directement permettrait de le forger.
|
|
||||||
|
|
||||||
## Sauvegarde manuelle
|
|
||||||
|
|
||||||
La base est en mode WAL : **un `cp` du fichier `.db` peut ne rien sauvegarder du tout.**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'"
|
sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'"
|
||||||
```
|
```
|
||||||
|
|
||||||
`yunohost backup create` fait déjà ce qu'il faut, sans arrêter le service.
|
et surtout pas un `cp` du seul fichier `.db`.
|
||||||
|
|
||||||
## Ce dont le serveur a besoin
|
## Le scan du code-barres exige HTTPS
|
||||||
|
|
||||||
- **Accès sortant HTTPS** vers `catalogue.bnf.fr` et `openlibrary.org`, pour pré-remplir les
|
L'accès à la caméra n'est autorisé par les navigateurs que dans un contexte sécurisé. En
|
||||||
fiches depuis un ISBN. Sans lui l'application marche, mais toute saisie devient manuelle.
|
production, le certificat Let's Encrypt de YunoHost suffit. En revanche, joindre le serveur par
|
||||||
- **HTTPS** pour le scan du code-barres : les navigateurs n'ouvrent la caméra qu'en contexte
|
son IP locale (`http://192.168.x.x`) fera **toujours** échouer le scan : ce n'est pas un
|
||||||
sécurisé. Le certificat Let's Encrypt de YunoHost suffit ; joindre le serveur par son IP
|
contexte sécurisé. La saisie manuelle de l'ISBN reste disponible dans tous les cas.
|
||||||
locale fera toujours échouer le scan. La saisie manuelle reste disponible.
|
|
||||||
- **Un domaine entier.** MaBibli ne s'installe pas sous un sous-chemin : le client Blazor fige
|
|
||||||
son chemin de base à la compilation, et rien n'est compilé sur le serveur.
|
|
||||||
|
|
||||||
---
|
## Accès sortant nécessaire
|
||||||
|
|
||||||
Le détail — conception du paquet, publication, mise à jour pas à pas, retour arrière —
|
Le serveur doit pouvoir joindre `catalogue.bnf.fr` et `openlibrary.org` en HTTPS pour
|
||||||
est dans le [README du dépôt du code](https://forge.example.org/mabibli#readme).
|
pré-remplir les fiches à partir d'un ISBN. Sans accès sortant, l'application fonctionne, mais
|
||||||
|
toute saisie devient manuelle.
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
## Comment l'authentification fonctionne
|
||||||
|
|
||||||
|
MaBibli n'a **pas de connexion propre**. L'identité vient entièrement du portail YunoHost :
|
||||||
|
nginx authentifie le visiteur, puis SSOwat injecte dans la requête les en-têtes `YNH_USER`,
|
||||||
|
`YNH_USER_EMAIL` et `YNH_USER_FULLNAME`, que l'application se contente de lire.
|
||||||
|
|
||||||
|
Conséquences pratiques :
|
||||||
|
|
||||||
|
- **Qui peut voir la bibliothèque se règle dans les permissions YunoHost** (`mabibli.main`),
|
||||||
|
pas dans l'application.
|
||||||
|
- La collection est **commune** à toutes les personnes autorisées. Chaque livre garde une trace
|
||||||
|
de qui l'a saisi, mais personne n'est cloisonné : c'est une bibliothèque de foyer.
|
||||||
|
- Les **statuts de lecture sont personnels** (chacun sa progression), les **prêts sont communs**
|
||||||
|
(un livre absent l'est pour tout le monde).
|
||||||
|
- Se déconnecter du portail YunoHost ne déconnecte pas nécessairement des applications :
|
||||||
|
chacune garde sa propre session. C'est une limite connue de YunoHost, pas de MaBibli.
|
||||||
|
|
||||||
|
⚠️ **Ne pas exposer le port interne.** Le service écoute volontairement sur `127.0.0.1`
|
||||||
|
uniquement. L'application fait confiance à `YNH_USER` parce que SSOwat écrase cet en-tête à
|
||||||
|
chaque requête ; un service joignable directement permettrait à quiconque de forger
|
||||||
|
`YNH_USER` et de contourner le portail. Le port n'est pas ouvert au pare-feu, et
|
||||||
|
`ASPNETCORE_URLS` dans l'unité systemd ne doit jamais être élargi à `0.0.0.0` ou `*`.
|
||||||
|
|
||||||
|
## Domaine entier obligatoire
|
||||||
|
|
||||||
|
MaBibli s'installe **sur un domaine entier**, pas sous un sous-chemin (`/mabibli`).
|
||||||
|
|
||||||
|
Le client est une application Blazor WebAssembly : son chemin de base et les empreintes
|
||||||
|
d'intégrité de son service worker sont figés **à la compilation**. Comme le paquet installe une
|
||||||
|
archive déjà compilée — et ne compile jamais rien sur le serveur — il n'existe pas de moyen
|
||||||
|
propre de les réécrire à l'installation. YunoHost refusera donc un changement d'URL vers un
|
||||||
|
sous-chemin.
|
||||||
|
|
||||||
|
## Où vivent les données
|
||||||
|
|
||||||
|
| Quoi | Où |
|
||||||
|
|---|---|
|
||||||
|
| Binaires et client web | `/var/www/mabibli` (appartient à `root`, l'application ne peut pas s'y écrire) |
|
||||||
|
| Base SQLite | `/home/yunohost.app/mabibli/mabibli.db` |
|
||||||
|
| Journaux | `journalctl -u mabibli` |
|
||||||
|
|
||||||
|
La base est **délibérément séparée des binaires** : une mise à jour remplace intégralement
|
||||||
|
`/var/www/mabibli` sans jamais toucher aux données. Le schéma est migré automatiquement au
|
||||||
|
démarrage, il n'y a aucune commande à lancer après une mise à jour.
|
||||||
|
|
||||||
|
## Sauvegarde
|
||||||
|
|
||||||
|
La base tourne en mode **WAL**. C'est important pour qui voudrait bricoler une sauvegarde à la
|
||||||
|
main : à un instant donné, l'essentiel des données peut se trouver dans `mabibli.db-wal` et
|
||||||
|
**pas** dans `mabibli.db`. Mesuré sur une base fraîchement migrée, `mabibli.db` faisait 4 Ko —
|
||||||
|
et ne contenait **aucune table** — pendant que le fichier `-wal` en portait 205 Ko.
|
||||||
|
|
||||||
|
Le script de sauvegarde du paquet ne copie donc pas les fichiers tels quels : il demande à
|
||||||
|
SQLite un instantané cohérent (`.backup`, l'API de sauvegarde en ligne), déposé à côté de la
|
||||||
|
base sous le nom `mabibli-instantane.db`. C'est ce fichier que la restauration remet en place,
|
||||||
|
en écartant au passage les `-wal` / `-shm` de l'archive, qui décrivaient l'état d'une autre
|
||||||
|
copie de la base.
|
||||||
|
|
||||||
|
Le service **n'est pas arrêté** pendant la sauvegarde : l'API de sauvegarde en ligne garantit la
|
||||||
|
cohérence du fichier produit sans bloquer les lectures, et couper l'application à chaque
|
||||||
|
sauvegarde nocturne coûterait une indisponibilité pour rien.
|
||||||
|
|
||||||
|
Pour une sauvegarde manuelle, la bonne commande est donc :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 /home/yunohost.app/mabibli/mabibli.db ".backup '/quelque/part/mabibli.db'"
|
||||||
|
```
|
||||||
|
|
||||||
|
et surtout pas un `cp` du seul fichier `.db`.
|
||||||
|
|
||||||
|
## Le scan du code-barres exige HTTPS
|
||||||
|
|
||||||
|
L'accès à la caméra n'est autorisé par les navigateurs que dans un contexte sécurisé. En
|
||||||
|
production, le certificat Let's Encrypt de YunoHost suffit. En revanche, joindre le serveur par
|
||||||
|
son IP locale (`http://192.168.x.x`) fera **toujours** échouer le scan : ce n'est pas un
|
||||||
|
contexte sécurisé. La saisie manuelle de l'ISBN reste disponible dans tous les cas.
|
||||||
|
|
||||||
|
## Accès sortant nécessaire
|
||||||
|
|
||||||
|
Le serveur doit pouvoir joindre `catalogue.bnf.fr` et `openlibrary.org` en HTTPS pour
|
||||||
|
pré-remplir les fiches à partir d'un ISBN. Sans accès sortant, l'application fonctionne, mais
|
||||||
|
toute saisie devient manuelle.
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
MaBibli est une application de gestion de bibliothèque personnelle, pensée pour un foyer.
|
MaBibli est une application de gestion de bibliothèque personnelle, pensée pour un foyer.
|
||||||
|
|
||||||
⚠️ **Application privée.** L'accès est réservé aux comptes du serveur (`all_users`) et
|
|
||||||
l'installation ne pose pas la question. MaBibli n'a pas de connexion propre — elle fait
|
|
||||||
confiance au portail YunoHost — et toute personne autorisée peut modifier et supprimer
|
|
||||||
n'importe quel livre. **Ne l'ouvrez pas aux visiteurs.**
|
|
||||||
|
|
||||||
- **Catalogue** des livres physiques et des ebooks (fiches uniquement, aucun fichier n'est hébergé).
|
- **Catalogue** des livres physiques et des ebooks (fiches uniquement, aucun fichier n'est hébergé).
|
||||||
- **Prêts** : à qui le livre a été confié, depuis quand, et l'historique complet des prêts passés.
|
- **Prêts** : à qui le livre a été confié, depuis quand, et l'historique complet des prêts passés.
|
||||||
- **Scan ISBN** au code-barres depuis le téléphone, ou saisie manuelle, avec pré-remplissage automatique du titre, de l'auteur, de l'éditeur et de la couverture.
|
- **Scan ISBN** au code-barres depuis le téléphone, ou saisie manuelle, avec pré-remplissage automatique du titre, de l'auteur, de l'éditeur et de la couverture.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
MaBibli est une application de gestion de bibliothèque personnelle, pensée pour un foyer.
|
||||||
|
|
||||||
|
- **Catalogue** des livres physiques et des ebooks (fiches uniquement, aucun fichier n'est hébergé).
|
||||||
|
- **Prêts** : à qui le livre a été confié, depuis quand, et l'historique complet des prêts passés.
|
||||||
|
- **Scan ISBN** au code-barres depuis le téléphone, ou saisie manuelle, avec pré-remplissage automatique du titre, de l'auteur, de l'éditeur et de la couverture.
|
||||||
|
- **Statuts de lecture** personnels : chaque membre du foyer suit sa propre progression sur une collection commune.
|
||||||
|
- **Consultation hors-ligne** : l'application est une PWA installable, et la bibliothèque reste consultable sans réseau.
|
||||||
|
|
||||||
|
Les métadonnées viennent du catalogue de la **BnF** puis d'**OpenLibrary**, deux sources libres et sans clé d'API. Aucune dépendance à Google Books.
|
||||||
+17
-83
@@ -7,7 +7,7 @@ name = "MaBibli"
|
|||||||
description.fr = "Gestion de bibliothèque personnelle : catalogue, prêts et scan ISBN"
|
description.fr = "Gestion de bibliothèque personnelle : catalogue, prêts et scan ISBN"
|
||||||
description.en = "Personal library manager: catalogue, loans and ISBN scanning"
|
description.en = "Personal library manager: catalogue, loans and ISBN scanning"
|
||||||
|
|
||||||
version = "1.2.0~ynh1"
|
version = "0.4.1~ynh1"
|
||||||
|
|
||||||
maintainers = ["mathieu"]
|
maintainers = ["mathieu"]
|
||||||
|
|
||||||
@@ -19,13 +19,10 @@ code = "https://git.akbar.nohost.me/mathieu/mabibli"
|
|||||||
yunohost = ">= 12.1.38"
|
yunohost = ">= 12.1.38"
|
||||||
helpers_version = "2.1"
|
helpers_version = "2.1"
|
||||||
|
|
||||||
# ⚠️ Une architecture déclarée ici DOIT avoir son archive dans la release : YunoHost
|
# x86_64 uniquement : CLAUDE.md acte un serveur PC/VPS, et la chaîne de publication
|
||||||
# choisit `<arch>.url` d'après la machine, et une URL manquante échoue chez celui qui
|
# ne produit qu'un binaire `linux-x64`. Déclarer d'autres architectures promettrait
|
||||||
# installe, dans `ynh_setup_source`. `build/publier.sh` compile les deux à chaque
|
# des archives qui n'existent pas.
|
||||||
# publication, précisément pour qu'aucune des deux ne puisse être oubliée.
|
architectures = ["amd64"]
|
||||||
#
|
|
||||||
# armhf (32 bits) n'est PAS déclarée : .NET ne la prend plus en charge.
|
|
||||||
architectures = ["amd64", "arm64"]
|
|
||||||
|
|
||||||
# Une seule instance : la bibliothèque est commune au foyer (CLAUDE.md, « Portée des
|
# Une seule instance : la bibliothèque est commune au foyer (CLAUDE.md, « Portée des
|
||||||
# données »). Deux instances voudraient dire deux collections étanches, ce qui est
|
# données »). Deux instances voudraient dire deux collections étanches, ce qui est
|
||||||
@@ -40,33 +37,8 @@ sso = true
|
|||||||
# ~170 Mo de publish self-contained (runtime .NET embarqué), plus la base et la marge
|
# ~170 Mo de publish self-contained (runtime .NET embarqué), plus la base et la marge
|
||||||
# de décompression de l'archive.
|
# de décompression de l'archive.
|
||||||
disk = "500M"
|
disk = "500M"
|
||||||
|
|
||||||
# ⚠️ `ram.build` ne désigne AUCUNE compilation : rien n'est compilé sur le serveur, c'est
|
|
||||||
# tout l'intérêt du self-contained. Il couvre le téléchargement et la décompression de
|
|
||||||
# l'archive de 69 Mo. Ne pas aller chercher la valeur du côté d'un build .NET.
|
|
||||||
ram.build = "50M"
|
ram.build = "50M"
|
||||||
|
ram.runtime = "200M"
|
||||||
# Mesuré sur le serveur le 2026-08-22, service en marche :
|
|
||||||
# cat /sys/fs/cgroup/system.slice/mabibli.service/memory.peak -> 208 793 600 o
|
|
||||||
# soit 199,1 Mio de pic, contre 200M déclarés jusqu'ici : 0,4 % de marge, c'est-à-dire
|
|
||||||
# aucune. Porté à 256M.
|
|
||||||
#
|
|
||||||
# ⚠️ `systemctl show -p MemoryPeak` ne renvoie RIEN sur ce serveur — la commande réussit
|
|
||||||
# en silence et l'on croit lire un pic là où l'on ne lit rien. Passer par le cgroup.
|
|
||||||
#
|
|
||||||
# ⚠️ Le chiffre MAJORE le besoin : `memory.peak` compte le cache de fichiers, donc une
|
|
||||||
# part des 69 Mo de binaire mappé, récupérable sous pression — et le GC de .NET se serre
|
|
||||||
# sur une machine étroite. Mais YunoHost compare une déclaration statique à la RAM
|
|
||||||
# disponible, sans faire cette nuance. L'asymétrie tranche : un refus d'installation est
|
|
||||||
# visible et se contourne, un OOM survient plus tard, sous une opération lourde, sans que
|
|
||||||
# rien ne désigne la cause.
|
|
||||||
#
|
|
||||||
# ⚠️ Le pic n'est PAS le lookup ISBN, contrairement à l'intuition — une notice. C'est
|
|
||||||
# « Nouveautés » sur un auteur très réédité : jusqu'à dix pages de 100 notices SRU en
|
|
||||||
# parallèle, plus le catalogue entier chargé pour le rapprochement. Ce cas-là n'a pas
|
|
||||||
# encore été mesuré, et il croît avec la taille de la bibliothèque : à reprendre le jour
|
|
||||||
# où le fonds aura beaucoup grossi.
|
|
||||||
ram.runtime = "256M"
|
|
||||||
|
|
||||||
[install]
|
[install]
|
||||||
|
|
||||||
@@ -86,34 +58,23 @@ ram.runtime = "256M"
|
|||||||
# le cœur classe l'app en « full_domain » et refusera un changement d'URL vers
|
# le cœur classe l'app en « full_domain » et refusera un changement d'URL vers
|
||||||
# un sous-chemin.
|
# un sous-chemin.
|
||||||
|
|
||||||
# Pas de question `init_main_permission` : l'accès est fixé à `all_users` par la
|
[install.init_main_permission]
|
||||||
# ressource `permissions` ci-dessous, et l'installation ne demande donc rien.
|
type = "group"
|
||||||
#
|
default = "all_users"
|
||||||
# Vérifié dans le cœur YunoHost (`src/utils/resources.py`) :
|
help.fr = "Qui peut accéder à la bibliothèque. MaBibli n'a pas de connexion propre : l'identité vient du portail YunoHost, et la collection est commune à toutes les personnes autorisées."
|
||||||
#
|
help.en = "Who may access the library. MaBibli has no login of its own: identity comes from the YunoHost portal, and the collection is shared between all allowed users."
|
||||||
# init_allowed = infos["allowed"] or self.get_setting(f"init_{perm}_permission") or []
|
|
||||||
#
|
|
||||||
# `allowed` l'emporte sur la question ; garder la question l'aurait donc posée pour
|
|
||||||
# rien, en laissant croire que la réponse comptait.
|
|
||||||
#
|
|
||||||
# ⚠️ Ce code ne s'exécute qu'à la CRÉATION de la permission. Une montée de version ne
|
|
||||||
# réaligne rien : un accès élargi à la main le reste.
|
|
||||||
|
|
||||||
[resources]
|
[resources]
|
||||||
|
|
||||||
[resources.sources.main]
|
[resources.sources.main]
|
||||||
# Archives **déjà compilées** (publish self-contained), produites par
|
# Archive **déjà compilée** (publish self-contained linux-x64), produite par
|
||||||
# `build/publier.sh` et déposées en release sur le Gitea de l'utilisateur.
|
# `build/publier-release.sh` et déposée en release sur le Gitea de l'utilisateur.
|
||||||
# Le SDK .NET n'est jamais installé sur le serveur : c'est tout l'intérêt du
|
# Le SDK .NET n'est jamais installé sur le serveur : c'est tout l'intérêt du
|
||||||
# self-contained (CLAUDE.md, « Décisions techniques actées »).
|
# self-contained (CLAUDE.md, « Ne jamais compiler sur le serveur »).
|
||||||
#
|
#
|
||||||
# YunoHost retient la ligne correspondant à l'architecture de la machine.
|
# Ces deux valeurs sont réécrites par `build/publier-release.sh`.
|
||||||
#
|
amd64.url = "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v0.4.1/mabibli-0.4.1-linux-x64.tar.gz"
|
||||||
# ⚠️ Ces quatre valeurs sont réécrites par `build/publier.sh`, jamais à la main.
|
amd64.sha256 = "21f6a437dcd118ed863d24ce1137f711ca19e004b5e89b6d482dcfd07ab7a884"
|
||||||
amd64.url = "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v1.2.0/mabibli-1.2.0-linux-x64.tar.gz"
|
|
||||||
amd64.sha256 = "daac8651a51b252aa1d8e569b195a5b62fc5de4149589d52e21bf0ad7b699acd"
|
|
||||||
arm64.url = "https://git.akbar.nohost.me/mathieu/mabibli/releases/download/v1.2.0/mabibli-1.2.0-linux-arm64.tar.gz"
|
|
||||||
arm64.sha256 = "d3c3e31338745d8373664a50c1bb72f849d04f05c824a6c31801f5ecf8365db6"
|
|
||||||
|
|
||||||
# L'archive dépose ses fichiers à la racine, sans dossier intermédiaire.
|
# L'archive dépose ses fichiers à la racine, sans dossier intermédiaire.
|
||||||
in_subdir = false
|
in_subdir = false
|
||||||
@@ -144,33 +105,6 @@ ram.runtime = "256M"
|
|||||||
main.url = "/"
|
main.url = "/"
|
||||||
main.auth_header = true
|
main.auth_header = true
|
||||||
|
|
||||||
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
||||||
# ║ ⚠️ MaBibli EST UNE APPLICATION PRIVÉE. NE JAMAIS L'OUVRIR AUX `visitors`. ║
|
|
||||||
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
|
||||||
#
|
|
||||||
# Ce n'est pas une préférence de confidentialité, c'est une propriété du code, et
|
|
||||||
# elle vaut sur les DEUX moitiés de l'application :
|
|
||||||
#
|
|
||||||
# • L'application n'a AUCUNE authentification propre. Elle lit `YNH_USER`, un
|
|
||||||
# en-tête que SSOwat injecte, et fait confiance à ce qu'il contient. Ouverte aux
|
|
||||||
# visiteurs, il n'y a plus personne pour l'écraser : tout ce qui est PERSONNEL
|
|
||||||
# (statuts de lecture, liste d'envies) devient lisible et modifiable par le
|
|
||||||
# premier venu. C'est la même frontière que « le service n'écoute que sur
|
|
||||||
# 127.0.0.1 » — SSOwat n'est pas un confort, c'est la seule porte.
|
|
||||||
#
|
|
||||||
# • Il n'existe AUCUNE notion d'écriture réservée. Toute personne autorisée peut
|
|
||||||
# ajouter, modifier et SUPPRIMER n'importe quel livre du catalogue commun, ainsi
|
|
||||||
# que noter un prêt ou son retour. C'est délibéré — un foyer, pas un service
|
|
||||||
# public — et cela suppose que les personnes autorisées se connaissent.
|
|
||||||
#
|
|
||||||
# `all_users` est donc le plus large qui ait du sens : les comptes du serveur.
|
|
||||||
# Restreindre davantage (un groupe dédié) est légitime ; élargir ne l'est pas.
|
|
||||||
#
|
|
||||||
# ⚠️ Volontairement PAS `protected = true` : l'administrateur du serveur reste
|
|
||||||
# maître de ses permissions, et peut vouloir restreindre à un groupe. C'est
|
|
||||||
# l'élargissement qui est un contresens, pas le réglage lui-même.
|
|
||||||
main.allowed = "all_users"
|
|
||||||
|
|
||||||
[resources.ports]
|
[resources.ports]
|
||||||
# Port interne uniquement. `exposed` vaut `false` par défaut : le pare-feu reste
|
# Port interne uniquement. `exposed` vaut `false` par défaut : le pare-feu reste
|
||||||
# fermé, et l'unité systemd n'écoute que sur 127.0.0.1.
|
# fermé, et l'unité systemd n'écoute que sur 127.0.0.1.
|
||||||
|
|||||||
Reference in New Issue
Block a user