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】EdDSA署名鍵を生成して公開鍵をInfo.plistに設定する
- 親記事: GitHubを使い、macOSアプリを署名・公証・自動アップデート付きで公開する
- 次の記事: 【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-versions | appcast.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を付けると、すべてのリリースノートを埋め込む。
