72 lines
3.4 KiB
Python
72 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Stage a reproducible, non-secret Trainlog desktop synchronization candidate."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
RUNTIME_TOOLS = [
|
|
"sync_orchestrator.py", "sync_peer_worker.py", "sync_generation_exchange.py",
|
|
"trainlog_sqlite.py", "validate_json.py", "import_mobile_export.py",
|
|
"import_exercise_aliases.py", "import_equipment_definitions.py",
|
|
"import_equipment_associations.py", "import_exercise_body_zones.py",
|
|
"import_training_feedback.py", "execution_draft_exchange.py",
|
|
"causal_delete_exchange.py", "import_exercise_profile_state.py",
|
|
"export_exercise_profile_state.py",
|
|
"export_pc_mobile.py", "export_equipment_definitions.py",
|
|
"export_exercise_body_zones.py", "export_training_feedback.py",
|
|
]
|
|
|
|
|
|
def digest(path: Path) -> str:
|
|
value = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
value.update(chunk)
|
|
return value.hexdigest()
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--output", required=True, type=Path)
|
|
parser.add_argument("--build", default=ROOT / "build", type=Path)
|
|
args = parser.parse_args()
|
|
if args.output.exists():
|
|
raise RuntimeError("candidate output already exists")
|
|
binary = args.build / "tui/trainlog"
|
|
if not binary.is_file():
|
|
raise RuntimeError("compiled trainlog executable is missing")
|
|
(args.output / "bin").mkdir(parents=True)
|
|
(args.output / "libexec").mkdir(parents=True)
|
|
(args.output / "tools").mkdir(parents=True)
|
|
(args.output / "catalog").mkdir(parents=True)
|
|
shutil.copy2(binary, args.output / "libexec/trainlog")
|
|
for name in RUNTIME_TOOLS:
|
|
shutil.copy2(ROOT / "tools" / name, args.output / "tools" / name)
|
|
for source in sorted((ROOT / "catalog").glob("*.json")):
|
|
shutil.copy2(source, args.output / "catalog" / source.name)
|
|
launcher = args.output / "bin/trainlog"
|
|
launcher.write_text("#!/bin/sh\nset -eu\nroot=$(CDPATH= cd -- \"$(dirname -- \"$0\")/..\" && pwd)\nexport TRAINLOG_SYNC_TOOLS_DIR=\"$root/tools\"\nexec \"$root/libexec/trainlog\" \"$@\"\n")
|
|
files = sorted(path for path in args.output.rglob("*") if path.is_file())
|
|
inventory = {
|
|
"format": "trainlog-sync-candidate-inventory", "version": 1,
|
|
"source_commit": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(),
|
|
"product_version": "0.1.2", "desktop_schema": 21, "android_schema": 20,
|
|
"protocols": ["mobile-export-v3", "mobile-history-v4", "causal-delete-v1",
|
|
"execution-draft-v1", "generation-manifest-v1", "generation-ack-v1"],
|
|
"entry_points": ["bin/trainlog", "tools/sync_orchestrator.py"],
|
|
"runtime_dependencies": ["python>=3.11", "sqlite3", "utf8proc", "libuuid", "libudev", "libmtp", "notcurses"],
|
|
"files": [{"path": str(path.relative_to(args.output)), "sha256": digest(path), "size": path.stat().st_size} for path in files],
|
|
}
|
|
(args.output / "candidate-inventory.json").write_text(json.dumps(inventory, indent=2, sort_keys=True) + "\n")
|
|
os.chmod(args.output / "bin/trainlog", 0o755)
|
|
os.chmod(args.output / "libexec/trainlog", 0o755)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|