feat: add initial AppStore catalog
This commit is contained in:
Executable
+101
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = []
|
||||
# ///
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
DESCRIPTION_PATTERN = re.compile(r"^--\s*DESCRIPTION:\s*(.+?)\s*$")
|
||||
|
||||
|
||||
def sha256(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def payload_files(app_dir: Path):
|
||||
for path in sorted(app_dir.rglob("*")):
|
||||
if path.is_symlink():
|
||||
raise ValueError(f"symlinks are not supported: {path.relative_to(ROOT)}")
|
||||
if path.is_file() and path.name != "manifest.txt":
|
||||
yield path
|
||||
|
||||
|
||||
def description(app_dir: Path) -> str:
|
||||
with (app_dir / "main.lua").open(encoding="utf-8") as source:
|
||||
for line in source:
|
||||
match = DESCRIPTION_PATTERN.match(line)
|
||||
if match:
|
||||
value = match.group(1)
|
||||
if "|" in value:
|
||||
raise ValueError(f"description contains '|': {app_dir.name}")
|
||||
return value
|
||||
raise ValueError(f"missing DESCRIPTION in {app_dir.name}/main.lua")
|
||||
|
||||
|
||||
def build_manifest(app_dir: Path) -> bytes:
|
||||
lines = ["XTEINK-MANIFEST|1"]
|
||||
for path in payload_files(app_dir):
|
||||
relative = path.relative_to(app_dir).as_posix()
|
||||
if "|" in relative:
|
||||
raise ValueError(f"path contains '|': {relative}")
|
||||
data = path.read_bytes()
|
||||
lines.append(f"{relative}|{len(data)}|{sha256(data)}")
|
||||
return ("\n".join(lines) + "\n").encode()
|
||||
|
||||
|
||||
def generated_files():
|
||||
apps = [path for path in ROOT.iterdir() if path.is_dir() and (path / "main.lua").is_file()]
|
||||
apps.sort(key=lambda path: path.name.casefold())
|
||||
|
||||
manifests = {}
|
||||
catalog = ["XTEINK-CATALOG|1"]
|
||||
for app_dir in apps:
|
||||
app_id = app_dir.name
|
||||
if not ID_PATTERN.fullmatch(app_id):
|
||||
raise ValueError(f"invalid app id: {app_id}")
|
||||
manifest = build_manifest(app_dir)
|
||||
manifest_path = f"{app_id}/manifest.txt"
|
||||
manifests[ROOT / manifest_path] = manifest
|
||||
catalog.append(
|
||||
f"{app_id}|{description(app_dir)}|{manifest_path}|{len(manifest)}|{sha256(manifest)}"
|
||||
)
|
||||
|
||||
manifests[ROOT / "catalog.txt"] = ("\n".join(catalog) + "\n").encode()
|
||||
return manifests
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Regenerate app manifests and catalog.txt")
|
||||
parser.add_argument("--check", action="store_true", help="fail instead of updating stale files")
|
||||
args = parser.parse_args()
|
||||
|
||||
stale = []
|
||||
for path, expected in generated_files().items():
|
||||
if not path.exists() or path.read_bytes() != expected:
|
||||
stale.append(path)
|
||||
if not args.check:
|
||||
temporary = path.with_suffix(path.suffix + ".tmp")
|
||||
temporary.write_bytes(expected)
|
||||
temporary.replace(path)
|
||||
print(f"updated {path.relative_to(ROOT)}")
|
||||
|
||||
if args.check and stale:
|
||||
for path in stale:
|
||||
print(f"stale: {path.relative_to(ROOT)}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except ValueError as error:
|
||||
print(error, file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user