From dd225b2b8e861a06971af077532ac89913dfe36b Mon Sep 17 00:00:00 2001 From: Micha Date: Mon, 8 Dec 2025 19:30:12 +0100 Subject: [PATCH] Sparkle fixes --- Sparkle/appcast-local.xml | 14 +++++++ scripts/make_local_appcast.sh | 75 ++++++++++++++++++++++++++++++++++ scripts/serve_local_appcast.sh | 25 ++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 Sparkle/appcast-local.xml create mode 100755 scripts/make_local_appcast.sh create mode 100755 scripts/serve_local_appcast.sh diff --git a/Sparkle/appcast-local.xml b/Sparkle/appcast-local.xml new file mode 100644 index 0000000..b61abdb --- /dev/null +++ b/Sparkle/appcast-local.xml @@ -0,0 +1,14 @@ + + + + iKeyMon + + 26.0.37 (reinstall) + Mon, 08 Dec 2025 18:17:55 +0000 + 26.0.37 + 1077 + 15.2 + + + + \ No newline at end of file diff --git a/scripts/make_local_appcast.sh b/scripts/make_local_appcast.sh new file mode 100755 index 0000000..03904a8 --- /dev/null +++ b/scripts/make_local_appcast.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate a local appcast that reuses the latest Sparkle entry but bumps the +# sparkle:version so Sparkle will reinstall the same build. Useful for testing +# updates without cutting a new release. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +INPUT="${1:-$ROOT_DIR/Sparkle/appcast.xml}" +OUTPUT="${2:-$ROOT_DIR/Sparkle/appcast-local.xml}" + +python3 - "$INPUT" "$OUTPUT" <<'PY' +import sys, xml.etree.ElementTree as ET, datetime, email.utils + +if len(sys.argv) != 3: + sys.exit("Usage: make_local_appcast.sh [input] [output]") + +src, dst = sys.argv[1], sys.argv[2] +tree = ET.parse(src) +root = tree.getroot() +channel = root.find("channel") +items = channel.findall("item") if channel is not None else [] +if not items: + sys.exit("No items found in appcast") + +latest = items[0] +enc = latest.find("enclosure") +if enc is None: + sys.exit("Latest item missing enclosure") + +sparkle_ns = "{http://www.andymatuschak.org/xml-namespaces/sparkle}" + +def get_text(tag): + el = latest.find(tag) + return el.text if el is not None else "" + +short_version = get_text(f"{sparkle_ns}shortVersionString") +version_el = latest.find(f"{sparkle_ns}version") +try: + next_build = str(int(version_el.text) + 1000 if version_el is not None else 9999) +except ValueError: + next_build = "9999" + +now_rfc822 = email.utils.format_datetime(datetime.datetime.now(datetime.timezone.utc)) + +new_item = ET.Element("item") +ET.SubElement(new_item, "title").text = f"{short_version} (reinstall)" +ET.SubElement(new_item, "pubDate").text = now_rfc822 +sv = ET.SubElement(new_item, f"{sparkle_ns}shortVersionString") +sv.text = short_version +bv = ET.SubElement(new_item, f"{sparkle_ns}version") +bv.text = next_build +min_os = latest.find(f"{sparkle_ns}minimumSystemVersion") +if min_os is not None and min_os.text: + ET.SubElement(new_item, f"{sparkle_ns}minimumSystemVersion").text = min_os.text +new_enc = ET.SubElement(new_item, "enclosure") +for attr in ("url", "length", "type", f"{sparkle_ns}edSignature"): + if attr in enc.attrib: + new_enc.set(attr, enc.attrib[attr]) + +new_channel = ET.Element("channel") +title = channel.find("title") +ET.SubElement(new_channel, "title").text = title.text if title is not None else "Local" +new_channel.append(new_item) + +new_root = ET.Element("rss", { + "version": "2.0", + "xmlns:sparkle": "http://www.andymatuschak.org/xml-namespaces/sparkle" +}) +new_root.append(new_channel) + +ET.indent(new_root, space=" ") +ET.ElementTree(new_root).write(dst, encoding="utf-8", xml_declaration=True) +print(f"✅ wrote {dst}") +PY diff --git a/scripts/serve_local_appcast.sh b/scripts/serve_local_appcast.sh new file mode 100755 index 0000000..a3b6097 --- /dev/null +++ b/scripts/serve_local_appcast.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate a local appcast and serve it over HTTP for Sparkle testing without +# publishing a new release. Sparkle disallows file:// feeds, so we use +# http://localhost:${PORT}/appcast-local.xml. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PORT="${PORT:-8000}" +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +"$ROOT_DIR/scripts/make_local_appcast.sh" "${1:-$ROOT_DIR/Sparkle/appcast.xml}" "$WORKDIR/appcast-local.xml" + +echo "🍏 Serving local appcast on http://127.0.0.1:${PORT}/appcast-local.xml" +echo "Set SUFeedURL to that URL, then launch iKeyMon and check for updates." +echo +echo "To set it:" +echo " defaults write net.24unix.iKeyMon SUFeedURL \"http://127.0.0.1:${PORT}/appcast-local.xml\"" +echo " killall iKeyMon 2>/dev/null; open /Applications/iKeyMon.app" +echo +echo "Press Ctrl+C to stop." + +cd "$WORKDIR" +python3 -m http.server "$PORT"