OrcaSlicer/scripts/generate_appcast.py
2026-01-04 21:56:59 +08:00

208 lines
6.7 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Generate appcast.xml for Sparkle/WinSparkle auto-updates.
This script generates an appcast XML file that can be used by both
Sparkle (macOS) and WinSparkle (Windows) for auto-update functionality.
Usage:
python generate_appcast.py --version 2.1.0 \
--win-url https://github.com/.../OrcaSlicer_Windows.exe \
--win-signature "BASE64_SIGNATURE" \
--win-length 150000000 \
--mac-url https://github.com/.../OrcaSlicer_Mac.dmg \
--mac-signature "BASE64_SIGNATURE" \
--mac-length 200000000 \
--release-notes-url https://github.com/.../releases/tag/v2.1.0 \
--output appcast.xml
"""
import argparse
import os
import sys
from datetime import datetime, timezone
from xml.etree import ElementTree as ET
from xml.dom import minidom
SPARKLE_NS = "http://www.andymatuschak.org/xml-namespaces/sparkle"
DC_NS = "http://purl.org/dc/elements/1.1/"
def create_appcast(
version: str,
release_notes_url: str,
win_url: str = None,
win_signature: str = None,
win_length: int = None,
mac_url: str = None,
mac_signature: str = None,
mac_length: int = None,
title: str = "OrcaSlicer Updates",
description: str = "Most recent updates to OrcaSlicer",
link: str = "https://github.com/OrcaSlicer/OrcaSlicer",
) -> str:
"""
Create an appcast XML string.
Args:
version: Version string (e.g., "2.1.0")
release_notes_url: URL to release notes HTML page
win_url: Download URL for Windows installer
win_signature: EdDSA signature for Windows installer
win_length: File size in bytes for Windows installer
mac_url: Download URL for macOS DMG
mac_signature: EdDSA signature for macOS DMG
mac_length: File size in bytes for macOS DMG
title: Feed title
description: Feed description
link: Link to project homepage
Returns:
XML string of the appcast
"""
# Register namespaces
ET.register_namespace("sparkle", SPARKLE_NS)
ET.register_namespace("dc", DC_NS)
# Create root RSS element
rss = ET.Element("rss")
rss.set("version", "2.0")
rss.set(f"xmlns:sparkle", SPARKLE_NS)
rss.set(f"xmlns:dc", DC_NS)
# Create channel
channel = ET.SubElement(rss, "channel")
ET.SubElement(channel, "title").text = title
ET.SubElement(channel, "link").text = link
ET.SubElement(channel, "description").text = description
ET.SubElement(channel, "language").text = "en"
# Create item for this release
item = ET.SubElement(channel, "item")
ET.SubElement(item, "title").text = f"Version {version}"
# Publication date in RFC 2822 format
pub_date = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
ET.SubElement(item, "pubDate").text = pub_date
# Release notes link
release_notes = ET.SubElement(item, f"{{{SPARKLE_NS}}}releaseNotesLink")
release_notes.text = release_notes_url
# Windows enclosure
if win_url and win_signature:
win_enclosure = ET.SubElement(item, "enclosure")
win_enclosure.set("url", win_url)
win_enclosure.set(f"{{{SPARKLE_NS}}}version", version)
win_enclosure.set(f"{{{SPARKLE_NS}}}os", "windows")
win_enclosure.set(f"{{{SPARKLE_NS}}}edSignature", win_signature)
if win_length:
win_enclosure.set("length", str(win_length))
win_enclosure.set("type", "application/octet-stream")
# macOS enclosure
if mac_url and mac_signature:
mac_enclosure = ET.SubElement(item, "enclosure")
mac_enclosure.set("url", mac_url)
mac_enclosure.set(f"{{{SPARKLE_NS}}}version", version)
mac_enclosure.set(f"{{{SPARKLE_NS}}}os", "macos")
mac_enclosure.set(f"{{{SPARKLE_NS}}}edSignature", mac_signature)
if mac_length:
mac_enclosure.set("length", str(mac_length))
mac_enclosure.set("type", "application/octet-stream")
# Convert to pretty-printed string
rough_string = ET.tostring(rss, encoding="unicode", method="xml")
reparsed = minidom.parseString(rough_string)
pretty_xml = reparsed.toprettyxml(indent=" ", encoding=None)
# Remove extra blank lines
lines = [line for line in pretty_xml.split("\n") if line.strip()]
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Generate appcast.xml for OrcaSlicer auto-updates",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--version", "-v", required=True, help="Version string (e.g., 2.1.0)"
)
parser.add_argument(
"--release-notes-url",
"-r",
required=True,
help="URL to release notes page",
)
parser.add_argument(
"--win-url", help="Download URL for Windows installer"
)
parser.add_argument(
"--win-signature", help="EdDSA signature for Windows installer"
)
parser.add_argument(
"--win-length", type=int, help="File size in bytes for Windows installer"
)
parser.add_argument(
"--mac-url", help="Download URL for macOS DMG"
)
parser.add_argument(
"--mac-signature", help="EdDSA signature for macOS DMG"
)
parser.add_argument(
"--mac-length", type=int, help="File size in bytes for macOS DMG"
)
parser.add_argument(
"--output", "-o", default="appcast.xml", help="Output file path"
)
parser.add_argument(
"--title", default="OrcaSlicer Updates", help="Feed title"
)
parser.add_argument(
"--link",
default="https://github.com/OrcaSlicer/OrcaSlicer",
help="Project homepage URL",
)
args = parser.parse_args()
# Validate that at least one platform is specified
has_windows = args.win_url and args.win_signature
has_macos = args.mac_url and args.mac_signature
if not has_windows and not has_macos:
print(
"Error: At least one platform (Windows or macOS) must have both URL and signature",
file=sys.stderr,
)
sys.exit(1)
# Generate appcast
xml_content = create_appcast(
version=args.version,
release_notes_url=args.release_notes_url,
win_url=args.win_url,
win_signature=args.win_signature,
win_length=args.win_length,
mac_url=args.mac_url,
mac_signature=args.mac_signature,
mac_length=args.mac_length,
title=args.title,
link=args.link,
)
# Write output
if args.output == "-":
print(xml_content)
else:
with open(args.output, "w", encoding="utf-8") as f:
f.write(xml_content)
print(f"Appcast written to: {args.output}")
if __name__ == "__main__":
main()