Sparkleのアプリは、更新の情報が書かれたappcast.xmlを定期的に取得して、新しいバージョンがあるかを判断する。generate_appcastは、配布用のzipを置いたディレクトリから、appcast.xmlを生成する。zipの中のInfo.plistからバージョンを読み取り、EdDSAの署名も付ける。

$ generate_appcast --account [アカウント名] [zipを置いたディレクトリ]
Wrote 2 new updates, updated 0 existing updates, and removed 0 old updates in appcast.xml

実行例はSparkle 2.10.0、macOS 27.0.1のもの。generate_appcastの場所は、Sparkleの導入の記事で紹介している。署名には、鍵の生成の記事で作成した秘密鍵を使う。

参考: 【Sparkle】SwiftPMで導入してアプリに更新チェックを組み込む

appcast.xmlを生成する

配布するバージョンのzipを、1つのディレクトリに置く。ファイル名は、[アプリ名]-[バージョン].zipなどにする。

$ ls archives
ReleaseTestApp-1.0.zip
ReleaseTestApp-1.1.zip

ディレクトリを指定して、generate_appcastを実行する。

$ generate_appcast archives
Wrote 2 new updates, updated 0 existing updates, and removed 0 old updates in appcast.xml
$ ls archives
ReleaseTestApp-1.0.zip
ReleaseTestApp-1.1.zip
ReleaseTestApp2-1.delta
appcast.xml

ディレクトリには、appcast.xmlと、差分更新のファイル(.delta)が生成される。

generate_appcastは、zipを展開してアプリのInfo.plistを読み取る。展開した内容は、~/Library/Caches/Sparkle_generate_appcastにキャッシュされ、次回以降の実行で再利用される。

署名に使う鍵を指定する

秘密鍵は、既定ではキーチェーンのed25519のアカウントから読み取る。別のアカウント名で生成した鍵を使う場合は、--accountで指定する。

$ generate_appcast --account [アカウント名] archives

キーチェーンではなく、ファイルの秘密鍵を使うときは、--ed-key-fileにパスを指定する。-を指定すると、標準入力から読み取る。

$ generate_appcast --ed-key-file sparkle_private.key archives
$ echo "$SPARKLE_PRIVATE_KEY" | generate_appcast --ed-key-file - archives

標準入力であれば、CIのSecretsに登録した秘密鍵を、ファイルへ書き出さずに渡せる。

appcast.xmlの構造を読む

生成されたappcast.xmlは、バージョンごとにitemを持つ。

<?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
    <channel>
        <title>[アプリ名]</title>
        <item>
            <title>1.1</title>
            <pubDate>[公開日時]</pubDate>
            <sparkle:version>2</sparkle:version>
            <sparkle:shortVersionString>1.1</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>27.0</sparkle:minimumSystemVersion>
            <enclosure url="[zipのURL]" length="[zipのサイズ]" type="application/octet-stream" sparkle:edSignature="[署名]"/>
            <sparkle:deltas>
                <enclosure url="[deltaのURL]" sparkle:deltaFrom="1" length="[deltaのサイズ]" type="application/octet-stream" ... sparkle:edSignature="[署名]"/>
            </sparkle:deltas>
        </item>
        <item>
            <title>1.0</title>
            ...
            <sparkle:version>1</sparkle:version>
            <sparkle:shortVersionString>1.0</sparkle:shortVersionString>
            ...
        </item>
    </channel>
</rss>

主な要素は次のとおりである。

要素内容
titleバージョンの表示名
pubDate公開日時
sparkle:versionビルド番号(CFBundleVersion)。更新の判定に使う
sparkle:shortVersionString表示用のバージョン(CFBundleShortVersionString)
sparkle:minimumSystemVersion更新の対象となる最小のmacOSのバージョン。アプリのLSMinimumSystemVersionから推測される
enclosure更新のzipの情報。url、length、sparkle:edSignature(EdDSAの署名)を持つ
sparkle:deltas差分更新の情報

更新を判定するのはsparkle:versionで、sparkle:shortVersionStringは表示用である。

enclosureのURLの決まり方

zipのダウンロード先は、enclosureのurlに書かれる。--download-url-prefixを指定しなくても、urlは相対パスにならず、絶対のURLになる。

generate_appcastは、アプリのInfo.plistにあるSUFeedURLのディレクトリを、urlの基準にする。SUFeedURLがhttps://example.com/releases/latest/download/appcast.xmlであれば、zipのurlは、次のようになる。

https://example.com/releases/latest/download/[アプリ名]-1.1.zip

zipの置き場所がSUFeedURLと異なる場合は、--download-url-prefixで、URLの接頭辞を指定する。

$ generate_appcast --download-url-prefix "https://example.com/dl/" archives

zipと差分更新のファイルのurlが、指定した接頭辞になる。

https://example.com/dl/[アプリ名]-1.1.zip
https://example.com/dl/[アプリ名]2-1.delta

appcast.xmlと同じ場所にzipを置くのであれば、--download-url-prefixは不要である。

