Compare commits

...
Author SHA1 Message Date
koka 235cf51d6e Document Sanctuary Web alignment with the Beta database
Build Sanctuary / build (push) Waiting to run
2026-10-25 05:09:39 +01:00
koka 17c1cd9e71 Verify permanent Beta route and retire UPnP fallback
Build Sanctuary / build (push) Waiting to run
2026-10-25 04:51:51 +01:00
koka 276c8f2144 Record Beta server deployment, web connections and remaining Bbox target
Build Sanctuary / build (push) Waiting to run
2026-10-25 04:44:08 +01:00
koka 23b295356b Embed JEI and add a managed dedicated server pack (beta.280)
Build Sanctuary / build (push) Waiting to run
2026-10-25 04:17:18 +01:00
15 changed files with 654 additions and 40 deletions
+11
View File
@@ -1,5 +1,16 @@
# Sanctuary
## beta.280 — JEI réintégré et packwiz serveur
[Un seul JAR Sanctuary avec le fork JEI et un profil packwiz serveur](docs/jei-server-beta280.md).
Client et serveur partagent le même artefact ; le profil serveur ne contient
que Sanctuary et Fabric API. Le [kit Linux](docs/server-pack.md) prépare les
mises à jour et la connexion aux services Web existants. Build, 13 GameTests,
sept contrôles de l'outil de mise à jour, installation packwiz et démarrage du
JAR de production vérifiés. La nouvelle CT 350 sur `gpu` accueille le serveur
de 20 places, relié à l'API, à la communauté et à la console privée du site.
[Release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280).
Les **quatre expansions validées sont disponibles dans Sanctuary normal** :
Nord glacial, Sud des canyons, Ouest océanique et Est volcanique.
Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
+4 -2
View File
@@ -113,8 +113,10 @@ aucune copie des textures de mobs ni aucun ancien pouvoir n’est embarqué.
Just Enough Items, mezz, MIT : fork source de la branche 26.2, commit
`aae2dfcfb82e6b5bce787ba72bb2eda7339b274f`, adapté à Minecraft 26.3-pre-2.
Depuis beta.257, le fork Minecraft 26.3 `30.32.0-sanctuary.4` est un JAR client
séparé et facultatif, avec la licence d’origine et [la provenance du portage](mods/jei/PROVENANCE.md).
Depuis beta.280, le fork Minecraft 26.3 `30.32.0-sanctuary.4` est à nouveau
embarqué dans le JAR Sanctuary, avec la licence d’origine et
[la provenance du portage](mods/jei/PROVENANCE.md). Sa distribution séparée
facultative concernait les beta.257–278.
Les patches et la reconstruction sont fournis ; les téléchargements restent ignorés.
Baked Substring Index 0.1.0 (MIT, James Mitchell) et Suffix Tree 1.1.0
(Apache-2.0, Alessandro Bahgat Shehata) sont inclus avec leurs licences.
+13
View File
@@ -15,6 +15,12 @@ allprojects {
tasks.named('build') { dependsOn(':sanctuary:build', ':jei:build', ':sanctuary-test:build') }
tasks.named('check') { dependsOn(':sanctuary:check', ':jei:check', ':sanctuary-test:check', 'verifyPack') }
tasks.register('verifyServerUpdater', Exec) {
group = 'verification'
commandLine('python3', 'scripts/test_server_runtime.py')
}
tasks.named('check') { dependsOn('verifyServerUpdater') }
tasks.register('verifyPack', Exec) {
group = 'verification'
description = 'Verify pinned pack dependencies, versions and index hashes.'
@@ -28,6 +34,13 @@ tasks.register('assemblePack', Exec) {
commandLine('python3', 'scripts/pack.py', 'assemble')
}
tasks.register('assembleServerPack', Exec) {
group = 'distribution'
description = 'Build the Linux server kit and verify its packwiz profile.'
dependsOn('assemblePack')
commandLine('python3', 'scripts/server_pack.py')
}
tasks.register('verifyResourcePack', Exec) {
group = 'verification'
description = 'Verify the versioned Sanctuary texture sources and their release hashes.'
+189
View File
@@ -0,0 +1,189 @@
# DIST-280 — JEI embarqué et packwiz serveur
Branche `codex/jei-packwiz-server`, base commit `697e4a2` (beta.278).
Version réservée `beta.280` : beta.279 appartient au chantier parallèle
Map Island Zoom, dont les modifications ne sont pas reprises ici.
## Contrat
- Un seul JAR Sanctuary à distribuer, avec le fork JEI exact
`30.32.0-sanctuary.4` et ses notices/bibliothèques embarqués. Fabric API reste
une dépendance commune ; Sodium et Sound Physics restent facultatifs côté client.
- Même JAR sur client et serveur. JEI possède une initialisation commune
(recettes et transfert), ses écrans restent dans son entrée client.
- Un manifeste serveur `server/pack.toml` sur le même canal packwiz que le
client, avec uniquement Sanctuary et Fabric API. Même version, URL et hash
du JAR Sanctuary dans les deux profils. Publication atomique des manifestes
après vérification de l'artefact immuable.
- Un kit serveur local avec mise à jour packwiz sans interface et lancement
Fabric. Les mondes, configurations, permissions, listes de joueurs et secrets
restent hors de l'index. Une erreur de mise à jour empêche le démarrage.
- Documenter les paramètres existants de l'API Sanctuary Web et de la MariaDB
communautaire, sans inventer d'API ni migrer les données ou configurer un site
public implicitement. Les secrets restent dans l'environnement du service.
## Vérifications locales
`./gradlew check build assemblePack assembleServerPack
-PsanctuaryFocusedTests=recipes,progression -PsanctuaryAtlasOnly=true` réussit :
13 GameTests requis, contrôles purs du dépôt et assemblages. Le fixture ciblé
évite le harnais worldgen historique `unified`, sans modifier sa génération.
Les sept tests de l'outil de mise à jour passent séparément, y compris le
relais du signal d'arrêt et l'attente de Java avant libération du verrou.
Installation réelle avec packwiz, deux synchronisations et six fichiers
personnels témoins conservés octet par octet. Le JAR de production démarre et
s'arrête dans un monde plat de développement neuf avec le JEI imbriqué ;
celui-ci correspond au fork construit. Minecraft normalise lui-même les fichiers
`server.properties`, `ops.json` et `whitelist.json` au lancement : leurs valeurs
sont vérifiées après cet arrêt, distinctement de la préservation par packwiz.
La transition du profil client retire réellement le JEI séparé suivi par
packwiz et conserve les favoris. Les manifestes de publication client/serveur
référencent le même artefact. Aucun nouveau parcours graphique n'est revendiqué ;
les contrôles historiques Vulkan de l'interface ne sont pas rejoués ici.
Journaux et reçus ignorés : `build/jei-server-final.log`,
`build/server280-updater-final.log`, `build/server280-validation-delivery/validation.json`,
`build/client280-validation/validation.json`.
## Serveur dédié demandé le 25 octobre 2026
Le créateur demande une **nouvelle CT pour 20 joueurs** sur le nœud `gpu`.
La CT 350 `sanctuary-beta-20` est créée à cette fin : Debian 13 amd64,
6 vCPU, 8 Gio de RAM, disque 80 Gio, non privilégiée. Le budget Java retenu est
6 Gio, avec 20 places, portée 6 et simulation 4 ; ce dimensionnement n'est pas
une mesure de charge avec vingt joueurs connectés.
L'Alpha (CT 300), les CT 330 et 340 et les mondes existants restent hors cible.
Le nouveau monde utilisera `sanctuary:sanctuary` (réglages initiaux
`sanctuary:island231_medium`), graine `28020261025`, version beta.280.
Aucune expansion n'est activée par le déploiement. Le site est dans la CT 320 ;
son API et son schéma communautaire 3 sont présents.
## Livraison et mise en service
La [release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280)
est publiée depuis le commit `23b295356b499404e0056affbb5d3b3811c1b01f`.
Le canal packwiz est avancé au commit `338e8a62387048c7e703ef036fda759ef57b4130`,
avec les profils client et serveur dans le même commit. Aucun JEI séparé n'est
joint. Le MRpack contient le seul JAR Sanctuary en override, et trois dépendances
téléchargées (Fabric API, Sodium et Sound Physics).
| Artefact | SHA-256 |
| --- | --- |
| `sanctuary-beta.280.jar` | `489dade38218d34d2a4502fa4ffe71f0bc13035948dc1c52b633b528e9855c7e` |
| `Sanctuary-Server-beta.280.zip` | `54ed73e75b53305a9f0cc5505f3d64624d23598f906ac69d121030255ced0d5a` |
| `Sanctuary-beta.280.mrpack` | `b23ba2d9cf72d27da2595e3f4a32a36656f71abf2d2cb4d6cefd7f9d29e4cd01` |
Le kit téléchargé dans la nouvelle CT correspond à cet artefact immuable.
Java 25, Python 3.13 et le service `sanctuary-beta.service` sont installés ;
le service est activé au démarrage. Le premier monde Sanctuary a été créé à
la graine annoncée, avec le générateur 231, taille Medium, en Survie et
`online-mode=true`. La réponse réseau Minecraft annonce 26.3, protocole 777,
**20 places**, et le JEI embarqué est chargé côté serveur.
La synchronisation packwiz a été exécutée après arrêt propre du nouveau serveur :
**61 fichiers de monde, de configuration et de secrets gardent leurs hashes**.
Le même monde redémarre ensuite. L'outil maintient son verrou jusqu'à la fin
de la sauvegarde Java, y compris lorsque systemd transmet SIGTERM.
L'API du site accepte l'authentification et refuse un corps de vérification
vide avec HTTP 422 : aucun code d'habitant n'est consommé. L'accueil serveur
confirme que les codes sont requis. Un compte MariaDB propre à Beta a été créé,
restreint à son hôte et aux sept tables nécessaires, avec TLS obligatoire ;
Connector/J utilise `sslMode=verify-full`. Le schéma existant 3 n'est pas migré.
La ligne de statistiques du nouveau serveur est alimentée toutes les 30 secondes.
La console du site était non configurée. Seules ses trois variables RCON ont
été renseignées pour Beta, après sauvegarde privée ; le cache Laravel est
reconstruit sans redémarrer le site. La commande de lecture `list`, appelée
par le vrai service PHP, répond « 0 of a max of 20 ». Le port RCON est filtré
dans la CT Beta : accès du site et de loopback uniquement, refus depuis un autre
client LAN. Aucune redirection publique RCON n'est créée.
Les secrets restent dans `/etc/sanctuary/beta.env`, le `server.properties`
privé et l'environnement Web, jamais dans Git ou packwiz. Les unités
`sanctuary-beta-rcon-firewall.service` et le complément systemd associé
appartiennent à cette installation, hors de l'index de mods.
Le domaine demandé est `beta.sanctuary-minecraft.net`. Les deux enregistrements
OVH sont publiés : A pour Beta et SRV `_minecraft._tcp.beta` vers le port 25567,
TTL 300. Les quatorze enregistrements DNS préexistants sont conservés. Une
réservation DHCP lie l'adresse de la CT à sa MAC ; elle est activée après test
de syntaxe et redémarrage de dnsmasq, sans changer ses deux fichiers existants.
Aucun serveur Minecraft ou proxy existant n'est redémarré par cette opération.
Une redirection UPnP Beta seule a servi d'amorçage. Le 25 octobre, le créateur
réenregistre la règle Bbox permanente « Sanctuary beta 20 » : **port externe
25567 → 192.168.1.122:25565**. L'API de la Bbox déclare encore `protocol=all`
(TCP/UDP) ; l'accès Minecraft TCP est vérifié.
La redirection UPnP appartenant à Beta est ensuite supprimée. Son timer est
désactivé, le service est arrêté, puis les trois fichiers du maintien sont
retirés de la configuration active et archivés dans la CT sous
`/root/sanctuary-beta-maintenance/upnp-retired-20261025-1792900244/`.
Les sept autres règles NAT permanentes et les quatre autres redirections UPnP
sont conservées. Le processus Minecraft reste le même : aucun redémarrage ni
modification d'une sauvegarde n'est nécessaire à ce retrait.
Après suppression du secours, `beta.sanctuary-minecraft.net:25567` répond bien
Minecraft **26.3**, protocole **777**, **0/20**, avec le MOTD du nouveau Beta.
Le SRV public permet de saisir seulement `beta.sanctuary-minecraft.net`.
La vérification utilise le chemin public depuis le LAN (hairpin) ; une validation
depuis un réseau Internet extérieur reste à effectuer.
Le [MRpack client publié](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-beta.280.mrpack)
est retéléchargé et vérifié : 16 589 058 octets, hash identique au fichier local,
ZIP intègre, version beta.280, Minecraft 26.3 et un seul override Sanctuary avec
JEI intégré.
Aucune charge réelle avec vingt joueurs, nouvelle connexion OAuth Discord ou
nouvelle qualification graphique n'est revendiquée.
Les sources sont isolées de Map Island Zoom. La prochaine livraison doit
reprendre ce profil serveur et utiliser un compteur supérieur à beta.280.
L'instance Prism personnelle n'a pas été synchronisée par cette mise en service.
Preuves locales supplémentaires : `build/server280-publication.log`,
`build/server280-deployment.log`, `build/server280-gpu-update-validation.log`,
`build/server280-rcon-setup.log`, `build/server280-dhcp-reservation.log`,
`build/server280-upnp-retirement.log` et
les inventaires sous `/root/beta-publication-20261025/` sur PVE. Ces fichiers
restent hors de Git.
## Raccordement Sanctuary Web corrigé le 25 octobre 2026
Le statut et l'Intendance de [Sanctuary Web](https://sanctuary-minecraft.net)
lisaient encore l'ancienne base distante par la connexion Laravel `game`, alors
que Beta écrivait déjà dans la base `sanctuary` de la CT Web 320. La vérification
initiale des écritures serveur ne couvrait donc pas toutes les lectures du site.
Hermes confirme l'identité de cette instance Web ; les anciens sites de release
ne sont pas remis en service.
La connexion `game` utilise maintenant la même base locale que Beta, en gardant
`COMMUNITY_DB_CONNECTION=game` pour les services et les ressources
d'administration. Le cache de configuration Laravel est reconstruit. Les sources
Web déployées, qui contiennent des changements préexistants, restent inchangées.
La connexion Discord fonctionne selon le créateur et n'est pas modifiée.
La nouvelle communauté ne contient aucune publication : Gazette, Tableau et
Intendance affichent désormais cet état, sans suppression dans l'ancienne base.
Les trois comptes, dont deux administrateurs, le groupe d'équipe, ses deux
adhésions et les vingt-deux permissions sont conservés. Les empreintes des
identités, des droits administrateurs et des tables d'équipe sont identiques
avant et après l'opération.
Les services publics et les requêtes d'administration Gazette/Tableau lisent
zéro publication. La page d'accueil et les deux listes publiques répondent HTTP
200. Les métriques sont fraîches, le serveur est déclaré en fonctionnement, et
son temps cumulé continue d'augmenter. Le compteur reste à zéro jour tant que
Beta n'a pas cumulé vingt-quatre heures ; les anciens décès ne sont plus affichés.
La configuration précédente est sauvegardée dans la CT Web, avec accès root
seul, sous
`/root/sanctuary-beta-maintenance/web-game-connection-20261025-1792901058/`.
Le reçu `build/sanctuary-web-beta-alignment.log` et la capture
`build/sanctuary-web-beta-after.png` restent hors de Git. Cette correction de
configuration et de documentation ne change aucun binaire beta.280 ni aucune
sauvegarde Minecraft.
+29
View File
@@ -1,5 +1,34 @@
# Distribution packwiz et Prism
## beta.280 — JEI embarqué et profil serveur
JEI rejoint le JAR Sanctuary ; `mods/sanctuary.pw.toml` reste le seul artefact
Sanctuary à publier. `python3 scripts/pack.py release <sanctuary-https-url>`
prépare désormais le client et `server/pack.toml`, qui partagent le même JAR,
la même URL et le même hash. Le profil serveur contient seulement Fabric API
et Sanctuary ; Sodium et Sound Physics restent facultatifs côté client.
Les deux profils sont publiés dans le même commit du canal, après vérification
du JAR immuable. L'ancienne entrée JEI est retirée de l'index client.
`./gradlew assembleServerPack` construit un kit Linux avec les outils épinglés
et les commandes de synchronisation/démarrage. [Mode d'emploi](server-pack.md),
[contrat et vérifications](jei-server-beta280.md). La configuration Sanctuary Web,
MariaDB et les secrets restent hors de l'index. La préparation locale ne publie
pas le canal et ne met pas à jour une installation personnelle.
Livraison du 25 octobre : [release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280)
publiée, canal `338e8a62387048c7e703ef036fda759ef57b4130`, puis nouveau serveur
CT 350 sur `gpu` installé avec le kit vérifié. Le monde neuf, les paramètres
et les services Web sont vérifiés dans le [reçu](jei-server-beta280.md).
Une synchronisation du serveur conserve 61 fichiers suivis. L'Alpha et
l'instance Prism personnelle restent hors de cette mise en service.
La règle Bbox permanente est ensuite vérifiée et le secours UPnP Beta retiré.
Le domaine Minecraft `beta.sanctuary-minecraft.net` répond sans ce secours,
avec 20 places. [MRpack client beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-beta.280.mrpack)
vérifié et disponible ; le [reçu](jei-server-beta280.md) conserve les empreintes
et la limite de validation réseau.
## beta.257 — Mods client séparés
JEI Sanctuary, Sound Physics Remastered et Sodium sont facultatifs et séparés
+100
View File
@@ -0,0 +1,100 @@
# Sanctuary — serveur Linux avec packwiz
Prérequis : Java 25, Python 3.11 ou plus récent, Linux et accès HTTPS sortant.
Plateforme épinglée : Minecraft **26.3**, Fabric Loader **0.19.5**, Fabric API
**0.160.5+26.3**. Le même JAR Sanctuary contient le fork JEI
**30.32.0-sanctuary.4**, son réseau commun et son interface client.
## Préparer le kit
Dans les sources : `./gradlew check build assemblePack assembleServerPack`.
Le kit est `build/Sanctuary-Server-beta.280.zip` ; les manifestes locaux sont
dans `build/packwiz-server/`. Le ZIP est un amorçage : il télécharge les mods
depuis le canal au premier démarrage, il n'est pas une copie hors ligne du pack.
Il n'est utilisable sur le canal public qu'après publication de cette version.
Décompresser le kit **une seule fois**, dans un dossier serveur neuf. Pour
reprendre une installation existante, arrêter le service, sauvegarder le dossier
entier, vérifier Minecraft/Loader et poser uniquement les fichiers du kit en
conservant le monde et la configuration. Ne pas écraser un fichier déjà présent
sans comparaison. Un ancien JEI externe géré par packwiz est retiré par le nouvel
index ; un JAR ajouté manuellement doit être retiré après sauvegarde pour éviter
de charger une autre version du fork. Conserver `config/jei/`.
Canal prévu, publié avec le client dans le même commit de distribution :
```text
https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/server/pack.toml
```
`./server.sh update` synchronise les mods sans démarrer Minecraft.
`./server.sh start` synchronise puis lance Fabric en console. En cas d'échec,
le serveur ne démarre pas. Accepter l'EULA Minecraft et régler le serveur
explicitement avant ouverture aux joueurs ; le kit ne les remplit pas.
Le lanceur Fabric télécharge les fichiers Mojang/Loader nécessaires au lancement.
Les outils de démarrage sont épinglés par hash dans `.tools/platform.json`.
Un changement de Minecraft ou Loader est refusé avant mise à jour et exige un
nouveau kit et un contrat de migration. Le verrou empêche une mise à jour en
parallèle d'un serveur lancé par ce kit. Arrêter aussi tout service historique
qui utiliserait un autre lanceur.
Variables facultatives : `SANCTUARY_JAVA` (chemin du binaire Java),
`SANCTUARY_MEMORY` (`4G` par défaut), `SANCTUARY_PACK_URL` (autre canal ou
serveur HTTP de laboratoire). Les variables sont héritées de l'environnement ;
le script ne charge pas automatiquement un `.env`.
Un exemple systemd est fourni sous `examples/sanctuary-beta.service`.
Installer le kit dans `/srv/minecraft/sanctuary-beta`, créer l'utilisateur
`minecraft`, donner ce dossier à cet utilisateur et placer l'environnement
privé dans `/etc/sanctuary/beta.env` (root, mode 600). Copier ensuite l'unité
dans `/etc/systemd/system/`, recharger systemd et activer le service. Le mode
`KillMode=mixed` transmet l'arrêt au lanceur, qui attend la sauvegarde Java
avant de rendre le verrou de mise à jour.
## Relier Sanctuary Web
L'API d'accueil et la communauté sont deux services existants distincts.
Le pack ne contient ni secret ni configuration de production.
Pour l'accès/candidature, fournir dans l'environnement du service Minecraft :
```text
SANCTUARY_APP_URL=https://sanctuary-minecraft.net
SANCTUARY_API_TOKEN=<jeton API du site>
```
Le site doit fournir les POST authentifiés `/api/v1/access-codes/verify` et
`/redeem`, avec les reçus d'admission de beta.166. Aucun appel d'essai ne doit
consommer un vrai code de joueur. Sans ces deux paramètres, le mod autonome
laisse l'accueil sans code ; une configuration partielle ferme l'accueil.
Pour Gazette/Tableau et les statistiques, reprendre le **serverId existant**
dans `config/sanctuary-community.json`, configurer `mode: database`, le JDBC
MariaDB, l'utilisateur et `passwordEnv: SANCTUARY_COMMUNITY_DB_PASSWORD`.
Le mot de passe est uniquement dans l'environnement. Utiliser le même UUID dans
`SANCTUARY_COMMUNITY_SERVER_ID` côté Laravel. L'exemple sous `examples/` doit être
complété, puis copié volontairement ; il ne remplace aucune configuration.
Garder `online-mode=true` pour l'identité et la liaison des comptes.
Les migrations SQL sont celles du site et doivent être vérifiées sur la base
cible avant activation. Basculer du mode fichier vers MariaDB n'importe pas les
anciennes publications : cela demande un contrat de migration séparé.
Le kit n'effectue aucune migration SQL ni modification de sauvegarde.
## Mises à jour et retour arrière
Arrêter le service, sauvegarder, lancer `./server.sh update`, vérifier les logs,
puis redémarrer le même dossier serveur. `packwiz.json` doit rester présent.
Le pack ne possède que les mods Fabric API et Sanctuary. Il ne gère ni monde,
`server.properties`, `eula.txt`, `config/`, listes de joueurs, permissions,
captures ou secrets. Les réglages des opérateurs restent en place.
Sauvegarder aussi l'identifiant d'admission et ses reçus avec le monde, ainsi
que les configurations et l'UUID communautaire. Un retour arrière reprend le
dernier artefact immuable et la sauvegarde correspondant à son contrat de
données ; ne pas supprimer de journaux pour forcer le chargement.
Référence : [installation packwiz sur serveur](https://packwiz.infra.link/tutorials/installing/packwiz-installer/#using-a-modpack-with-a-server).
Dans le dépôt, consulter aussi `docs/inscription-web-beta166.md` et
`docs/community-contract-v1.md` pour les contrats et droits SQL complets.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.278
pack_version=beta.278
mod_version=beta.280
pack_version=beta.280
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.4
+4 -2
View File
@@ -27,8 +27,10 @@ le cache ignoré manque, extrait le commit fixé et applique `patches/26.3.patch
La première reconstruction nécessite Internet, Git, patch et Python 3.
`./gradlew :jei:sourcesJar` produit aussi les sources Java effectivement compilées.
Depuis beta.257, JEI est un JAR client séparé et facultatif. Sanctuary conserve
son pont de catalogue ; le retrait de JEI ne supprime aucune connaissance serveur.
Depuis beta.280, JEI est de nouveau embarqué dans Sanctuary, avec ses métadonnées
Fabric, ses entrées communes/cliente et son pont de catalogue. Il n'est plus
distribué séparément dans packwiz. La séparation facultative beta.257–278
n'a modifié aucune connaissance serveur ; ces données restent conservées.
JEI conserve ses deux bibliothèques internes nécessaires à la recherche :
- `net.mezzdev:baked-substring-index:0.1.0`, MIT, James Mitchell.
+2
View File
@@ -686,6 +686,8 @@ dependencies {
if (providers.gradleProperty('sanctuaryJeiRuntime').map { it.toBoolean() }.getOrElse(true)) {
runtimeOnly project(':jei')
}
// One distributed Sanctuary JAR; keep the exact JEI fork and its own metadata.
include project(':jei')
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.278"
version = "beta.280"
pack-format = "packwiz:1.1.0"
[index]
+48 -28
View File
@@ -106,7 +106,7 @@ def https_url(value):
return value
def assemble(jar_url=None, jei_url=None):
def assemble(jar_url=None):
values, entries = check()
jar = ROOT / 'mods/sanctuary/build/libs' / ('sanctuary-' + values['mod_version'] + '.jar')
require(jar.is_file(), 'Build Sanctuary before assembling the pack: ' + str(jar))
@@ -122,17 +122,19 @@ def assemble(jar_url=None, jei_url=None):
require('fr/koka/sanctuarytest/QuickTestMod.class' not in archive.namelist(), 'Lab entrypoint leaked into production')
require(not {'jei', 'sound_physics_remastered', 'sodium'} & metadata['depends'].keys(),
'External client mods must not be required by Sanctuary')
embedded_jei = []
for nested_mod in metadata.get('jars', []):
with zipfile.ZipFile(io.BytesIO(archive.read(nested_mod['file']))) as candidate:
if 'fabric.mod.json' in candidate.namelist():
info = json.loads(candidate.read('fabric.mod.json'))
require(info['id'] not in ('jei', 'sound_physics_remastered', 'sodium'),
require(info['id'] not in ('sound_physics_remastered', 'sodium'),
'External client mod is still nested in Sanctuary: ' + info['id'])
if info['id'] == 'jei':
embedded_jei.append(archive.read(nested_mod['file']))
require(all('licenses/sound-physics-remastered/' + name in archive.namelist()
for name in ('LICENSE', 'PROVENANCE.md')), 'Sound Physics notices missing')
built_jei = ROOT / 'mods/jei/build/libs' / f"jei-sanctuary-{values['minecraft_version']}-{values['jei_version']}.jar"
require(built_jei.is_file(), 'Build the separate Sanctuary JEI fork before assembling the pack')
with zipfile.ZipFile(built_jei) as jei:
require(len(embedded_jei) == 1, 'Sanctuary must embed exactly one JEI fork')
with zipfile.ZipFile(io.BytesIO(embedded_jei[0])) as jei:
info = json.loads(jei.read('fabric.mod.json'))
require(info['id'] == 'jei' and info['version'] == values['jei_version'], 'JEI metadata drift')
require(info['depends']['minecraft'] == values['minecraft_version'].replace('-pre-', '-pre.'), 'JEI Minecraft version drift')
@@ -152,7 +154,6 @@ def assemble(jar_url=None, jei_url=None):
info = json.loads(archive.read('fabric.mod.json'))
require(info['id'] == 'sound_physics_remastered' and info['version'] == values['sound_physics_version'], 'Sound Physics version drift')
require(info['depends']['minecraft'] == values['minecraft_version'] + '.x', 'Sound Physics Minecraft version drift')
require(bool(jar_url) == bool(jei_url), 'Release needs immutable URLs for both Sanctuary and JEI')
destination = ROOT / ('build/packwiz-release' if jar_url else 'build/packwiz')
# Only this generated staging directory is replaced; never touch a game instance.
if destination.exists():
@@ -180,36 +181,55 @@ def assemble(jar_url=None, jei_url=None):
else:
shutil.copy2(jar, target)
staged_entries.append({'file': 'mods/' + jar.name, 'hash': sha256(target)})
target = destination / 'mods' / ('jei-sanctuary.pw.toml' if jei_url else built_jei.name)
if jei_url:
target.write_text('\n'.join([
'name = "JEI Sanctuary"', 'filename = ' + json.dumps(built_jei.name),
'side = "client"', '', '[download]',
'url = ' + json.dumps(https_url(jei_url)), 'hash-format = "sha256"',
'hash = ' + json.dumps(sha256(built_jei)), '', '[option]',
'optional = true', 'default = true',
'description = "Catalogue de recettes facultatif. / Optional recipe catalogue."', '',
]), encoding='utf-8')
staged_entries.append({'file': 'mods/jei-sanctuary.pw.toml', 'hash': sha256(target), 'metafile': True})
else:
# Local previews ship the fork as a loose JAR, removable in the launcher.
shutil.copy2(built_jei, target)
staged_entries.append({'file': 'mods/' + built_jei.name, 'hash': sha256(target)})
write_index(destination, staged_entries)
server = destination / 'server' if jar_url else ROOT / 'build/packwiz-server'
if server.exists():
shutil.rmtree(server)
server.mkdir(parents=True)
server_entries = []
for entry in staged_entries:
if entry['file'] not in ('mods/fabric-api.pw.toml', 'mods/sanctuary.pw.toml', 'mods/' + jar.name):
continue
target = server / entry['file']
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(destination / entry['file'], target)
server_entries.append(entry)
write_index(server, server_entries, name='Sanctuary Server')
verify_server(server)
print('Client pack staged at ' + str(destination))
print('Server pack staged at ' + str(server))
def write_index(destination, entries, name=None):
lines = ['hash-format = "sha256"', '']
for entry in sorted(staged_entries, key=lambda e: e['file']):
for entry in sorted(entries, key=lambda e: e['file']):
lines.append('[[files]]')
for key, value in entry.items():
lines.append(key + ' = ' + json.dumps(value))
lines.extend(key + ' = ' + json.dumps(value) for key, value in entry.items())
lines.append('')
index_path = destination / 'index.toml'
index_path.write_text('\n'.join(lines), encoding='utf-8')
manifest = (SOURCE / 'pack.toml').read_text(encoding='utf-8')
if name:
manifest = manifest.replace('name = "Sanctuary"', 'name = ' + json.dumps(name), 1)
manifest, count = re.subn(r'(?m)^hash = "[a-f0-9]+"$',
'hash = "' + sha256(index_path) + '"', manifest)
require(count == 1, 'Expected a single pack index hash')
(destination / 'pack.toml').write_text(manifest, encoding='utf-8')
check_index(destination)
print('Pack staged at ' + str(destination))
def verify_server(base):
manifest, entries = check_index(base)
values = properties()
require(manifest['version'] == values['pack_version'], 'Server pack version drift')
require(manifest['versions'] == read_toml(SOURCE / 'pack.toml')['versions'], 'Server platform drift')
files = {entry['file'] for entry in entries}
sanctuary = 'mods/sanctuary.pw.toml' if 'mods/sanctuary.pw.toml' in files else 'mods/sanctuary-' + values['mod_version'] + '.jar'
require(files == {'mods/fabric-api.pw.toml', sanctuary}, 'Server pack must contain only Fabric API and Sanctuary')
for entry in entries:
if entry.get('metafile'):
require(read_toml(base / entry['file'])['side'] == 'both', 'Client mod in server pack')
return manifest, entries
def prism(pack_url):
@@ -255,10 +275,10 @@ def prism(pack_url):
if __name__ == '__main__':
require((len(sys.argv) == 2 and sys.argv[1] in ('check', 'assemble'))
or (len(sys.argv) == 3 and sys.argv[1] == 'prism')
or (len(sys.argv) == 4 and sys.argv[1] == 'release'),
'Usage: python3 scripts/pack.py check|assemble|release <sanctuary-https-url> <jei-https-url>|prism <pack-https-url>')
or (len(sys.argv) == 3 and sys.argv[1] == 'release'),
'Usage: python3 scripts/pack.py check|assemble|release <sanctuary-https-url>|prism <pack-https-url>')
if sys.argv[1] == 'release':
assemble(https_url(sys.argv[2]), https_url(sys.argv[3]))
assemble(https_url(sys.argv[2]))
elif sys.argv[1] == 'prism':
prism(sys.argv[2])
elif sys.argv[1] == 'assemble':
+7 -5
View File
@@ -124,6 +124,10 @@ def channel_file(relative):
def publish_channel(origin, version, source_head):
staged = ROOT / 'build/packwiz-release'
pack.check_index(staged)
pack.verify_server(staged / 'server')
pack.require((staged / 'mods/sanctuary.pw.toml').read_bytes() ==
(staged / 'server/mods/sanctuary.pw.toml').read_bytes(),
'Client and server must publish the same Sanctuary artifact')
files = [path for path in staged.rglob('*') if path.is_file()]
pack.require(all(channel_file(path.relative_to(staged)) for path in files),
'Distribution branch must contain only generated TOML metadata and the pack icon')
@@ -183,10 +187,9 @@ def main():
mod = json.loads(archive.read('fabric.mod.json'))
pack.require(mod['id'] == 'sanctuary' and mod['version'] == values['mod_version'],
'Built JAR does not match the Sanctuary source version')
jei = ROOT / 'mods/jei/build/libs' / f"jei-sanctuary-{values['minecraft_version']}-{values['jei_version']}.jar"
# Validate every artifact before creating a public release or uploading a file.
pack.assemble()
assets = [jar, jei] + [path.resolve() for path in args.asset]
assets = [jar] + [path.resolve() for path in args.asset]
pack.require(all(path.is_file() for path in assets), 'A requested release asset is missing')
pack.require(len({path.name for path in assets}) == len(assets), 'Release asset names must be unique')
notes = args.notes_file.read_text(encoding='utf-8') if args.notes_file else (
@@ -207,10 +210,9 @@ def main():
pack.require(not release.get('draft'), 'The existing release is a draft; publish it explicitly first')
pack.require(remote_tag_commit(tag) == head, 'Release tag must resolve to the verified source commit')
jar_url = gitea.upload(release, jar)
jei_url = gitea.upload(release, jei)
for asset in assets[2:]:
for asset in assets[1:]:
gitea.upload(release, asset)
pack.assemble(jar_url, jei_url)
pack.assemble(jar_url)
publish_channel(origin, version, head)
stable_url = gitea.repo_url + '/raw/branch/' + CHANNEL + '/pack.toml'
with urlopen(stable_url, timeout=60) as response:
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Build a Linux server bootstrap kit; never install into an existing server."""
import hashlib
import json
from pathlib import Path
from urllib.request import urlopen
import zipfile
import pack
TOOLS = {
'fabric-server-launch.jar': (
'https://meta.fabricmc.net/v2/versions/loader/26.3/0.19.5/1.1.2/server/jar',
'0b56ad54d762172e8b8748e467f584f071e4dedecd93dc336cf2c68837e790be'),
'packwiz-installer.jar': (
'https://github.com/packwiz/packwiz-installer/releases/download/v0.5.14/packwiz-installer.jar',
'c9f646908d340d84773948a9a7d98bc1dae250d35e1016dc6e2b8459760b5598'),
'packwiz-installer-bootstrap.jar': (pack.BOOTSTRAP_URL, pack.BOOTSTRAP_SHA256),
}
PACK_URL = 'https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/server/pack.toml'
def put(archive, name, data, mode=0o100644):
entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
entry.compress_type = zipfile.ZIP_DEFLATED
entry.external_attr = mode << 16
archive.writestr(entry, data)
def assemble():
values = pack.properties()
manifest, _ = pack.verify_server(pack.ROOT / 'build/packwiz-server')
pack.require(manifest['versions'] == {'minecraft': '26.3', 'fabric': '0.19.5'},
'Re-pin the server launcher for an explicit Minecraft/Fabric migration')
cache = pack.ROOT / 'build/server-tools'
cache.mkdir(parents=True, exist_ok=True)
for name, (url, digest) in TOOLS.items():
target = cache / name
if not target.is_file():
with urlopen(url, timeout=45) as response:
data = response.read()
pack.require(hashlib.sha256(data).hexdigest() == digest, 'Server tool hash mismatch: ' + name)
target.write_bytes(data)
pack.require(pack.sha256(target) == digest, 'Cached server tool hash mismatch: ' + name)
platform = {'versions': manifest['versions'], 'pack_url': PACK_URL,
'sha256': {name: digest for name, (_, digest) in TOOLS.items()}}
output = pack.ROOT / 'build' / f'Sanctuary-Server-{values["pack_version"]}.zip'
with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as archive:
for name in TOOLS:
put(archive, '.tools/' + name, (cache / name).read_bytes())
put(archive, '.tools/server.py', (pack.ROOT / 'scripts/server_runtime.py').read_bytes())
put(archive, '.tools/platform.json', json.dumps(platform, indent=2) + '\n')
put(archive, 'server.sh', '#!/bin/sh\nset -eu\ncd "$(dirname "$0")"\nexec python3 .tools/server.py "$@"\n', mode=0o100755)
put(archive, 'README.md', (pack.ROOT / 'docs/server-pack.md').read_bytes())
# Examples are deliberately outside config/ and outside the pack index.
put(archive, 'examples/sanctuary-community.json.example', json.dumps({
'mode': 'database', 'serverId': 'REPLACE_WITH_EXISTING_SERVER_UUID',
'jdbcUrl': 'jdbc:mariadb://db.example.net:3306/sanctuary?sslMode=verify-full',
'user': 'sanctuary_game', 'passwordEnv': 'SANCTUARY_COMMUNITY_DB_PASSWORD',
}, indent=2) + '\n')
put(archive, 'examples/sanctuary.env.example',
'SANCTUARY_APP_URL=https://sanctuary-minecraft.net\n'
'SANCTUARY_API_TOKEN=\nSANCTUARY_COMMUNITY_DB_PASSWORD=\n'
'SANCTUARY_MEMORY=4G\n')
put(archive, 'examples/sanctuary-beta.service',
'[Unit]\nDescription=Sanctuary Beta Minecraft server\n'
'Wants=network-online.target\nAfter=network-online.target\n\n'
'[Service]\nType=simple\nUser=minecraft\nGroup=minecraft\n'
'WorkingDirectory=/srv/minecraft/sanctuary-beta\n'
'EnvironmentFile=/etc/sanctuary/beta.env\n'
'ExecStart=/srv/minecraft/sanctuary-beta/server.sh start\n'
'Restart=on-failure\nRestartSec=30\nTimeoutStopSec=90\nKillMode=mixed\n'
'UMask=0077\nNoNewPrivileges=true\nPrivateTmp=true\nProtectHome=true\n'
'ProtectSystem=strict\nReadWritePaths=/srv/minecraft/sanctuary-beta\n'
'LimitNOFILE=65535\n\n[Install]\nWantedBy=multi-user.target\n')
print('Server bootstrap kit: ' + str(output))
if __name__ == '__main__':
assemble()
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Linux server kit entrypoint. Configuration and worlds are never pack-owned."""
import argparse
import fcntl
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import tomllib
from urllib.request import urlopen
def update(root, platform, url, java):
# packwiz updates mods, not Minecraft/Loader. Refuse an implicit platform migration.
with urlopen(url, timeout=30) as response:
manifest = tomllib.loads(response.read(65536).decode('utf-8'))
if manifest['versions'] != platform['versions']:
raise SystemExit('Minecraft/Fabric changed: prepare an explicit migration before updating.')
tools = root / '.tools'
for name, digest in platform['sha256'].items():
if hashlib.sha256((tools / name).read_bytes()).hexdigest() != digest:
raise SystemExit('Server tool hash mismatch: ' + name)
subprocess.run([java, '-jar', str(tools / 'packwiz-installer-bootstrap.jar'),
'--bootstrap-main-jar', str(tools / 'packwiz-installer.jar'),
'--bootstrap-no-update', '-g', '-s', 'server',
'--pack-folder', str(root), url], cwd=root, check=True)
def run_minecraft(root, java, memory):
process = subprocess.Popen([java, '-Xms512M', '-Xmx' + memory, '-jar',
str(root / '.tools/fabric-server-launch.jar'), 'nogui'], cwd=root)
def stop(signum, _frame):
process.send_signal(signum)
previous = {s: signal.signal(s, stop) for s in (signal.SIGTERM, signal.SIGINT)}
try:
# Keep the update lock until Java has finished saving during shutdown.
return process.wait()
finally:
for signum, handler in previous.items():
signal.signal(signum, handler)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('command', choices=('update', 'start'), nargs='?', default='start')
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
platform = json.loads((root / '.tools/platform.json').read_text())
java = os.environ.get('SANCTUARY_JAVA', 'java')
with (root / '.sanctuary-server.lock').open('a') as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise SystemExit('Server is running or an update is already in progress.')
update(root, platform, os.environ.get('SANCTUARY_PACK_URL', platform['pack_url']), java)
if args.command == 'start':
memory = os.environ.get('SANCTUARY_MEMORY', '4G')
# No shell evaluation of Java options or credentials.
raise SystemExit(run_minecraft(root, java, memory))
if __name__ == '__main__':
try:
main()
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
# Do not print environment values or HTTP response bodies.
raise SystemExit('Server operation failed (' + type(error).__name__ + ').')
+95
View File
@@ -0,0 +1,95 @@
"""Guard the updater's platform boundary and failure behaviour without a live server."""
import hashlib
import io
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import patch
# Reuse pack's macOS Python 3.11+ selection before importing tomllib users.
import pack
import server_runtime
class ServerUpdateTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
(self.root / '.tools').mkdir()
(self.root / '.tools/tool.jar').write_bytes(b'verified-tool')
self.platform = {'versions': {'minecraft': '26.3', 'fabric': '0.19.5'},
'sha256': {'tool.jar': hashlib.sha256(b'verified-tool').hexdigest()}}
self.platform['pack_url'] = 'https://pack.example/pack.toml'
(self.root / '.tools/platform.json').write_text(json.dumps(self.platform))
def manifest(self, minecraft='26.3', loader='0.19.5'):
return io.BytesIO(f'[versions]\nminecraft="{minecraft}"\nfabric="{loader}"\n'.encode())
def test_headless_server_update_uses_pinned_tools_and_instance(self):
with patch('server_runtime.urlopen', return_value=self.manifest()), patch('server_runtime.subprocess.run') as run:
server_runtime.update(self.root, self.platform, 'https://pack.example/pack.toml', '/java path/java')
args = run.call_args.args[0]
self.assertEqual(args[0], '/java path/java')
self.assertEqual(args[args.index('-s') + 1], 'server')
self.assertIn('-g', args)
self.assertIn('--bootstrap-no-update', args)
self.assertEqual(args[args.index('--pack-folder') + 1], str(self.root))
self.assertTrue(run.call_args.kwargs['check'])
def test_platform_changes_stop_before_packwiz(self):
for minecraft, loader in [('26.4', '0.19.5'), ('26.3', '0.20.0')]:
with self.subTest(minecraft=minecraft, loader=loader), patch('server_runtime.urlopen', return_value=self.manifest(minecraft, loader)), patch('server_runtime.subprocess.run') as run:
with self.assertRaisesRegex(SystemExit, 'explicit migration'):
server_runtime.update(self.root, self.platform, 'https://pack.example/pack.toml', 'java')
run.assert_not_called()
def test_tool_hash_mismatch_stops_before_packwiz(self):
(self.root / '.tools/tool.jar').write_bytes(b'changed')
with patch('server_runtime.urlopen', return_value=self.manifest()), patch('server_runtime.subprocess.run') as run:
with self.assertRaisesRegex(SystemExit, 'hash mismatch'):
server_runtime.update(self.root, self.platform, 'https://pack.example/pack.toml', 'java')
run.assert_not_called()
def test_update_failure_propagates_and_personal_files_are_untouched(self):
settings = self.root / 'server.properties'
settings.write_bytes(b'online-mode=true\n')
with patch('server_runtime.urlopen', return_value=self.manifest()), patch('server_runtime.subprocess.run', side_effect=subprocess.CalledProcessError(1, 'installer')):
with self.assertRaises(subprocess.CalledProcessError):
server_runtime.update(self.root, self.platform, 'https://pack.example/pack.toml', 'java')
self.assertEqual(settings.read_bytes(), b'online-mode=true\n')
def test_failed_update_prevents_minecraft_start(self):
with patch('server_runtime.__file__', str(self.root / '.tools/server.py')), patch('sys.argv', ['server.py', 'start']), patch('server_runtime.update', side_effect=OSError('unreachable')), patch('server_runtime.subprocess.run') as run:
with self.assertRaises(OSError):
server_runtime.main()
run.assert_not_called()
def test_running_server_prevents_an_update(self):
with (self.root / '.sanctuary-server.lock').open('a') as lock:
server_runtime.fcntl.flock(lock, server_runtime.fcntl.LOCK_EX)
with patch('server_runtime.__file__', str(self.root / '.tools/server.py')), patch('sys.argv', ['server.py', 'update']), patch('server_runtime.update') as update:
with self.assertRaisesRegex(SystemExit, 'already in progress'):
server_runtime.main()
update.assert_not_called()
def test_shutdown_waits_for_java_before_releasing_the_lock(self):
handlers = {}
def install(signum, handler):
handlers[signum] = handler
return 'original-handler'
with patch('server_runtime.signal.signal', side_effect=install), patch('server_runtime.subprocess.Popen') as popen:
process = popen.return_value
def wait():
handlers[server_runtime.signal.SIGTERM](server_runtime.signal.SIGTERM, None)
return 0
process.wait.side_effect = wait
self.assertEqual(server_runtime.run_minecraft(self.root, 'java', '8G'), 0)
process.send_signal.assert_called_once_with(server_runtime.signal.SIGTERM)
self.assertTrue(all(h == 'original-handler' for h in handlers.values()))
if __name__ == '__main__':
unittest.main()