Files
simple-updater/quickstart.py
T
2026-09-12 03:11:49 +03:00

226 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
quickstart.py
----------------------------------
Одна команда вместо ручного дёргания generate_key.py / generate_manifest.py /
generate_updater_manifest.py по отдельности - серверная часть пайплайна.
Что делает, по порядку, всё с оглядкой на config.json:
1. Для каждого клиента в config.json's "clients", у которого нет
access_keys - генерирует его и сохраняет в config.json.
2. Если приватный ключ подписи (файл, заданный signing_keys.private_key_file
в config.json) отсутствует - генерирует новую Ed25519 keypair:
приватный ключ сохраняется в отдельный .txt файл, публичный - в
config.json (signing_keys.public).
3. Создаёт папки клиентов (из "clients"), если их ещё нет на диске,
рядом с этим скриптом.
4. Генерирует manifest.json + .sig для каждой клиентской папки, у которой
есть хоть один файл (пустые папки пропускаются с предупреждением -
скорее всего, вы просто ещё не положили туда файлы игры).
Каждый шаг идемпотентен и безопасен перезапускать повторно: уже
сгенерированные ключи не трогаются (кроме как через generate_key.py
--save напрямую, если вы явно хотите их сменить), уже существующие папки
клиентов не перезаписываются, манифесты просто перегенерируются с
текущим содержимым папки.
Сборка updater.exe - ОТДЕЛЬНЫЙ шаг, вне этого скрипта. PyInstaller не
умеет кросс-компилировать: собранный на Linux "updater.exe" - это ELF-
бинарник с расширением .exe, который на Windows просто не запустится.
Соберите его на настоящей Windows-машине (или Windows VM/CI-раннере)
скриптом build_client.py, который тянет всё нужное (access-ключи,
публичный ключ подписи, адрес сервера и т.д.) прямо с работающего
сервера через /client_build_info.json - см. build_client.py и README.
Usage:
python quickstart.py # ключи + манифесты
python quickstart.py --skip-keys # только манифесты
python quickstart.py --skip-manifests # только ключи
"""
import os
import sys
import json
import argparse
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
_CONFIG_PATH = os.path.join(_SCRIPT_DIR, "config.json")
def log(msg):
print(f"[quickstart] {msg}")
def load_config():
if not os.path.exists(_CONFIG_PATH):
raise SystemExit(f"config.json not found at {_CONFIG_PATH}")
with open(_CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def save_config(cfg):
with open(_CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(cfg, f, indent=4)
f.write("\n")
# ------------------------------------------------------------
# Step 1: access keys per client
# ------------------------------------------------------------
def ensure_access_keys(cfg):
import secrets
clients = cfg.get("clients", {})
if not clients:
log("WARNING: config.json has no 'clients' configured - nothing to generate access keys for.")
return cfg
cfg.setdefault("access_keys", {})
changed = False
for name in clients:
if cfg["access_keys"].get(name):
log(f"Access key for '{name}' already set, leaving it alone.")
continue
key = secrets.token_hex(32)
cfg["access_keys"][name] = key
changed = True
log(f"Generated access key for '{name}'.")
if changed:
save_config(cfg)
return cfg
# ------------------------------------------------------------
# Step 2: Ed25519 signing keypair
# ------------------------------------------------------------
def ensure_signing_keypair(cfg):
"""
Delegates the actual key generation/writing to generate_key.py so
there's exactly one place (generate_key.py's write_private_key_file)
that decides how the private key is stored - here we just decide
WHETHER to generate one.
The private half is written to a standalone .txt file next to the
scripts (never into config.json - see generate_key.py's module
docstring for why); only the public half and the key filename land in
config.json.
"""
sys.path.insert(0, _SCRIPT_DIR)
import generate_key
importlib_reload(generate_key)
signing = cfg.setdefault("signing_keys", {"public": "", "private_key_file": "SIGNING_PRIVATE_KEY.txt"})
signing.setdefault("private_key_file", "SIGNING_PRIVATE_KEY.txt")
key_path, _ = generate_key._private_key_file_path(cfg)
has_public = bool(signing.get("public"))
has_private_file = os.path.exists(key_path)
if has_public and has_private_file:
log("Signing keypair already set, leaving it alone.")
return cfg
if has_public or has_private_file:
raise SystemExit(
f"config.json has signing_keys.public set ({has_public}) but the private "
f"key file {'exists' if has_private_file else 'is missing'} ({key_path}) - "
"these are out of sync. Fix or clear both manually, then re-run quickstart.py."
)
private_hex, public_hex = generate_key.generate_signing_keypair()
generate_key.write_private_key_file(key_path, private_hex)
# Re-load in case write_private_key_file's caller (generate_key.py) has
# its own opinions about the file later - here we just need the dict
# we already have to carry the public half back to the caller.
cfg["signing_keys"]["public"] = public_hex
save_config(cfg)
log(f"Generated new Ed25519 signing keypair. Private key written to {os.path.basename(key_path)}.")
log(f"IMPORTANT: {os.path.basename(key_path)} contains a secret - do not commit or share it.")
return cfg
# ------------------------------------------------------------
# Step 3: client folders
# ------------------------------------------------------------
def ensure_client_folders(cfg):
clients = cfg.get("clients", {})
for name, folder in clients.items():
folder_path = os.path.join(_SCRIPT_DIR, folder)
if os.path.isdir(folder_path):
continue
os.makedirs(folder_path, exist_ok=True)
log(f"Created empty client folder '{folder}' for '{name}' - "
f"put game files in it before generating a real manifest.")
def _folder_has_files(folder_path):
for _root, _dirs, files in os.walk(folder_path):
if files:
return True
return False
# ------------------------------------------------------------
# Step 4: manifests
# ------------------------------------------------------------
def importlib_reload(module):
import importlib
importlib.reload(module)
def generate_manifests(cfg):
clients = cfg.get("clients", {})
if not clients:
return
sys.path.insert(0, _SCRIPT_DIR)
import generate_manifest
importlib_reload(generate_manifest) # pick up config.json changes from steps 1-2 above
for name, folder in clients.items():
folder_path = os.path.join(_SCRIPT_DIR, folder)
if not os.path.isdir(folder_path):
log(f"Skipping manifest for '{name}': folder '{folder}' doesn't exist.")
continue
if not _folder_has_files(folder_path):
log(f"Skipping manifest for '{name}': folder '{folder}' has no files yet.")
continue
log(f"Generating manifest for '{name}' ({folder})...")
generate_manifest.generate(folder_path)
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--skip-keys", action="store_true", help="Не генерировать access keys / signing keypair.")
parser.add_argument("--skip-manifests", action="store_true", help="Не генерировать manifest.json для клиентских папок.")
args = parser.parse_args()
cfg = load_config()
if not args.skip_keys:
cfg = ensure_access_keys(cfg)
cfg = ensure_signing_keypair(cfg)
else:
log("Skipping key generation (--skip-keys).")
ensure_client_folders(cfg)
if not args.skip_manifests:
generate_manifests(cfg)
else:
log("Skipping manifest generation (--skip-manifests).")
log("Done. To build updater.exe, run build_client.py on a Windows machine")
log("pointed at this server (see README) - PyInstaller can't cross-compile,")
log("so an exe built here on Linux would not run on players' machines.")
if __name__ == "__main__":
main()