143 lines
7.0 KiB
Python
143 lines
7.0 KiB
Python
# Copyright (c) 2026 Storm Dragon
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Check the repository and the built Tentacle without running a reader."""
|
|
|
|
import json
|
|
import hashlib
|
|
import os
|
|
from pathlib import Path
|
|
import select
|
|
import subprocess
|
|
import tomllib
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
def require(condition, message):
|
|
if not condition:
|
|
raise RuntimeError(message)
|
|
|
|
|
|
def read_toml(path):
|
|
with path.open("rb") as source:
|
|
return tomllib.load(source)
|
|
|
|
|
|
def check_metadata(repoRoot):
|
|
repository = read_toml(repoRoot / "repository.toml")
|
|
catalog = read_toml(repoRoot / "catalog.toml")
|
|
require(repository["format-version"] == catalog["format-version"] == 1,
|
|
"Unsupported metadata format")
|
|
require(repository["id"] == "storm.cthulhu-tentacles" and repository["name"],
|
|
"Repository identity is missing")
|
|
require([item["id"] for item in catalog["tentacles"]] == ["hello-world", "wormhole"],
|
|
"Catalog must list Hello World and Wormhole")
|
|
for item in catalog["tentacles"]:
|
|
name = item["id"]
|
|
require(item["manifest"] == f"{name}/tentacle.toml", "Manifest path must match ID")
|
|
manifest = read_toml(repoRoot / item["manifest"])
|
|
require(manifest["format-version"] == 1 and manifest["id"] == name
|
|
and manifest["name"] and manifest["version"] == ("0.1.2" if name == "wormhole" else "0.1.0"), "Invalid Tentacle metadata")
|
|
expectedGrants = ["presentation"] + (["interaction"] if name == "wormhole" else [])
|
|
expectedProgram = "wormhole-tentacle" if name == "wormhole" else name
|
|
require(manifest["entry-point"] == [f"bin/{expectedProgram}"]
|
|
and manifest["capabilities"] == expectedGrants
|
|
and manifest["apis"] == ["commands"] + expectedGrants,
|
|
"Unexpected launch or grants")
|
|
require((repoRoot / "LICENSE").is_file() and (repoRoot / name / "LICENSE").is_file()
|
|
and (repoRoot / name / "README.md").is_file(), "License and help are required")
|
|
seen = set()
|
|
for release in item.get("releases", []):
|
|
key = (release["platform"], release["architecture"])
|
|
require(key not in seen and key[0] == "linux" and key[1] in ("amd64", "arm64"),
|
|
"Invalid or duplicate release target")
|
|
seen.add(key)
|
|
parsed = urlparse(release["url"])
|
|
require(parsed.scheme == "https" and parsed.hostname and not parsed.username
|
|
and not parsed.fragment, "Release URL must be HTTPS without credentials")
|
|
digest = release["sha256"]
|
|
require(len(digest) == 64 and all(character in "0123456789abcdef" for character in digest),
|
|
"Invalid SHA-256 digest")
|
|
artifactPath = repoRoot / "dist" / parsed.path.rsplit("/", 1)[-1]
|
|
if artifactPath.exists():
|
|
require(hashlib.sha256(artifactPath.read_bytes()).hexdigest() == digest,
|
|
"Prepared package does not match its catalog digest")
|
|
|
|
|
|
def check_registration(programPath, name):
|
|
apis = ["commands", "presentation"] + (["interaction"] if name == "wormhole" else [])
|
|
with subprocess.Popen([str(programPath)], stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process:
|
|
try:
|
|
initialization = {
|
|
"jsonrpc": "2.0", "id": "1", "method": "initialize",
|
|
"params": {"protocol": {"major": 1, "minor": 0}, "session": "check",
|
|
"apis": apis,
|
|
"capabilities": apis},
|
|
}
|
|
process.stdin.write((json.dumps(initialization) + "\n").encode())
|
|
process.stdin.flush()
|
|
require(select.select([process.stdout], [], [], 5)[0], "Registration timed out")
|
|
registration = json.loads(process.stdout.readline())
|
|
require(registration["id"] == "1" and registration["result"]["session"] == "check",
|
|
"Invalid registration response")
|
|
commands = registration["result"]["commands"]
|
|
require(len(commands) == (9 if name == "wormhole" else 1) and commands[0]["id"] == name
|
|
and commands[0]["presentation"] is True, "Unexpected commands or grant")
|
|
require(commands[0]["bindings"] == [
|
|
{"layout": layout, "scope": "global", "key": ("Cthulhu+Ctrl+Shift+W" if name == "wormhole" else "Cthulhu+Alt+Shift+H")}
|
|
for layout in ("desktop", "laptop")], "Wrong suggested shortcut")
|
|
if name == "wormhole":
|
|
require([command["id"] for command in commands if command.get("settings")]
|
|
== ["settings"], "Wrong registered settings entry point")
|
|
process.stdin.close()
|
|
require(process.wait(timeout=5) == 0, "Tentacle did not stop cleanly on EOF")
|
|
finally:
|
|
if process.poll() is None:
|
|
process.kill()
|
|
process.wait(timeout=5)
|
|
|
|
|
|
def check_host(repoRoot, programPath, name):
|
|
readerRoot = repoRoot.parent / "cthulhu"
|
|
environment = os.environ.copy()
|
|
environment.setdefault("GOCACHE", "/tmp/cthulhu-cache")
|
|
flags = ["-interaction"] if name == "wormhole" else []
|
|
result = subprocess.run([
|
|
"go", "run", "./cmd/tentacle-host", "-presentation",
|
|
*flags, "-command", name, str(programPath),
|
|
], cwd=readerRoot, env=environment, capture_output=True, text=True, timeout=60, check=True)
|
|
if name == "wormhole":
|
|
value = json.loads(result.stdout)
|
|
require(value.get("form", {}).get("title") == "Wormhole menu"
|
|
or value == {"speech": "Magic wormhole is not installed",
|
|
"braille": "Magic wormhole is not installed", "uninterruptible": True},
|
|
"Host returned an unexpected Wormhole menu")
|
|
result = subprocess.run([
|
|
"go", "run", "./cmd/tentacle-host", "-presentation", "-interaction",
|
|
"-command", "settings", str(programPath),
|
|
], cwd=readerRoot, env=environment, capture_output=True, text=True,
|
|
timeout=60, check=True)
|
|
value = json.loads(result.stdout)
|
|
require(value.get("form", {}).get("title") == "Wormhole preferences"
|
|
and value.get("next") == "save-settings",
|
|
"Host returned an unexpected Wormhole settings form")
|
|
return
|
|
require(json.loads(result.stdout) == {
|
|
"speech": "Hello world!", "braille": "Hello world!", "uninterruptible": True,
|
|
}, "The actual host returned an unexpected greeting")
|
|
|
|
|
|
def main():
|
|
repoRoot = Path(__file__).resolve().parents[1]
|
|
programPath = repoRoot / "hello-world/bin/hello-world"
|
|
check_metadata(repoRoot)
|
|
for name, binary in (("hello-world", "hello-world"), ("wormhole", "wormhole-tentacle")):
|
|
programPath = repoRoot / name / "bin" / binary
|
|
check_registration(programPath, name)
|
|
check_host(repoRoot, programPath, name)
|
|
print("Repository metadata, shortcut/settings registration and actual-host greeting/menu/settings checks passed.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|