Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23b295356b |
@@ -1,5 +1,15 @@
|
||||
# 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. Une nouvelle CT pour 20 joueurs est en préparation
|
||||
sur `gpu`, séparément de l'Alpha.
|
||||
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# 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. La mise en service du
|
||||
nouveau serveur et le raccordement aux services sont en cours.
|
||||
@@ -1,5 +1,21 @@
|
||||
# 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.
|
||||
|
||||
## beta.257 — Mods client séparés
|
||||
|
||||
JEI Sanctuary, Sound Physics Remastered et Sodium sont facultatifs et séparés
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.278"
|
||||
version = "beta.280"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
+48
-28
@@ -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':
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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__ + ').')
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user