有做Mac App开发的朋友应该多少都听说过Sparkle Framework。因为macOS允许在Mac App Store以外进行App分发,所以如果你的App不上App Store那就得自己解决App更新问题。Sparkle就是一个非常好用的解决方案。
Sparkle的原理也很简单,以appcast.xml
文件为数据规范,提供客户端的检查更新、下载、数据校验、自动替换等通用能力。App的CDN存储、更新信息的XML文件hosting由开发者自行解决。
客户端的部分提供了多个进程,被打包进主App,non-sandbox app逻辑比较简单,直接由Autoupdate.app
来下载更新就行,但是sandboxed app就比较麻烦了,光是xpc的部分Sparkle就做了多个binary processes。
Sparkle的接入并不复杂,一般根据Sparkle官方文档操作就能完成,这里我把接入过程和遇到的问题写出来记录分享一下。
1. 接入Sparkle
推荐使用CocoaPods
接入,最简单。
use_frameworks!
//...
pod 'Sparkle'
主工程里初始化SUUpdater
,我一般放在AppDelegate.swift
里面,或者跟它同级别的实例:
var updater: SUUpdater!
func setupUpdater() {
updater.feedURL = URL(string: url)
updater.automaticallyChecksForUpdates = true
updater.updateCheckInterval = 606012 // 12hrs
updater.sendsSystemProfile = true
updater.delegate = self
}
url
就是你的server host的appcast.xml
,比如Just Focus的就是这样的:
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>JustFocus</title>
<item>
<title>2.0.0</title>
<pubDate>Mon, 15 Mar 2021 00:28:29 +0800</pubDate>
<sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
<enclosure url="https://your/update/file.zip" sparkle:version="601" sparkle:shortVersionString="2.0.0" length="15981682" type="application/octet-stream" sparkle:edSignature="<you-ed-signature>"/>
<description>
<h2>2.0.0 (Beta 1)</h2>
<p>Build 599, 14 March 2021</p>
<ul>
<li>New: Custom quotes supported.</li>
</ul>
</description>
</item>
</channel>
</rss>
最好是自己有一个一键打包上传脚本,自动更新一下appcast.xml
文件。其中edSignature
是Sparkle用于校验下载的包的,采用EdDSA (ed25519)
算法,是一种非对称加密算法。
我们可以通过Sparkle提供的./bin/generate_keys
工具来生成公私钥,公钥写进App的Info.plist
文件里,私钥默认存进Mac的Keychains。每次打包完就用Sparkle提供的generate_appcast
工具自动生成签名和xml文件即可。
需要注意的是:如果你换了一台编译机,那么私钥务必记得带过去,否则工具可能不会报错,但是会无法生成edSignature
导致App自动更新失败。
2. Hardened Runtime & Notarization
以往开发者最熟悉的安全需求应该是Code Signing,macOS可以很好地校验代码的安全性,主要是防止被第三方篡改,后来苹果在此基础上又增加了一大堆有的没的权限校验,给开发者带来不少麻烦。
苹果在2018年发布的macOS Mojave系统带上了一个安全性更新:Hardened Runtime。在code signing阶段,Xcode会自动给app打上一个flag,这样Cocoa runtime会在运行时进行一系列检查校验,未经授权的操作就会失败。对于开发者来讲,就是要在Xcode工程中的Capability选项中打开Hardened Runtime,并且勾选自己需要的权限,比如说MAP_JIT
允许你的App用mmap()
分配一块可写可运行的内存,比如JSCore
需要的JIT优化。
这个选项本来不是必须的,但是2019年WWDC之后苹果要求所有在Mac App Store以外分发的应用都需要进行Notarization。简单说就是把编译好的App上传到苹果的服务器,进行机器安全校验(不进行App Store的人工审核),如果校验通过就会在服务器端记录这个请求,并返回给你一个标记,你可以打进你的App里面(使用苹果的xcrun stapler
工具)。
这样macOS的Gatekeeper
在打开你的App时,如果有联网就会去服务器请求这个App是否通过了检查,否则默认阻止用户打开(需要右键打开)。
断网时就可以通过你staple进去的tag来识别。
那么这跟Sparkle有什么关系呢?前文提到Sparkle自带了几个App比如Autoupdate.app
,这些不在我们的主工程里做code signing但会带进我们的包里,所以传到notary服务器就会报错,notarization失败: Your Mac software was not notarized。
解决方案是针对这些自带的app做一次重签名:
post_install do |installer|
system("codesign --force -o runtime -s '<your-deveoploer-id-cert>' Pods/Sparkle/Sparkle.framework/Versions/A/Sparkle")
system("codesign --force -o runtime -s '<your-deveoploer-id-cert>' Pods/Sparkle/Sparkle.framework/Resources/Autoupdate.app/Contents/MacOS/Autoupdate")
system("codesign --force -o runtime -s '<your-deveoploer-id-cert>' Pods/Sparkle/Sparkle.framework/Resources/Autoupdate.app/Contents/MacOS/fileop")
end
3. Sandboxing
众所周知,Mac App Store要求上传的app全部都要打开App Sandbox,而Sparkle默认的实现无法对sandboxed app进行安装更新。如果你的app不上App Store那一切都好说,直接去掉就好了,但是如果App Store和自己的官网都要分发呢?
我们有两个做法:
- 针对App Store外的Build禁用Sandbox,引入Sparkle,App Store Build删掉Sparkle,开启Sandbox
- 采用Sparkle 2.x的Sandboxing做法
我还没有试过第二种方法,所以本文先看第一种方法,第二种以后试了再分享吧。
首先在Xcode中,我们在Project Settings里面设置两种Configurations,我这里设置的是Beta
和AppStore
。
因为我用xcconfig
文件来管理多个不同Schemes的宏变量,所以对应的Configuration Set
也要选好。关键是针对不同的Scheme增加标识宏:
// Beta的
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 BETA=1
接下来新建对应的两个Schemes,Archive的Build Configuration选择对应的Configuration,我这里分别是Beta
和AppStore
。
这样在代码中引用到Sparkle的地方,都是用宏包起来即可:
#if BETA || DEBUG
import Sparkle
#endif
这样就只有Beta
和Debug
对应的Build才有Sparkle Framework了。但是这样还是会导致Sparkle被打包进去,只是没被使用而已,所以我们还得修改Podfile
:
pod 'Sparkle', :configurations => ['Debug', 'Beta']
这样Sparkle就不会出现出现在App Store Build里了,完美。