GitHub Releasesで配信する場合は、SUFeedURLがreleases/latest/download/になる。旧バージョンのエントリのurlも、同じlatest/download/のURLになる点には注意が必要である。この点は、GitHub Releasesの記事で解説する。

参考: 【GitHub Releases】appcast.xmlを固定URLで配信する

差分更新のファイルが生成される

2つ以上のバージョンのzipを置くと、generate_appcastは、差分更新のファイル([アプリ名]2-1.delta)を生成する。名前の2-1は、バージョン1から2への更新を表す。zipに比べて、サイズが小さい。

$ ls -l archives
-rw-r--r--  1 [ユーザー名]  staff  1046592  [日時]  ReleaseTestApp-1.0.zip
-rw-r--r--  1 [ユーザー名]  staff  1046590  [日時]  ReleaseTestApp-1.1.zip
-rw-r--r--  1 [ユーザー名]  staff     8830  [日時]  ReleaseTestApp2-1.delta
-rw-r--r--  1 [ユーザー名]  staff      ...  [日時]  appcast.xml

差分更新のファイルは、appcast.xmlにURLが書かれるが、配信先へのアップロードは自動では行われない。差分更新を使わない場合は、--maximum-deltas 0を指定する。.deltaファイルは生成されず、appcast.xmlにもsparkle:deltasが含まれない。

zipとdeltaの数は、次のオプションで制御する。

オプション内容既定値
--maximum-versionsappcast.xmlに残すバージョンの最大数。0なら、すべて残す3
--maximum-deltas最新の更新に対して作成する差分更新の最大数5

appcast.xmlから外れた古いバージョンのzipは、old_updatesディレクトリに移動される。--maximum-versions 1を指定した場合は、次のようになる。

$ generate_appcast --maximum-versions 1 --maximum-deltas 0 archives
Moved 1 old update file to old_updates
$ ls archives
ReleaseTestApp-1.1.zip
appcast.xml
old_updates
$ ls archives/old_updates
ReleaseTestApp-1.0.zip

過去のappcast.xmlを引き継ぐ

generate_appcastは、対象のディレクトリにappcast.xmlがすでにあれば、そのファイルを再利用して、新しいバージョンのエントリだけを追加する。

過去のappcast.xmlを、zipを置いたディレクトリの中に置いて実行すると、過去のエントリが引き継がれる。

$ ls archives
ReleaseTestApp-1.1.zip
appcast.xml
$ generate_appcast archives
Wrote 1 new update, updated 0 existing updates, and removed 0 old updates in appcast.xml
$ grep shortVersionString archives/appcast.xml
            <sparkle:shortVersionString>1.1</sparkle:shortVersionString>
            <sparkle:shortVersionString>1.0</sparkle:shortVersionString>

この例では、appcast.xmlに1.0のエントリがあり、新しく1.1のzipを追加している。1.0のzipがなくても、1.0のエントリは残る。

過去のappcast.xmlが引き継がれない構成

過去のappcast.xmlを取得して、generate_appcastの対象ではない場所に保存すると、過去のエントリは引き継がれない。たとえば、CIで、過去のappcast.xmlをカレントディレクトリにダウンロードして、新しいzipだけをappcast-input/に置いて実行する構成である。

$ curl -L -o appcast.xml "https://github.com/[ユーザー名]/[リポジトリ名]/releases/latest/download/appcast.xml"
$ mkdir -p appcast-input
$ cp [アプリ名]-1.1.zip appcast-input/
$ generate_appcast appcast-input/
Wrote 1 new update, updated 0 existing updates, and removed 0 old updates in appcast.xml
$ grep -c '<item>' appcast-input/appcast.xml
1

appcast-input/には、appcast.xmlがないため、新しいappcast.xmlが作られる。結果は、1.1だけのエントリになり、1.0のエントリは失われる。

過去のappcast.xmlは、generate_appcastの対象のディレクトリの中に保存する。

$ curl -L -o appcast-input/appcast.xml "https://github.com/[ユーザー名]/[リポジトリ名]/releases/latest/download/appcast.xml"

初回のリリースでは、過去のappcast.xmlが存在しないため、curlが404で失敗する。-fを付けると、エラーページをファイルに保存せず、失敗として扱える。||で、失敗しても処理を続ける。

$ curl -fsSL -o appcast-input/appcast.xml "[appcast.xmlのURL]" || echo "No previous appcast found"

過去のエントリが失われても、Sparkleは最新のエントリだけで更新を判定するため、更新自体は成功する。ただし、appcast.xmlの履歴と、差分更新の情報は失われる。

リリースノートを付ける

zipと同じ名前の.html、.md、.txtのファイルを、同じディレクトリに置くと、generate_appcastが、リリースノートとして扱う。

archives/
  [アプリ名]-1.1.zip
  [アプリ名]-1.1.html

.htmlのファイルにDOCTYPEやbodyのタグがない場合は、appcast.xmlのdescriptionに、CDATAとして埋め込まれる。--embed-release-notesを付けると、すべてのリリースノートを埋め込む。