trainlog/tools/package_sync_candidate.py

163 lines
6.1 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 = [
"trainlog_syncd.py",
"sync_orchestrator.py",
"sync_peer_worker.py",
"sync_drive_transport.py",
"sync_generation_exchange.py",
"sleep_diary_exchange.py",
"trainlog_sqlite.py",
"validate_json.py",
"validate_training_knowledge.py",
"exercise_names.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_catalog.py",
"export_pc_mobile.py",
"export_exercise_aliases.py",
"export_equipment_definitions.py",
"export_equipment_associations.py",
"export_exercise_body_zones.py",
"export_training_feedback.py",
"export_ai_session_drafts.py",
"import_ai_session_draft.py",
"mark_ai_session_drafts_published.py",
"sync_ai_session_draft_drive.py",
"export_ai_history.py",
"post_sync_ai_drive.py",
"export_session_preparations.py",
"export_programs.py",
"program_execution_exchange.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"
sync_once = args.build / "tui/trainlog-sync-once"
mtp_adapter = args.build / "tui/trainlog-generation-mtp-adapter"
if not binary.is_file():
raise RuntimeError("compiled trainlog executable is missing")
if not mtp_adapter.is_file():
raise RuntimeError("compiled generation MTP adapter is missing")
if not sync_once.is_file():
raise RuntimeError("compiled legacy synchronization helper 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")
shutil.copy2(sync_once, args.output / "libexec/trainlog-sync-once")
shutil.copy2(mtp_adapter, args.output / "libexec/trainlog-generation-mtp-adapter")
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\nscript=$(readlink -f -- "$0")\nroot=$(CDPATH= cd -- "$(dirname -- "$script")/.." && pwd)\nexport TRAINLOG_SYNC_TOOLS_DIR="$root/tools"\nexport TRAINLOG_SYNC_MTP_ADAPTER="$root/libexec/trainlog-generation-mtp-adapter"\nexec "$root/libexec/trainlog" "$@"\n'
)
sync_once_launcher = args.output / "bin/trainlog-sync-once"
sync_once_launcher.write_text(
'#!/bin/sh\nset -eu\nscript=$(readlink -f -- "$0")\nroot=$(CDPATH= cd -- "$(dirname -- "$script")/.." && pwd)\nexec "$root/libexec/trainlog-sync-once" "$@"\n'
)
syncd_launcher = args.output / "bin/trainlog-syncd"
syncd_launcher.write_text(
'#!/bin/sh\nset -eu\nscript=$(readlink -f -- "$0")\nroot=$(CDPATH= cd -- "$(dirname -- "$script")/.." && pwd)\nexport TRAINLOG_SYNC_MTP_ADAPTER="$root/libexec/trainlog-generation-mtp-adapter"\nexec python3 "$root/tools/trainlog_syncd.py" --sync-once "$root/bin/trainlog-sync-once" "$@"\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.4",
"desktop_schema": 29,
"android_schema": 26,
"protocols": [
"mobile-export-v3",
"mobile-history-v4",
"causal-delete-v1",
"execution-draft-v1",
"generation-manifest-v1",
"generation-ack-v1",
"generation-archive-v1",
"session-preparations-v2",
"ai-session-drafts-v2",
"programs-v1",
"program-executions-v1",
"sleep-diary-v1",
],
"entry_points": [
"bin/trainlog",
"bin/trainlog-sync-once",
"bin/trainlog-syncd",
"libexec/trainlog-generation-mtp-adapter",
"tools/sync_orchestrator.py",
],
"runtime_dependencies": [
"python>=3.11",
"sqlite3",
"utf8proc",
"libuuid",
"libudev",
"libmtp",
"notcurses",
"coreutils-readlink",
],
"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 / "bin/trainlog-sync-once", 0o755)
os.chmod(args.output / "bin/trainlog-syncd", 0o755)
os.chmod(args.output / "libexec/trainlog", 0o755)
os.chmod(args.output / "libexec/trainlog-sync-once", 0o755)
os.chmod(args.output / "libexec/trainlog-generation-mtp-adapter", 0o755)
return 0
if __name__ == "__main__":
raise SystemExit(main())