fixes
This commit is contained in:
+225
@@ -0,0 +1,225 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user