major fixes test
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user