134 lines
5.4 KiB
Python
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()
|