最後一塊拼圖 — 更新檔案放在哪裡

第 1 篇完成了 Developer ID 憑證和公證的設定,第 2 篇完成了 Sparkle 簽署金鑰的準備。至此,我們已具備簽署應用程式、公證應用程式,以及驗證更新真實性的全部手段。

然而,第 2 篇中寫入應用程式 Info.plistSUFeedURLhttps://updates.example.com/appcast.xml)所指向的位置,目前還什麼都沒有。本篇(最終篇)將託管更新 Feed,並完成建置設定,收尾整個一次性準備工作。

與第 1、2 篇相同,所有名稱和網域名稱(FocusTimerexample.comexample-dev 等)均為範例值,實際使用時請替換為你自己的資訊。

為何要單獨建立更新儲存庫

自動更新要正常運作,網際網路上必須有以下兩樣內容:

  • appcast.xml — 更新 Feed,告知應用程式哪個版本是最新的以及從哪裡取得
  • .dmg — 實際的應用程式安裝檔案

這裡有一個重要限制:應用程式內的 Sparkle 透過簡單的、無認證的 HTTPS GET 請求取得這些檔案。也就是說,使用者電腦上的應用程式必須能夠不經任何登入等驗證流程直接下載。

很多開發者將應用程式的主原始碼儲存庫設為私有 (private)。然而私有儲存庫的發布檔案需要認證,Sparkle 無法取得。因此,常見的做法是分離儲存庫

  • 主儲存庫(如 FocusTimer)— 原始碼,可設為私有。
  • 更新儲存庫(如 FocusTimer-updates)— 僅託管 appcast.xml必須為公開 (public)

本文將使用 GitHub 免費的靜態託管服務 GitHub Pages 運營更新儲存庫。

第 1 步 — 建立公開更新儲存庫

在 GitHub 上建立新儲存庫。

  1. 點按 New repository
  2. 名稱:FocusTimer-updates — 該名稱稍後會用於 Feed 位址,請精確設定,包括大小寫。
  3. 擁有者 (Owner):你的帳戶或組織(範例:example-dev
  4. 可見性:Public — 必須為公開,Sparkle 需要無認證即可存取。
  5. 勾選 Add a README file(方便建立第一個提交)
  6. 點按 Create repository

第 2 步 — 啟用 GitHub Pages

將剛建立的儲存庫作為靜態網站發布。

  1. 進入儲存庫的 Settings → 左側選單選擇 Pages
  2. Source:選擇 Deploy from a branch
  3. Branch:選擇 main / (root) → 點按 Save
  4. 等待 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 服務商的設定介面中,新增以下記錄:

欄位
TypeCNAME
Nameupdates
Valueexample-dev.github.io
TTL3600(預設值)

這樣,updates.example.com 子網域就會指向 GitHub Pages。

3-2. 在儲存庫中新增 CNAME 檔案

在更新儲存庫(FocusTimer-updates)的根目錄建立名為 CNAME 的檔案,內容只寫一行網域名稱:

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 Releases,而非 GitHub Pages。直接將大型二進位檔案放入儲存庫會使儲存庫體積膨脹。Releases 的服務路徑與 Pages 不同,因此與上述自訂網域設定無關,保持現狀即可。

第 4 步 — 在本地保存更新儲存庫

發布時需要編輯並提交 appcast.xml,因此要在本地也複製一份更新儲存庫。將其複製到主專案資料夾內的適當位置(如 release/updates),建置和發布腳本就能在同一個地方操作兩個儲存庫,非常方便。

git clone [email protected]:example-dev/FocusTimer-updates.git release/updates

將一個儲存庫巢狀在另一個儲存庫中時,請將 release/updates/ 新增到主儲存庫的 .gitignore 中,以避免相互干擾。

第 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>
  • methoddeveloper-id,表示「直接分發,而非 Mac App Store」。
  • signingStyleautomatic,讓 Xcode 自動選用第 1 篇中申請的憑證。
  • 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>

建置時,Xcode 會將 $(PRODUCT_BUNDLE_IDENTIFIER) 自動替換為實際的 Bundle Identifier(com.example.FocusTimer)。詳細條目請參考 Sparkle 官方沙箱指南

對於直接分發的應用程式,沙箱並非必須(沙箱是 Mac App Store 的要求)。若應用程式不使用沙箱,則無需上述 mach-lookup 例外。但由於自動更新需要使用網路,network.client 權限和 Hardened Runtime 必須開啟,以滿足公證要求。

建置設定 (Build Settings)

在 Xcode 的建置設定中確認以下項目:

  • CODE_SIGN_ENTITLEMENTS — 上述權限檔案的路徑
  • ENABLE_HARDENED_RUNTIME = YES — 公證的必要條件
  • ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES — 允許用於檢查更新的網路存取

系列小結 — 一次性準備工作完成

歷經三篇的直接分發一次性準備工作全部完成。現在你已經準備好以下內容:

  • (第 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 的部分便利,換來的是對整個分發流程的完全掌控。

參考資料