major fixes test

This commit is contained in:
2026-09-09 12:30:40 +03:00
parent b184ea76f4
commit bcd200b1e8
9 changed files with 1785 additions and 226 deletions
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
generate_updater_manifest.py
----------------------------------
Публикует новую версию updater.exe для self-update.
Запускается на build-машине после того, как вы пересобрали updater.exe
(PyInstaller) с увеличенным UPDATER_VERSION внутри updater.py. Копирует
exe в раздаваемую папку клиента(ов), считает MD5, подписывает манифест тем
же Ed25519 приватным ключом, что и generate_manifest.py (оба читают его из
одного .txt файла, см. config.json's signing_keys.private_key_file), и
кладёт рядом updater_manifest.json + updater_manifest.json.sig.
Usage:
python generate_updater_manifest.py <path-to-new-updater.exe> <client_dir> [<client_dir> ...] [--version N]
Если --version не указан, берётся self_update.updater_version из
config.json - удобно, если вы просто хотите опубликовать то, что
quickstart.py только что собрал.
Example:
python generate_updater_manifest.py dist/updater.exe game_client game_client_test
python generate_updater_manifest.py dist/updater.exe game_client --version 3
Кладёт updater_manifest.json/.sig в КАЖДУЮ указанную client_dir - self-update
проверяется через тот же access-key URL, что и обычный игровой манифест
(см. UPDATER_MANIFEST_FILENAME в updater.py), так что файл должен лежать
там же, где manifest.json.
"""
import os
import sys
import json
import shutil
import hashlib
import argparse
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
_CONFIG_PATH = os.path.join(_SCRIPT_DIR, "config.json")
def _load_config():
if not os.path.exists(_CONFIG_PATH):
return {}
try:
with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
return {}
def _read_private_signing_key(cfg):
"""
Same convention as generate_manifest.py: the private key lives in a
standalone .txt file (see generate_key.py's write_private_key_file),
never in config.json. Both scripts read the SAME file, named by
config.json's signing_keys.private_key_file, so as long as that file
exists in one place, the two scripts can't drift apart on which key
they sign with.
"""
filename = cfg.get("signing_keys", {}).get("private_key_file", "SIGNING_PRIVATE_KEY.txt")
key_path = os.path.join(_SCRIPT_DIR, filename)
if not os.path.exists(key_path):
return None
try:
with open(key_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line.startswith("SIGNING_PRIVATE_KEY="):
return line.split("=", 1)[1].strip() or None
except OSError:
return None
return None
_CFG = _load_config()
_self_update_cfg = _CFG.get("self_update", {})
# Read from the same standalone .txt file generate_manifest.py uses - see
# _read_private_signing_key above. Never stored in config.json itself.
SIGNING_PRIVATE_KEY = _read_private_signing_key(_CFG)
MANIFEST_FILENAME = _self_update_cfg.get("updater_manifest_filename", "updater_manifest.json")
DOWNLOAD_FILENAME = _self_update_cfg.get("download_filename", "updater.exe")
# Used as the default --version when the caller doesn't pass one - this is
# the version quickstart.py would have baked into updater.py right before
# building the exe, so "publish whatever config.json currently says" is a
# safe and expected default for the common case of "I just built the exe
# quickstart just produced."
CONFIGURED_VERSION = _self_update_cfg.get("updater_version")
def get_md5(filepath):
hash_md5 = hashlib.md5()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def sign_bytes(data: bytes) -> str:
if not SIGNING_PRIVATE_KEY:
raise SystemExit(
"No private signing key found (expected in the .txt file named by "
"config.json's signing_keys.private_key_file). Run "
"'python generate_key.py --signing-keypair --save' first. An unsigned "
"updater_manifest.json would let updater.py's REQUIRE_SIGNATURE "
"check reject it outright (or, worse, get accepted if someone "
"disabled that check) - refusing to publish an update that "
"can't be verified as coming from you."
)
private_key = Ed25519PrivateKey.from_private_bytes(bytes.fromhex(SIGNING_PRIVATE_KEY))
return private_key.sign(data).hex()
def publish(exe_path, version, client_dirs):
if not os.path.isfile(exe_path):
raise SystemExit(f"Error: '{exe_path}' not found.")
exe_hash = get_md5(exe_path)
exe_size = os.path.getsize(exe_path)
manifest = {
"version": version,
"filename": DOWNLOAD_FILENAME,
"hash": exe_hash,
"size": exe_size,
}
manifest_bytes = json.dumps(manifest, indent=4, sort_keys=True).encode("utf-8")
signature_hex = sign_bytes(manifest_bytes)
for client_dir in client_dirs:
if not os.path.isdir(client_dir):
print(f"WARNING: '{client_dir}' is not a directory, skipping.")
continue
dest_exe = os.path.join(client_dir, DOWNLOAD_FILENAME)
dest_manifest = os.path.join(client_dir, MANIFEST_FILENAME)
dest_sig = dest_manifest + ".sig"
shutil.copy2(exe_path, dest_exe)
with open(dest_manifest, "wb") as f:
f.write(manifest_bytes)
with open(dest_sig, "w", encoding="utf-8") as f:
f.write(signature_hex)
print(f"Published v{version} to {client_dir}/ "
f"({DOWNLOAD_FILENAME}, {MANIFEST_FILENAME}, {MANIFEST_FILENAME}.sig)")
print()
print(f"Version: {version}")
print(f"MD5: {exe_hash}")
print(f"Size: {exe_size:,} bytes")
print()
print("Reminder: players only pick this up if their currently-running")
print("updater.exe has UPDATER_VERSION < the version you just published,")
print("and ENABLE_SELF_UPDATE = True was set when THAT build was compiled.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Publish a new self-updating updater.exe build.")
parser.add_argument("exe_path", help="Path to the newly built updater.exe")
parser.add_argument("client_dirs", nargs="+", help="One or more client folders to publish into (same ones served by server.py)")
parser.add_argument(
"--version", type=int, default=CONFIGURED_VERSION,
help=(
"Integer version number, must be > UPDATER_VERSION baked into the new exe. "
"Defaults to config.json's self_update.updater_version if omitted."
),
)
args = parser.parse_args()
if args.version is None:
raise SystemExit(
"No --version given and config.json has no self_update.updater_version set - "
"pass one explicitly: generate_updater_manifest.py <exe> <client_dir> ... --version <n>"
)
publish(args.exe_path, args.version, args.client_dirs)