最後のピース — アップデートをどこに置くか
1 編では Developer ID 証明書と公証を、2 編では Sparkle 署名キーを準備しました。アプリへの署名、公証、そしてアップデートの真正性を検証する手段が揃いました。
しかし、2 編でアプリの Info.plist に書いた SUFeedURL (https://updates.example.com/appcast.xml) が指す場所には、まだ何もありません。この最終編では、その場所に置くアップデートフィードをホストし、ビルド設定を仕上げて、事前設定全体を完了させます。
1 編・2 編と同様、すべての名前・ドメイン (
FocusTimer、example.com、example-devなど) はサンプル値です。実際にはご自身の情報に置き換えて使用してください。
なぜアップデートリポジトリを別に用意するのか
自動アップデートが機能するには、2 つのものがインターネット上に公開されている必要があります。
appcast.xml— どのバージョンが最新で、どこで入手できるかを伝えるアップデートフィード.dmg— 実際のアプリインストーラーファイル
ここで重要な制約があります。アプリ内の Sparkle は、これらのファイルを認証なしのシンプルな HTTPS GET でダウンロードします。つまり、ユーザーのコンピューター上のアプリが、ログインなどの手続きなしに直接ダウンロードできる必要があります。
多くの開発者がアプリの本体ソースリポジトリをプライベートに保っています。しかし、プライベートリポジトリのリリースファイルは認証が必要なため、Sparkle でダウンロードできません。そのため、よく使われる構成がリポジトリの分離です。
- 本体リポジトリ (例:
FocusTimer) — ソースコード。プライベートにしても問題なし。 - アップデートリポジトリ (例:
FocusTimer-updates) —appcast.xmlのみをホスト。必ず公開 (public) にする。
この記事では、アップデートリポジトリを GitHub の無料静的ホスティング GitHub Pages で運用します。
ステップ 1 — 公開アップデートリポジトリの作成
GitHub で新しいリポジトリを作成します。
- New repository をクリック
- 名前:
FocusTimer-updates— この名前はすぐ後にフィードのアドレスで使われるため、大文字・小文字も含めて正確に設定します。 - オーナー (Owner): 自分のアカウントまたは組織 (例:
example-dev) - 公開範囲: Public — Sparkle が認証なしでダウンロードできるため必須です。
- Add a README file にチェック (最初のコミットを便利にするため)
- Create repository をクリック
ステップ 2 — GitHub Pages の有効化
作成したリポジトリを静的サイトとして公開します。
- リポジトリの Settings → 左メニューの Pages
- Source: Deploy from a branch
- Branch:
main/(root)を選択 → Save - 1〜2 分後、
https://example-dev.github.io/FocusTimer-updates/にアクセスできれば成功です。
この時点で、アップデートファイルを置ける公開アドレスが既に生まれています。ただし、もう一つのステップが残っています。
ステップ 3 — カスタムドメインの接続
GitHub Pages のデフォルトアドレス (example-dev.github.io/...) をそのまま使っても動作はします。しかし、このアドレスをアプリの SUFeedURL に埋め込んでしまうと、将来 GitHub Pages から他の場所にホスティングを移す必要が生じたとき問題になります。すでに配布済みのすべてのユーザーのアプリが、古いアドレスを参照しているためです。
解決策は、自分がコントロールするドメインを一層挟むことです。SUFeedURL を https://updates.example.com/appcast.xml のように自分のドメインに設定しておけば、後でホスティングを移す際に DNS 設定を一行変えるだけで、既存ユーザーが自動的に新しい場所を参照するようになります。この設定は一度行えば永続的に有効です。
3-1. DNS レコードの追加
ドメイン (example.com) を管理している DNS プロバイダーの設定画面で、次のレコードを追加します。
| 項目 | 値 |
|---|---|
| Type | CNAME |
| Name | updates |
| Value | example-dev.github.io |
| TTL | 3600 (デフォルト) |
これにより、updates.example.com というサブドメインが GitHub Pages を指すようになります。
3-2. リポジトリに CNAME ファイルを追加
アップデートリポジトリ (FocusTimer-updates) のルートに CNAME というファイルを作成し、内容にはドメインを 1 行だけ記述します。
updates.example.com
このファイルをコミットしてプッシュします。
3-3. GitHub Pages にドメインを登録
リポジトリの Settings → Pages → Custom domain 欄に updates.example.com を入力し、Save をクリックします。GitHub が自動的に HTTPS 証明書を発行し、発行が完了したら Enforce HTTPS にチェックを入れます。
3-4. 確認
DNS の伝播と証明書の発行には通常 10 分ほどかかります。しばらくしてから次のコマンドで確認します。
curl -I https://updates.example.com/appcast.xml
レスポンスの最初の行に HTTP/2 200 が表示されれば正常です。(まだ appcast.xml をアップロードしていない場合は 404 になることがありますが、ドメインと HTTPS 接続自体は他の方法でも確認できます。重要なのは https://updates.example.com がレスポンスを返すことです。)
なお、
.dmgインストーラーファイルは GitHub Pages ではなく GitHub Releases にアップロードするのが一般的です。大きなバイナリをリポジトリに直接入れると肥大化するためです。Releases は Pages とは異なるパスで提供されるため、上記のカスタムドメイン設定とは無関係にそのままにしておきます。
ステップ 4 — アップデートリポジトリをローカルに置く
リリース作業時に appcast.xml を編集・コミットするために、アップデートリポジトリをローカルにも取得しておく必要があります。本プロジェクトフォルダ内の適切な場所 (例: release/updates) にクローンしておくと、ビルド・リリーススクリプトが一か所で両方のリポジトリを扱えて便利です。
git clone [email protected]:example-dev/FocusTimer-updates.git release/updates
このように別のリポジトリの中にさらにリポジトリを置く場合は、本体リポジトリの .gitignore に release/updates/ を追加して、互いに干渉しないようにします。
ステップ 5 — ExportOptions.plist
次はビルド側の設定です。Xcode の xcodebuild -exportArchive コマンドは、アーカイブを配布用の .app としてエクスポートする際に ExportOptions.plist という設定ファイルを読み込みます。直接配布 (Developer ID チャネル) 用の内容は次のようになります。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>signingStyle</key>
<string>automatic</string>
<key>teamID</key>
<string>ABCDE12345</string>
</dict>
</plist>
method—developer-id。「Mac App Store ではなく直接配布」という意味です。signingStyle—automatic。1 編で発行した証明書を Xcode が自動的に選択して使えるようにします。teamID— 1 編でメモした Team ID。
このファイルをプロジェクト内 (例: release/ExportOptions.plist) に一度作成しておけば、すべてのリリースで再利用します。
ステップ 6 — アプリ側の設定の確認
最後に、アプリプロジェクト自体に入っている必要のある設定を整理します。新しいアプリに初めて Sparkle を組み込む場合は、このリストをチェックリストとして使ってください。
Info.plist
2 編で追加した Sparkle のキーです。Info.plist には次のキーが含まれている必要があります。
<key>SUFeedURL</key>
<string>https://updates.example.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>5vT3kQbA9mZ0wR1yX8cD2eF4gH6jK7lN0pS2uV5xW8c=</string>
SUFeedURL がステップ 3 で作成したカスタムドメインを指していること、SUPublicEDKey が 2 編で生成した公開鍵と一致していることを再確認してください。
権限 (Entitlements)
アプリが App Sandbox を使用している場合、Sparkle がアップデートのインストールのために内部サービスと通信できるよう、例外権限が必要です。FocusTimer.entitlements ファイルに次の項目を追加します。
<key>com.apple.security.app-sandbox</key><true/>
<key>com.apple.security.network.client</key><true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
</array>
$(PRODUCT_BUNDLE_IDENTIFIER) はビルド時に Xcode が実際のバンドル識別子 (com.example.FocusTimer) に自動的に置換します。各項目の詳細については Sparkle 公式サンドボックスガイドを参照してください。
直接配布のアプリにサンドボックスは必須ではありません (サンドボックスは Mac App Store の要件です)。サンドボックスを使用しないアプリの場合、上記の
mach-lookup例外は不要です。ただし、自動アップデートはネットワークを使用するため、network.client権限と Hardened Runtime は公証のためにオンにしておく必要があります。
ビルド設定 (Build Settings)
Xcode のビルド設定で次の項目を確認します。
CODE_SIGN_ENTITLEMENTS— 上記の権限 (entitlements) ファイルのパスENABLE_HARDENED_RUNTIME = YES— 公証の必須条件ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES— アップデート確認のためのネットワーク許可
シリーズまとめ — 事前設定の完了
3 編にわたる直接配布の事前設定がすべて完了しました。これで次のものが揃っています。
- ✅ (1 編) Developer ID Application 証明書 + 公証用の認証情報
- ✅ (2 編) Sparkle EdDSA 署名キーペア + 秘密鍵のバックアップ
- ✅ (3 編) カスタムドメインに接続した公開アップデートリポジトリ +
ExportOptions.plist+ アプリ側の設定
これが一度だけ行えばよい設定です。これらの作業は、今後新しいバージョンをリリースするたびにやり直す必要はありません。
今後、新しいバージョンを配布する流れは毎回ほぼ同じように繰り返されます — アーカイブのビルド → ExportOptions.plist でのエクスポート → Developer ID 証明書で署名 → notarytool で公証 → .dmg 作成 → Sparkle キーで署名 → appcast.xml を更新 → .dmg を GitHub Release にアップロード。この繰り返しのプロセスはほとんどスクリプト一本で自動化でき、それは別の記事で扱うテーマです。
その中でも
.dmgの作成は、背景画像やアイコン配置などのデザイン要素が絡んでいるため、別シリーズ macOS アプリ配布用 DMG をデザインするで詳しく解説しています。
直接配布は最初に設定するものが多く、面倒に見えるかもしれませんが、重要なのは**「一度整えておけば継続して再利用される」**という点です。App Store の利便性の一部を諦める代わりに、配布のすべてのプロセスを自分でコントロールできるようになります。