Files
lgtv_audio_cap/tools/make-manifest.py
T
Rene KievitsandClaude Opus 5 4f008c558b Add a no-SSH install path: a self-hosted Homebrew Channel repository
ares-install needs a working ssh into the TV, and this TV only has root
access (no Developer Mode, no ssh currently reachable). The Homebrew
Channel's own "Add repository" dialog accepts any URL that returns
{"packages": [...]}, which is the same schema its own gen-manifest.js
uses per app — confirmed by reading the app's source directly rather
than guessing at the format.

make-manifest.py now also writes out/repo.json, the existing manifest
wrapped in that shape, so publishing is: build the ipk, run the script
with --base-url pointing at wherever the release assets will live,
attach ipk + icon + repo.json there, then paste the repo.json URL into
the TV once. No ares, no ssh, no review queue.

Root elevation afterwards is unaffected either way: "Grant root access"
in the app calls elevate-service over the Luna bus from inside the
running app, which was already ssh-independent.

Corrected two inaccuracies in the process: webosbrew/repo doesn't exist
(the real project is webosbrew/apps-repo), and the Homebrew Channel has
no "install from file" UI — Browse-and-install or the /install Luna
service are the only ways in, both of which need a URL, not a local
path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:14:31 +02:00

108 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Writes the Homebrew Channel manifest for a built ipk, and a custom-repo
index that wraps it.
The manifest (out/manifest.json) is one app entry: id, ipkUrl, ipkHash, and so
on. It is what you submit to webosbrew/apps-repo to get into the official
store.
The repo index (out/repo.json) is that same entry wrapped as
`{"packages": [...]}`, which is the format the Homebrew Channel's own
"Add repository" dialog expects (Settings -> Repositories -> Add repository).
Host it anywhere static, paste its URL in, and the app shows up in Browse —
no submission, no review, no shell access to the TV at all.
python3 tools/make-manifest.py \\
--base-url https://git.example/you/lgtv-audio-cap/releases/download/v1.0.0
Defaults to the newest ipk in out/ and writes out/manifest.json + out/repo.json.
"""
import argparse
import glob
import hashlib
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.join(HERE, os.pardir)
SOURCE_URL = "https://git.crylia.de/Crylia/lgtv_audio_cap"
def newest_ipk(directory):
matches = sorted(glob.glob(os.path.join(directory, "*.ipk")),
key=os.path.getmtime, reverse=True)
return matches[0] if matches else None
def sha256(path):
digest = hashlib.sha256()
with open(path, "rb") as fh:
for block in iter(lambda: fh.read(1 << 20), b""):
digest.update(block)
return digest.hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
parser.add_argument("--ipk", default=None, help="path to the ipk")
parser.add_argument("--base-url", default=None,
help="URL the ipk and icon will be served from")
parser.add_argument("--source-url", default=SOURCE_URL,
help="project page shown in the Homebrew Channel")
parser.add_argument("--out", default=os.path.join(ROOT, "out", "manifest.json"))
parser.add_argument("--repo-out", default=os.path.join(ROOT, "out", "repo.json"),
help="path for the custom-repo index (set to '' to skip)")
args = parser.parse_args()
appinfo = json.load(open(os.path.join(ROOT, "frontend", "appinfo.json")))
ipk = args.ipk or newest_ipk(os.path.join(ROOT, "out"))
if not ipk or not os.path.exists(ipk):
print("no ipk found; run ./tools/build.sh first", file=sys.stderr)
return 1
name = os.path.basename(ipk)
base = (args.base_url or "").rstrip("/")
manifest = {
"id": appinfo["id"],
"version": appinfo["version"],
"type": appinfo.get("type", "web"),
"title": appinfo.get("title", appinfo["id"]),
"appDescription": appinfo.get("appDescription", ""),
"iconUri": base + "/icon.png" if base else "icon.png",
"sourceUrl": args.source_url,
# The service reads the TV's audio devices, which are root-only. The
# Homebrew Channel uses this to elevate the service at install time.
"rootRequired": True,
"ipkUrl": base + "/" + name if base else name,
"ipkHash": {"sha256": sha256(ipk)},
"ipkSize": os.path.getsize(ipk),
}
os.makedirs(os.path.dirname(args.out), exist_ok=True)
with open(args.out, "w") as fh:
json.dump(manifest, fh, indent=2)
fh.write("\n")
print(json.dumps(manifest, indent=2))
print("\nwrote %s" % os.path.relpath(args.out, ROOT), file=sys.stderr)
if args.repo_out:
os.makedirs(os.path.dirname(args.repo_out) or ".", exist_ok=True)
with open(args.repo_out, "w") as fh:
json.dump({"packages": [manifest]}, fh, indent=2)
fh.write("\n")
print("wrote %s" % os.path.relpath(args.repo_out, ROOT), file=sys.stderr)
if not base:
print("note: no --base-url given, so ipkUrl and iconUri are relative",
file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())