Files
sanctuary-beta/scripts/pack.py
T
koka e48de1b400
Build Sanctuary / build (push) Canceled after 0s
Initialize Sanctuary Fabric 26.3 prototype and pack
2026-09-08 03:24:19 +02:00

134 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Validate the source pack or stage an installable packwiz directory (stdlib only)."""
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import sys
try:
import tomllib
except ImportError:
# macOS ships Python 3.9; use an installed modern Python without pip packages.
for name in ('python3.14', 'python3.13', 'python3.12', 'python3.11'):
candidate = shutil.which(name)
if candidate and Path(candidate).resolve() != Path(sys.executable).resolve():
os.execv(candidate, [candidate, *sys.argv])
raise SystemExit('Python 3.11+ is required for pack verification (tomllib).')
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'packwiz'
def read_toml(path):
return tomllib.loads(path.read_text(encoding='utf-8'))
def sha256(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def properties():
return dict(line.split('=', 1) for line in
(ROOT / 'gradle.properties').read_text().splitlines()
if line.strip() and not line.lstrip().startswith('#'))
def require(condition, message):
if not condition:
raise SystemExit(message)
def indexed_path(base, relative):
path = (base / relative).resolve()
require(path.is_relative_to(base.resolve()), 'Index path escapes pack: ' + relative)
require(path.is_file(), 'Missing indexed file: ' + str(path))
return path
def check_index(base):
pack = read_toml(base / 'pack.toml')
index_path = indexed_path(base, pack['index']['file'])
require(pack['index']['hash-format'] == 'sha256', 'Pack index must use sha256')
require(sha256(index_path) == pack['index']['hash'], 'Stale pack hash; run packwiz refresh')
index = read_toml(index_path)
require(index['hash-format'] == 'sha256', 'Index files must use sha256')
entries = index.get('files', [])
require(len({entry['file'] for entry in entries}) == len(entries), 'Duplicate index entry')
for entry in entries:
path = indexed_path(base, entry['file'])
require(sha256(path) == entry['hash'], 'Stale index hash: ' + entry['file'])
if entry.get('metafile'):
metadata = read_toml(path)
download = metadata['download']
require(download['url'].startswith('https://'), 'Dependency URL must use HTTPS')
algorithm = download['hash-format']
require(algorithm in ('sha256', 'sha512'), 'Dependency needs SHA-256/512')
length = 64 if algorithm == 'sha256' else 128
require(re.fullmatch('[0-9a-f]{' + str(length) + '}', download['hash']),
'Invalid dependency download hash')
return pack, entries
def check():
values = properties()
pack, entries = check_index(SOURCE)
require(pack['versions']['minecraft'] == values['minecraft_version'], 'Minecraft version drift')
require(pack['versions']['fabric'] == values['loader_version'], 'Fabric Loader version drift')
require(pack['version'] == values['pack_version'], 'Pack version drift')
api = read_toml(SOURCE / 'mods/fabric-api.pw.toml')
require(api['filename'] == 'fabric-api-' + values['fabric_api_version'] + '.jar',
'Fabric API version drift')
require(any(e['file'] == 'mods/fabric-api.pw.toml' and e.get('metafile') for e in entries),
'Fabric API is missing from the index')
print('Pack versions and index hashes verified.')
return values, entries
def assemble():
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))
destination = ROOT / 'build/packwiz'
# Only this generated staging directory is replaced; never touch a game instance.
if destination.exists():
shutil.rmtree(destination)
destination.mkdir(parents=True)
staged_entries = []
for entry in entries:
source = indexed_path(SOURCE, entry['file'])
target = destination / entry['file']
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
staged_entries.append(entry)
target = destination / 'mods' / jar.name
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(jar, target)
staged_entries.append({'file': 'mods/' + jar.name, 'hash': sha256(target)})
lines = ['hash-format = "sha256"', '']
for entry in sorted(staged_entries, key=lambda e: e['file']):
lines.append('[[files]]')
for key, value in entry.items():
lines.append(key + ' = ' + json.dumps(value))
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')
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('Installable development pack staged at ' + str(destination))
if __name__ == '__main__':
require(len(sys.argv) == 2 and sys.argv[1] in ('check', 'assemble'),
'Usage: python3 scripts/pack.py check|assemble')
if sys.argv[1] == 'assemble':
assemble()
else:
check()