feat(catalog): separate generated production and local test authority
The launcher previously defaulted to fixtures and production generation used the source database directly. Generate a separate database and manifest set from package directories, validating staged output with the application loader before installation. Keep strict selection by default, with independent opt-ins for missing games and package version overrides in the copied database. Share database filtering and staging with the fixture publisher, and include source metadata and generation modes in the publication cache. Make normal runs consume existing production authority. Add an explicit local test recipe with separate resources, app settings, and a compiled startup game directory. Build-time gates exclude local authority from production and require Tauri development mode. Document the generation and launch workflows. Clear generated catalog copies before Tauri copies the selected resource tree so mode switches and reduced catalogs cannot retain stale manifests. Watch the copied files to repair deletion and preserve prior output mtimes only when the bytes are unchanged, allowing subsequent builds to become fresh. Test Plan: - `just fmt` -- passed. - `just clippy` -- passed with warnings denied. - `just test` -- workspace tests passed using fixture authority. - `just frontend-test` -- 94 passed. - `python3 -m unittest discover -s tools -p 'test_catalog_source_cache.py'` -- 4 passed. - `git diff --cached --check` -- passed. - Interactive GUI launches and production bundles were not exercised.
This commit is contained in:
1 parent
ab0c95b80f
commit
6b65a66465
21 files changed
+1777
-273
No files matched your search
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cheap source metadata cache for complete catalog publication."""
|
||||
"""Cheap source metadata cache for production catalog publication."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
CACHE_SCHEMA = 1
|
||||
CACHE_SCHEMA = 3
|
||||
CATALOG_CONTENT_INDEX = "catalog-content-index-v1.jsonl"
|
||||
CATALOG_PUBLICATION_MARKER = ".lanspread-catalog-publication-in-progress"
|
||||
|
||||
@@ -74,8 +74,11 @@ def _path_metadata(path: Path) -> dict[str, int | str]:
|
||||
def _cache_payload(
|
||||
packages_dir: Path,
|
||||
catalog_db: Path,
|
||||
source_catalog_db: Path,
|
||||
manifests_dir: Path,
|
||||
unrar: Path,
|
||||
allow_missing_games: bool,
|
||||
allow_version_mismatch: bool,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"schema": CACHE_SCHEMA,
|
||||
@@ -84,11 +87,17 @@ def _cache_payload(
|
||||
"path": _absolute(catalog_db),
|
||||
**_path_metadata(catalog_db),
|
||||
},
|
||||
"source_catalog_db": {
|
||||
"path": _absolute(source_catalog_db),
|
||||
**_path_metadata(source_catalog_db),
|
||||
},
|
||||
"manifests_dir": _absolute(manifests_dir),
|
||||
"unrar": {
|
||||
"path": _absolute(unrar),
|
||||
**_path_metadata(unrar),
|
||||
},
|
||||
"allow_missing_games": allow_missing_games,
|
||||
"allow_version_mismatch": allow_version_mismatch,
|
||||
"packages_fingerprint": _tree_fingerprint(packages_dir),
|
||||
}
|
||||
|
||||
@@ -110,8 +119,11 @@ def cache_is_current(
|
||||
stamp: Path,
|
||||
packages_dir: Path,
|
||||
catalog_db: Path,
|
||||
source_catalog_db: Path,
|
||||
manifests_dir: Path,
|
||||
unrar: Path,
|
||||
allow_missing_games: bool = False,
|
||||
allow_version_mismatch: bool = False,
|
||||
) -> bool:
|
||||
if not _output_is_ready(catalog_db, manifests_dir):
|
||||
return False
|
||||
@@ -119,21 +131,40 @@ def cache_is_current(
|
||||
cached = json.loads(stamp.read_text(encoding="utf-8"))
|
||||
except (FileNotFoundError, OSError, UnicodeDecodeError, json.JSONDecodeError):
|
||||
return False
|
||||
return cached == _cache_payload(packages_dir, catalog_db, manifests_dir, unrar)
|
||||
return cached == _cache_payload(
|
||||
packages_dir,
|
||||
catalog_db,
|
||||
source_catalog_db,
|
||||
manifests_dir,
|
||||
unrar,
|
||||
allow_missing_games,
|
||||
allow_version_mismatch,
|
||||
)
|
||||
|
||||
|
||||
def record_cache(
|
||||
stamp: Path,
|
||||
packages_dir: Path,
|
||||
catalog_db: Path,
|
||||
source_catalog_db: Path,
|
||||
manifests_dir: Path,
|
||||
unrar: Path,
|
||||
allow_missing_games: bool = False,
|
||||
allow_version_mismatch: bool = False,
|
||||
) -> None:
|
||||
if not _output_is_ready(catalog_db, manifests_dir):
|
||||
raise RuntimeError("cannot record a catalog cache before publication is complete")
|
||||
stamp.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = json.dumps(
|
||||
_cache_payload(packages_dir, catalog_db, manifests_dir, unrar),
|
||||
_cache_payload(
|
||||
packages_dir,
|
||||
catalog_db,
|
||||
source_catalog_db,
|
||||
manifests_dir,
|
||||
unrar,
|
||||
allow_missing_games,
|
||||
allow_version_mismatch,
|
||||
),
|
||||
ensure_ascii=True,
|
||||
indent=2,
|
||||
sort_keys=True,
|
||||
@@ -162,8 +193,11 @@ def _parser() -> argparse.ArgumentParser:
|
||||
subparser.add_argument("--stamp", type=Path, required=True)
|
||||
subparser.add_argument("--packages-dir", type=Path, required=True)
|
||||
subparser.add_argument("--catalog-db", type=Path, required=True)
|
||||
subparser.add_argument("--source-catalog-db", type=Path, required=True)
|
||||
subparser.add_argument("--manifests-dir", type=Path, required=True)
|
||||
subparser.add_argument("--unrar", type=Path, required=True)
|
||||
subparser.add_argument("--allow-missing-games", action="store_true")
|
||||
subparser.add_argument("--allow-version-mismatch", action="store_true")
|
||||
return parser
|
||||
|
||||
|
||||
@@ -173,8 +207,11 @@ def main(arguments: list[str] | None = None) -> int:
|
||||
options.stamp,
|
||||
options.packages_dir,
|
||||
options.catalog_db,
|
||||
options.source_catalog_db,
|
||||
options.manifests_dir,
|
||||
options.unrar,
|
||||
options.allow_missing_games,
|
||||
options.allow_version_mismatch,
|
||||
)
|
||||
if options.command == "check":
|
||||
if current:
|
||||
@@ -187,8 +224,11 @@ def main(arguments: list[str] | None = None) -> int:
|
||||
options.stamp,
|
||||
options.packages_dir,
|
||||
options.catalog_db,
|
||||
options.source_catalog_db,
|
||||
options.manifests_dir,
|
||||
options.unrar,
|
||||
options.allow_missing_games,
|
||||
options.allow_version_mismatch,
|
||||
)
|
||||
print(f"catalog cache recorded: {options.stamp}")
|
||||
return 0
|
||||
|
||||
@@ -20,6 +20,8 @@ class CatalogSourceCacheTests(unittest.TestCase):
|
||||
(self.package / "game.eti").write_bytes(b"eti")
|
||||
self.catalog_db = self.root / "game.db"
|
||||
self.catalog_db.write_bytes(b"catalog")
|
||||
self.source_catalog_db = self.root / "source-game.db"
|
||||
self.source_catalog_db.write_bytes(b"source catalog")
|
||||
self.unrar = self.root / "unrar"
|
||||
self.unrar.write_bytes(b"unrar")
|
||||
self.manifests = self.root / "manifests"
|
||||
@@ -38,6 +40,7 @@ class CatalogSourceCacheTests(unittest.TestCase):
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
@@ -46,6 +49,7 @@ class CatalogSourceCacheTests(unittest.TestCase):
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
@@ -54,6 +58,7 @@ class CatalogSourceCacheTests(unittest.TestCase):
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
@@ -64,6 +69,7 @@ class CatalogSourceCacheTests(unittest.TestCase):
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
@@ -74,6 +80,7 @@ class CatalogSourceCacheTests(unittest.TestCase):
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
@@ -83,6 +90,7 @@ class CatalogSourceCacheTests(unittest.TestCase):
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
@@ -93,15 +101,63 @@ class CatalogSourceCacheTests(unittest.TestCase):
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
stamp = json.loads(self.stamp.read_text(encoding="utf-8"))
|
||||
self.assertEqual(stamp["schema"], 1)
|
||||
self.assertEqual(stamp["schema"], 3)
|
||||
self.assertEqual(stamp["catalog_db"]["size"], len(b"catalog"))
|
||||
self.assertEqual(
|
||||
stamp["source_catalog_db"]["size"], len(b"source catalog")
|
||||
)
|
||||
self.assertEqual(stamp["unrar"]["size"], len(b"unrar"))
|
||||
self.assertEqual(stamp["packages_dir"], str(self.packages.absolute()))
|
||||
|
||||
def test_source_catalog_and_generation_modes_are_cache_inputs(self) -> None:
|
||||
catalog_source_cache.record_cache(
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
self.source_catalog_db.write_bytes(b"changed source catalog")
|
||||
self.assertFalse(
|
||||
catalog_source_cache.cache_is_current(
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
)
|
||||
)
|
||||
self.source_catalog_db.write_bytes(b"source catalog")
|
||||
self.assertFalse(
|
||||
catalog_source_cache.cache_is_current(
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
allow_missing_games=True,
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
catalog_source_cache.cache_is_current(
|
||||
self.stamp,
|
||||
self.packages,
|
||||
self.catalog_db,
|
||||
self.source_catalog_db,
|
||||
self.manifests,
|
||||
self.unrar,
|
||||
allow_version_mismatch=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user