103 lines
3.3 KiB
Python
Executable File
103 lines
3.3 KiB
Python
Executable File
#!/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
|
|
APPS_DIR = ROOT / "apps"
|
|
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 APPS_DIR.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"apps/{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)
|