fix(sync): package user service helpers
This commit is contained in:
parent
cc87f26b98
commit
dd8c91a946
6 changed files with 113 additions and 23 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## Unreleased — 0.1.2
|
||||
|
||||
- Packaged `trainlog-sync-once` and `trainlog-syncd` with the relocatable sync
|
||||
candidate so user services cannot mix a versioned install with live-checkout
|
||||
helpers during rollout.
|
||||
|
||||
- Closed the native candidate warning gate with checked path and HTTP response
|
||||
bounds, strict GCC and Clang builds, full Clang ASan/UBSan validation, and a
|
||||
stable concurrent object-I/O MTP double. Rebuilt the desktop, current Android
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ Clang ASan/UBSan build passes it with leak detection. Bounded path assembly and
|
|||
the Web sync accepted response reject insufficient capacity without publishing
|
||||
partial output. Fresh desktop, current-Android and schema-v17 bridge candidates
|
||||
were rebuilt from commit `2c5a8be2827e31fc5be2accb65073bfeb79d6068`.
|
||||
The rollout packager now keeps the legacy request daemon and one-shot helper in
|
||||
the same relocatable, hashed bundle as the Web generation coordinator, avoiding
|
||||
mixed-checkout user-service execution during the controlled rollout.
|
||||
|
||||
Snapshot date: **2026-09-17**.
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ the separately authorized rollout are complete.
|
|||
1. Build and validate one revision, then run `python3
|
||||
tools/package_sync_candidate.py --output <new-private-directory>`. Its
|
||||
inventory records hashes, schemas, protocols, entry points and dependencies.
|
||||
The relocatable bundle includes `trainlog`, `trainlog-sync-once`,
|
||||
`trainlog-syncd`, the production generation MTP adapter and their matching
|
||||
Python codecs. User units must point only into one versioned bundle; they
|
||||
must not combine a packaged binary with helpers from a live checkout.
|
||||
2. `assembleDebug` is development evidence only. Rollout requires the existing
|
||||
release signing identity, verification of its public certificate and a valid
|
||||
increasing `versionCode`; this mission reads no signing secret.
|
||||
|
|
|
|||
|
|
@ -1333,6 +1333,12 @@ JDK 17. The real Firefox/object-I/O-boundary MTP conversation also passes after
|
|||
formatting. A source comparison against the pre-readability commit confirms
|
||||
that the scoped test names and assertion counts are unchanged.
|
||||
|
||||
The deployment-tools regression also constructs the relocatable candidate and
|
||||
requires its inventory to include the packaged `trainlog-sync-once` binary,
|
||||
the `trainlog-syncd` launcher and daemon source, and the generation worker.
|
||||
This prevents a user unit from silently executing a helper from a different
|
||||
checkout than the installed desktop candidate.
|
||||
|
||||
The isolated JDK 17 harness reports **220 Android tests: 215 passed, five
|
||||
skipped, zero failures/errors**. The skips are optional, externally supplied
|
||||
copy gates: `BridgeBackupMigrationTest.verifiedSchemaSeventeenBridgeBackupMigratesThroughCurrentOwner`,
|
||||
|
|
|
|||
|
|
@ -1,29 +1,84 @@
|
|||
import json, sqlite3, subprocess, tempfile, unittest
|
||||
import json
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class DeploymentToolsTest(unittest.TestCase):
|
||||
def test_sqlite_backup_captures_committed_wal_and_restores_invariants(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root=Path(directory);source=root/"source.db";backup=root/"backup.db"
|
||||
db=sqlite3.connect(source);db.execute("PRAGMA journal_mode=WAL");db.execute("PRAGMA foreign_keys=ON")
|
||||
db.executescript("CREATE TABLE parent(id INTEGER PRIMARY KEY);CREATE TABLE child(parent_id INTEGER REFERENCES parent);INSERT INTO parent VALUES(7);INSERT INTO child VALUES(7);");db.commit()
|
||||
result=subprocess.run(["python3",str(ROOT/"tools/backup_trainlog_sqlite.py"),str(source),str(backup)],capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
with sqlite3.connect(backup) as restored:
|
||||
self.assertEqual(restored.execute("SELECT * FROM child").fetchall(),[(7,)])
|
||||
self.assertEqual(restored.execute("PRAGMA integrity_check").fetchone(),("ok",))
|
||||
db.close()
|
||||
def test_sqlite_backup_captures_committed_wal_and_restores_invariants(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / "source.db"
|
||||
backup = root / "backup.db"
|
||||
database = sqlite3.connect(source)
|
||||
database.execute("PRAGMA journal_mode=WAL")
|
||||
database.execute("PRAGMA foreign_keys=ON")
|
||||
database.executescript(
|
||||
"CREATE TABLE parent(id INTEGER PRIMARY KEY);"
|
||||
"CREATE TABLE child(parent_id INTEGER REFERENCES parent);"
|
||||
"INSERT INTO parent VALUES(7);"
|
||||
"INSERT INTO child VALUES(7);"
|
||||
)
|
||||
database.commit()
|
||||
result = subprocess.run(
|
||||
[
|
||||
"python3",
|
||||
str(ROOT / "tools/backup_trainlog_sqlite.py"),
|
||||
str(source),
|
||||
str(backup),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
with sqlite3.connect(backup) as restored:
|
||||
self.assertEqual(
|
||||
restored.execute("SELECT * FROM child").fetchall(),
|
||||
[(7,)],
|
||||
)
|
||||
self.assertEqual(
|
||||
restored.execute("PRAGMA integrity_check").fetchone(),
|
||||
("ok",),
|
||||
)
|
||||
database.close()
|
||||
|
||||
def test_candidate_is_self_contained_and_hashed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output=Path(directory)/"candidate"
|
||||
result=subprocess.run(["python3",str(ROOT/"tools/package_sync_candidate.py"),"--output",str(output)],capture_output=True,text=True)
|
||||
self.assertEqual(result.returncode,0,result.stderr)
|
||||
inventory=json.loads((output/"candidate-inventory.json").read_text())
|
||||
self.assertEqual(inventory["product_version"],"0.1.2")
|
||||
self.assertTrue((output/"bin/trainlog").is_file())
|
||||
self.assertTrue(any(row["path"]=="tools/sync_peer_worker.py" for row in inventory["files"]))
|
||||
def test_candidate_is_self_contained_and_hashed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "candidate"
|
||||
result = subprocess.run(
|
||||
[
|
||||
"python3",
|
||||
str(ROOT / "tools/package_sync_candidate.py"),
|
||||
"--output",
|
||||
str(output),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
inventory = json.loads((output / "candidate-inventory.json").read_text())
|
||||
self.assertEqual(inventory["product_version"], "0.1.2")
|
||||
self.assertTrue((output / "bin/trainlog").is_file())
|
||||
self.assertTrue((output / "bin/trainlog-sync-once").is_file())
|
||||
self.assertTrue((output / "bin/trainlog-syncd").is_file())
|
||||
self.assertTrue((output / "libexec/trainlog-sync-once").is_file())
|
||||
self.assertTrue(
|
||||
any(
|
||||
row["path"] == "tools/trainlog_syncd.py"
|
||||
for row in inventory["files"]
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
any(
|
||||
row["path"] == "tools/sync_peer_worker.py"
|
||||
for row in inventory["files"]
|
||||
)
|
||||
)
|
||||
|
||||
if __name__=="__main__": unittest.main()
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from pathlib import Path
|
|||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RUNTIME_TOOLS = [
|
||||
"trainlog_syncd.py",
|
||||
"sync_orchestrator.py",
|
||||
"sync_peer_worker.py",
|
||||
"sync_generation_exchange.py",
|
||||
|
|
@ -49,16 +50,20 @@ def main() -> int:
|
|||
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)
|
||||
|
|
@ -68,6 +73,14 @@ def main() -> int:
|
|||
launcher.write_text(
|
||||
'#!/bin/sh\nset -eu\nroot=$(CDPATH= cd -- "$(dirname -- "$0")/.." && 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\nroot=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\nexec "$root/libexec/trainlog-sync-once" "$@"\n'
|
||||
)
|
||||
syncd_launcher = args.output / "bin/trainlog-syncd"
|
||||
syncd_launcher.write_text(
|
||||
'#!/bin/sh\nset -eu\nroot=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\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",
|
||||
|
|
@ -88,6 +101,8 @@ def main() -> int:
|
|||
],
|
||||
"entry_points": [
|
||||
"bin/trainlog",
|
||||
"bin/trainlog-sync-once",
|
||||
"bin/trainlog-syncd",
|
||||
"libexec/trainlog-generation-mtp-adapter",
|
||||
"tools/sync_orchestrator.py",
|
||||
],
|
||||
|
|
@ -113,7 +128,10 @@ def main() -> int:
|
|||
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue