Compare commits
3 Commits
10683ebc73
...
b96b018f70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b96b018f70 | ||
|
|
65a65939a7 | ||
|
|
25723b7f07 |
@@ -9,6 +9,8 @@ final class SparkleUpdater: NSObject, ObservableObject {
|
||||
}()
|
||||
private let logger = Logger(subsystem: "net.24unix.iKeyMon", category: "Sparkle")
|
||||
private let verboseLogging: Bool
|
||||
@Published var logMessages: [String] = []
|
||||
@Published var showLogs: Bool = false
|
||||
|
||||
override init() {
|
||||
self.verboseLogging = ProcessInfo.processInfo.environment["SPARKLE_VERBOSE_LOGGING"] == "1"
|
||||
@@ -34,6 +36,7 @@ final class SparkleUpdater: NSObject, ObservableObject {
|
||||
|
||||
private func log(_ message: String) {
|
||||
logger.log("\(message, privacy: .public)")
|
||||
addLogMessage("[INFO] \(message)")
|
||||
if verboseLogging {
|
||||
print("[Sparkle] \(message)")
|
||||
}
|
||||
@@ -41,11 +44,21 @@ final class SparkleUpdater: NSObject, ObservableObject {
|
||||
|
||||
private func logError(_ message: String) {
|
||||
logger.error("\(message, privacy: .public)")
|
||||
addLogMessage("[ERROR] \(message)")
|
||||
if verboseLogging {
|
||||
fputs("[Sparkle][error] \(message)\n", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
private func addLogMessage(_ message: String) {
|
||||
let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium)
|
||||
let timestampedMessage = "[\(timestamp)] \(message)"
|
||||
logMessages.append(timestampedMessage)
|
||||
if logMessages.count > 100 {
|
||||
logMessages.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
private func describe(update item: SUAppcastItem) -> String {
|
||||
let short = item.displayVersionString
|
||||
let build = item.versionString
|
||||
@@ -53,41 +66,58 @@ final class SparkleUpdater: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
extension SparkleUpdater: SPUUpdaterDelegate {
|
||||
func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
||||
log("Loaded Sparkle appcast containing \(appcast.items.count) item(s).")
|
||||
nonisolated func updater(_ updater: SPUUpdater, didFinishLoading appcast: SUAppcast) {
|
||||
Task { @MainActor in
|
||||
log("Loaded Sparkle appcast containing \(appcast.items.count) item(s).")
|
||||
}
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
||||
log("Found valid update \(describe(update: item))")
|
||||
nonisolated func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
|
||||
Task { @MainActor in
|
||||
log("Found valid update \(describe(update: item))")
|
||||
}
|
||||
}
|
||||
|
||||
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
|
||||
log("No updates available.")
|
||||
nonisolated func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
|
||||
Task { @MainActor in
|
||||
log("No updates available.")
|
||||
}
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, willDownloadUpdate item: SUAppcastItem, with request: NSMutableURLRequest) {
|
||||
log("Downloading \(describe(update: item)) from \(request.url?.absoluteString ?? "unknown URL")")
|
||||
nonisolated func updater(_ updater: SPUUpdater, willDownloadUpdate item: SUAppcastItem, with request: NSMutableURLRequest) {
|
||||
Task { @MainActor in
|
||||
log("Downloading \(describe(update: item)) from \(request.url?.absoluteString ?? "unknown URL")")
|
||||
}
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
||||
log("Finished downloading \(describe(update: item))")
|
||||
nonisolated func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
||||
Task { @MainActor in
|
||||
log("Finished downloading \(describe(update: item))")
|
||||
}
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
|
||||
logError("Failed to download \(describe(update: item)): \(error.localizedDescription)")
|
||||
nonisolated func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
|
||||
Task { @MainActor in
|
||||
logError("Failed to download \(describe(update: item)): \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func userDidCancelDownload(_ updater: SPUUpdater) {
|
||||
log("User cancelled Sparkle download.")
|
||||
nonisolated func userDidCancelDownload(_ updater: SPUUpdater) {
|
||||
Task { @MainActor in
|
||||
log("User cancelled Sparkle download.")
|
||||
}
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
|
||||
log("Will install update \(describe(update: item))")
|
||||
nonisolated func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
|
||||
Task { @MainActor in
|
||||
log("Will install update \(describe(update: item))")
|
||||
}
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
||||
logError("Sparkle aborted: \(error.localizedDescription)")
|
||||
nonisolated func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
|
||||
Task { @MainActor in
|
||||
logError("Sparkle aborted: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,11 +257,64 @@ private struct UpdatesPreferencesView: View {
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.top, 4)
|
||||
|
||||
Divider()
|
||||
.padding(.vertical, 8)
|
||||
|
||||
Button(action: { sparkleUpdater.showLogs.toggle() }) {
|
||||
Label(sparkleUpdater.showLogs ? "Hide Logs" : "Show Logs", systemImage: "terminal.fill")
|
||||
}
|
||||
|
||||
if sparkleUpdater.showLogs {
|
||||
logsView
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.toggleStyle(.switch)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var logsView: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Update Logs")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if sparkleUpdater.logMessages.isEmpty {
|
||||
Text("No logs yet. Check for updates to see activity.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
} else {
|
||||
ForEach(sparkleUpdater.logMessages, id: \.self) { message in
|
||||
Text(message)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(3)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(8)
|
||||
}
|
||||
.frame(height: 200)
|
||||
.background(Color(nsColor: .controlBackgroundColor))
|
||||
.cornerRadius(6)
|
||||
.border(.separator, width: 1)
|
||||
|
||||
Button(action: {
|
||||
sparkleUpdater.logMessages.removeAll()
|
||||
}) {
|
||||
Label("Clear Logs", systemImage: "trash")
|
||||
.font(.caption)
|
||||
}
|
||||
.disabled(sparkleUpdater.logMessages.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NotificationsPreferencesView: View {
|
||||
|
||||
16
Sparkle/appcast.xml
vendored
16
Sparkle/appcast.xml
vendored
@@ -2,6 +2,14 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>iKeyMon</title>
|
||||
<item>
|
||||
<title>26.0.41</title>
|
||||
<pubDate>Tue, 30 Dec 2025 13:18:56 +0100</pubDate>
|
||||
<sparkle:version>86</sparkle:version>
|
||||
<sparkle:shortVersionString>26.0.41</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.2</sparkle:minimumSystemVersion>
|
||||
<enclosure url="https://git.24unix.net/tracer/iKeyMon/releases/download/v26.0.41/iKeyMon-26.0.41.zip" length="4847296" type="application/octet-stream" sparkle:edSignature="8ITx6pM0LDVh0IpAz7eej7+eomnLTvRbchNaxSbdfQT/A16Bxg6F+FgtRDJkV5khJTrunpEPal9x3OUi7FO4Bg=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>26.0.40</title>
|
||||
<pubDate>Tue, 30 Dec 2025 13:01:18 +0100</pubDate>
|
||||
@@ -18,13 +26,5 @@
|
||||
<sparkle:minimumSystemVersion>15.2</sparkle:minimumSystemVersion>
|
||||
<enclosure url="https://git.24unix.net/tracer/iKeyMon/releases/download/v26.0.39/iKeyMon-26.0.39.zip" length="4829477" type="application/octet-stream" sparkle:edSignature="/J0LTgTj5uFa30okaLC7l6+wFUQxu8/E18y8vu3wqCXnM8tCG6TanZaGY69UWTUzaO9858oORy2yY6/MYtERBw=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>26.0.38</title>
|
||||
<pubDate>Tue, 30 Dec 2025 12:37:13 +0100</pubDate>
|
||||
<sparkle:version>80</sparkle:version>
|
||||
<sparkle:shortVersionString>26.0.38</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.2</sparkle:minimumSystemVersion>
|
||||
<enclosure url="https://git.24unix.net/tracer/iKeyMon/releases/download/v26.0.38/iKeyMon-26.0.38.zip" length="4829624" type="application/octet-stream" sparkle:edSignature="Pydppvzp9xiEXWOTObEyx6f7m3qZyFZYHCovclUk8xEbXNUAas70296TrjKvAhJvu9dLkObEyO39A/B2ZLQTAw=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -322,7 +322,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = iKeyMon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
|
||||
DEVELOPMENT_TEAM = Q5486ZVAFT;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -337,7 +337,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 26.0.40;
|
||||
MARKETING_VERSION = 26.0.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.24unix.iKeyMon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -353,7 +353,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = iKeyMon.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
|
||||
DEVELOPMENT_TEAM = Q5486ZVAFT;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -368,7 +368,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 26.0.40;
|
||||
MARKETING_VERSION = 26.0.41;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.24unix.iKeyMon;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"marketing_version": "26.0.40"
|
||||
"marketing_version": "26.0.41"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user