mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 09:51:02 +00:00
tools/make_release_linux.py assembles the Linux release folder and packs it with the static type-2 AppImage runtime: python-build-standalone 3.14 with tkinter, LLVM 23.1.4 (clang, lld, llvm-ar) and the part of the Steam Runtime 3 sniper SDK sysroot a game build reads, so the game, hostcheck, hactool and the translator need no newer glibc than 2.31 and run on SteamOS. The game it builds needs only glibc, the system's Vulkan loader and the libSDL3.so.0 beside it; Opus, libvpx, zstd and libstdc++ are linked in. --prove builds generated/v121 with the folder alone and runs the probes. The installer runs on Linux: release and dev layouts, temp under the XDG cache (the AppImage mount is read-only), cancel through the CLI's process group, language from LANG, xdg-open and a PNG icon, texts that name the right executable, and a warning on kernels older than 6.7. hostcheck reads x86-64-v3 from CPUID, since clang's __builtin_cpu_supports reads it from libgcc, which before GCC 12 has no level bits. build_game.py takes --sysroot, build_hactool.py --cc and build_xlate.py --llvm/--sysroot; linux.c carries the Linux 6.7 write-watch definitions for older headers. Also: shader_census.py and package_build.py use the Linux executable names, and probe-shader writes its shader path with forward slashes.
124 lines
4.7 KiB
Python
124 lines
4.7 KiB
Python
"""Progress events: one JSON object per line on stdout for the window and tests, the same in English to a log file.
|
|
|
|
Every event has "type" and "t" (seconds since the start). Types:
|
|
|
|
start release, mode (install, resume, update), dest, log, steps (the step ids in order)
|
|
step step, state: started | done (seconds) | skipped (reason: current, not_needed)
|
|
progress step, phase, and done/total/unit (bytes, chunks, objects, shaders) when the amount is known
|
|
space dest, need, free (bytes on the install disk; with the temp folder on the same disk need includes it),
|
|
temp, temp_need, temp_free, same_disk
|
|
warning id, params
|
|
error id, params, step (absent outside a step)
|
|
end result: ok | failed | cancelled, seconds
|
|
|
|
Text is never the payload: ids name a step ("step.<id>"), a phase ("phase.<step>.<phase>") or a message ("<id>"),
|
|
and installer/text/en.json holds their English text with {param} placeholders. The log renders events with it.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
CATALOG = Path(__file__).resolve().parent / 'text' / 'en.json'
|
|
PROGRESS_EVERY = 0.5 # seconds between two progress events of one phase
|
|
|
|
|
|
def load_catalog(path=CATALOG):
|
|
return json.loads(path.read_text(encoding='utf-8'))
|
|
|
|
|
|
GAME_EXE = 'totk.exe' if os.name == 'nt' else 'totk' # {game_exe} in the catalogs
|
|
|
|
|
|
def render(catalog, id, params=None):
|
|
"""The English text of id, or the id itself when the catalog lacks it."""
|
|
text = catalog.get(id, id)
|
|
try:
|
|
return text.format(**{'game_exe': GAME_EXE, **(params or {})})
|
|
except (KeyError, IndexError, ValueError):
|
|
return f'{text} {params}'
|
|
|
|
|
|
class Reporter:
|
|
def __init__(self, out=None, log_path=None):
|
|
self.out = out or sys.stdout
|
|
self.t0 = time.monotonic()
|
|
self.catalog = load_catalog()
|
|
self.lock = threading.Lock()
|
|
self.log_file = None
|
|
self.last_progress = {}
|
|
if log_path:
|
|
Path(log_path).parent.mkdir(parents=True, exist_ok=True)
|
|
self.log_file = open(log_path, 'a', encoding='utf-8', buffering=1)
|
|
self.log_path = log_path
|
|
|
|
def elapsed(self):
|
|
return time.monotonic() - self.t0
|
|
|
|
def log(self, text):
|
|
"""A line for the log file only."""
|
|
if self.log_file:
|
|
with self.lock:
|
|
self.log_file.write(f'[{self.elapsed():8.1f}] {text}\n')
|
|
|
|
def emit(self, type, **fields):
|
|
event = {'type': type, 't': round(self.elapsed(), 2), **fields}
|
|
with self.lock:
|
|
self.out.write(json.dumps(event, ensure_ascii=False) + '\n')
|
|
self.out.flush()
|
|
self.log(self.describe(event))
|
|
return event
|
|
|
|
def describe(self, e):
|
|
c = self.catalog
|
|
kind = e['type']
|
|
if kind == 'step':
|
|
name = render(c, 'step.' + e['step'])
|
|
extra = f' ({e["seconds"]:.1f} s)' if 'seconds' in e else f' ({e["reason"]})' if 'reason' in e else ''
|
|
return f'{name}: {e["state"]}{extra}'
|
|
if kind == 'progress':
|
|
text = render(c, f'phase.{e["step"]}.{e["phase"]}')
|
|
if e.get('total'):
|
|
text += f' {e["done"]}/{e["total"]} {e.get("unit", "")}'.rstrip()
|
|
return text
|
|
if kind in ('warning', 'error'):
|
|
return f'{kind}: {render(c, e["id"], e.get("params"))} [{e["id"]}]'
|
|
return json.dumps(e, ensure_ascii=False)
|
|
|
|
def step(self, step, state, **fields):
|
|
return self.emit('step', step=step, state=state, **fields)
|
|
|
|
def progress(self, step, phase, done=None, total=None, unit=None, force=False):
|
|
"""A progress event, at most one per PROGRESS_EVERY per phase unless it is the first or the last."""
|
|
now = time.monotonic()
|
|
key = (step, phase)
|
|
last = self.last_progress.get(key)
|
|
final = total is not None and done is not None and done >= total
|
|
if last is not None and not final and not force and now - last < PROGRESS_EVERY:
|
|
return None
|
|
self.last_progress[key] = now
|
|
fields = {'step': step, 'phase': phase}
|
|
if done is not None:
|
|
fields['done'] = done
|
|
if total is not None:
|
|
fields['total'] = total
|
|
if unit:
|
|
fields['unit'] = unit
|
|
return self.emit('progress', **fields)
|
|
|
|
def warning(self, id, **params):
|
|
return self.emit('warning', id=id, params=params)
|
|
|
|
def error(self, id, params, step=None):
|
|
fields = {'id': id, 'params': params}
|
|
if step:
|
|
fields['step'] = step
|
|
return self.emit('error', **fields)
|
|
|
|
def close(self):
|
|
if self.log_file:
|
|
self.log_file.close()
|
|
self.log_file = None
|