Sparkleは、アプリが更新対象かどうかを、ビルド番号(CFBundleVersion)で判定する。表示用のバージョン番号(CFBundleShortVersionString)は、判定に使われない。Xcodeのビルド設定では、ビルド番号がCURRENT_PROJECT_VERSION、バージョン番号がMARKETING_VERSIONである。

MARKETING_VERSION = 1.0.1;       # CFBundleShortVersionString(表示用)
CURRENT_PROJECT_VERSION = 2;     # CFBundleVersion(Sparkleの判定用)

ここでは、2つの番号の役割と、Sparkleのバージョン比較の挙動、CIでの番号の付け方を紹介する。実行例はSparkle 2.10.0、Xcode 27.0、macOS 27.0.1のもの。

2つの番号の役割

アプリの2つの番号は、次のように対応する。

Xcodeのビルド設定Info.plistのキーappcast.xmlの要素用途
CURRENT_PROJECT_VERSIONCFBundleVersionsparkle:versionSparkleの更新の判定
MARKETING_VERSIONCFBundleShortVersionStringsparkle:shortVersionStringユーザーに表示するバージョン

generate_appcastは、zipの中のアプリのInfo.plistから、2つの番号を読み取ってappcast.xmlに書く。

参考: 【Sparkle】generate_appcastでappcast.xmlを作る

<item>
    <title>1.0.1</title>
    <sparkle:version>2</sparkle:version>
    <sparkle:shortVersionString>1.0.1</sparkle:shortVersionString>
    ...
</item>

アプリが更新の通知で表示する「1.0.1」は、shortVersionStringである。更新を判断する元の値は、sparkle:versionの「2」である。

ビルドしたアプリの番号は、plutilで確認できる。

$ plutil -p [アプリ名].app/Contents/Info.plist | grep -E 'ShortVersion|BundleVersion'
  "CFBundleShortVersionString" => "1.0.1"
  "CFBundleVersion" => "2"

判定はsparkle:versionで行われる

appcast.xmlのsparkle:versionが、インストール済みのアプリのビルド番号より大きいときだけ、Sparkleは更新を通知する。表示用のバージョンが新しくても、ビルド番号が同じであれば、更新は通知されない。

appcast.xmlのうち、最新のエントリのsparkle:versionを、インストール済みのアプリと同じ1に変えて、更新を確認する。sparkle:shortVersionStringは1.0.1のままにしている。

<sparkle:version>1</sparkle:version>
<sparkle:shortVersionString>1.0.1</sparkle:shortVersionString>

すると、Sparkleは更新を通知せず、次のダイアログを表示する。

You’re up to date!
[アプリ名] 1.0.0 is currently the newest version available.

sparkle:versionが同じ、または小さいと、更新は通知されない。ビルド番号は、リリースのたびに増やす。

ビルド番号の比較の挙動

Sparkleは、SUStandardVersionComparatorでバージョン文字列を比較する。Sparkleのフレームワークをリンクした小さなSwiftのスクリプトから、比較の結果を調べる。

import Foundation
import Sparkle

let comparator = SUStandardVersionComparator()
let result = comparator.compareVersion("0.1.9", toVersion: "0.1.10")
print(result == .orderedAscending ? "<" : (result == .orderedDescending ? ">" : "=="))
$ swiftc -F [Sparkle.xcframeworkのmacosディレクトリ] -framework Sparkle \
    -Xlinker -rpath -Xlinker [同じディレクトリ] compare.swift -o compare
$ ./compare
<

主な比較の結果は、次のとおりである。

左比較右
0.1.9<0.1.10
1.9<1.10
2<10
200<1000
1.0==1.0.0
3==3.0.0.0
1.0<1.0.1

ドット区切りの数値は、成分ごとに数値で比較される。文字列として比べると1.10は1.9より小さいが、Sparkleは1.10を大きいと判定する。末尾の.0の有無は、比較の結果に影響しない。

英字を含む番号

b1のように、数字の後ろに英字が付く番号は、付かない番号よりも小さい。

左比較右
1.0b1<1.0b2
1.0a1<1.0b1
1.0b1<1.0
1.0rc1<1.0
1.0.0b1<1.0.0
1.0beta2<1.0beta10

ベータ版のビルド番号には、1.0b1や1.0rc1のように、ハイフンなしで英字を付ける。

ハイフンの後ろは比較されない

ハイフンを使うと、意図した順序にならない。ハイフンから後ろの文字列は、比較に使われない。

左比較右
1.0-beta==1.0
1.0-beta==1.0-alpha
1.0-beta2==1.0-beta10
1.0.0-rc1==1.0.0

1.0-betaと1.0が同じ値になるため、ベータ版から正式版への更新が通知されない。ビルド番号には、ハイフンを使わない。

スペースの後ろの文字列は、比較に使われる。1.0 betaは、1.0よりも大きい値になる。

CIでの番号の付け方

ビルド番号は、リリースごとに大きくなる値とする。GitHub Actionsでは、実行のたびに増えるgithub.run_numberを、ビルド番号として使える。バージョン番号は、タグの名前から作る。

- name: Archive
  env:
    VERSION: ${{ github.ref_name }}
    BUILD_NUMBER: ${{ github.run_number }}
  run: |
    xcodebuild -project [プロジェクト名].xcodeproj -scheme [スキーム名] -configuration Release \
      -archivePath build/[アプリ名].xcarchive \
      MARKETING_VERSION="${VERSION#v}" CURRENT_PROJECT_VERSION="$BUILD_NUMBER" archive

${VERSION#v}は、タグ名の先頭のvを取り除く。v1.0.1のタグから、MARKETING_VERSIONは1.0.1になる。

xcodebuildの引数でMARKETING_VERSIONとCURRENT_PROJECT_VERSIONを渡すと、project.pbxprojを書き換えずに、ビルドする番号を上書きできる。

github.run_numberは、ワークフローごとに採番される。v1.0.0をリリースしたワークフローの1回目の実行では1、v1.0.1をリリースした2回目の実行では2になる。同じリポジトリの別のワークフローを実行していても、リリース用のワークフローの番号は、独立して増える。

$ plutil -p [v1.0.0のアプリ].app/Contents/Info.plist | grep -E 'ShortVersion|BundleVersion'
  "CFBundleShortVersionString" => "1.0.0"
  "CFBundleVersion" => "1"
$ plutil -p [v1.0.1のアプリ].app/Contents/Info.plist | grep -E 'ShortVersion|BundleVersion'
  "CFBundleShortVersionString" => "1.0.1"
  "CFBundleVersion" => "2"

ワークフローごとに採番されるため、同じアプリを複数のワークフローでリリースすると、ビルド番号が重複したり、後退したりする場合がある。リリースは、1つのワークフローに集約する。

ローカルでバージョンを管理する方法は、agvtoolの記事で紹介している。

参考: agvtoolでXcodeプロジェクトのバージョンを管理する