Sparkle 프레임워크는 macOS 앱의 자동업데이트를 지원하는 가장 널리 사용되는 프레임워크 중 하나입니다.
우리의 놀라운 친구 flutter는 모바일 앱 뿐만아니라 데스크톱 앱까지 지원하는데요 이때 desktop 앱을 자동으로 업데이트 하는 방법에 대해 알아보겠습니다.
저희가 사용할 패키지는 auto_updater입니다.
https://pub.dev/packages/auto_updater
같은 패키지를 쓴다 하더라도 os가 다르므로 macos와 windows 둘다 각각 조금 다른 세팅이 필요합니다.
순서대로 auto_updater를 사용해 자동 업데이트를 구현하는 과정을 설명하도록 하겠습니다.
auto_updater 패키지 다운로드
main.dart에 앱을 시작하자마자 업데이트를 체크할 수 있도록 작성
Future<void> checkForUpdate() async {
try {
String feedURL = '${your_feed_url}';
await autoUpdater.setFeedURL(feedURL);
//sparkle 업데이트 상태 강제 초기화
//sparkle이 업데이트 이미 진행중이면 새로운 업데이트를 실행할 수 없다.
//업데이트 설치 후에도 sparkle 프로세스가 백그라운드에 남아있는경우가 있을 가능성이 있다
Process.runSync("/usr/bin/pkill", ["-f", "Autoupdate"]);
Process.runSync("/usr/bin/pkill", ["-f", "sparkle-cli"]);
if (!_isCheckingForUpdate) {
_isCheckingForUpdate = true;
await autoUpdater.checkForUpdates(inBackground: true);
_isCheckingForUpdate = false;
}
await autoUpdater.setScheduledCheckInterval(3600);
} catch (e) {
_isCheckingForUpdate = false;
}
}
참고:
Process.runSync("/usr/bin/pkill", ["-f", "Autoupdate"]);
Process.runSync("/usr/bin/pkill", ["-f", "sparkle-cli"]);
이 2줄의 코드는 “Error: -checkForUpdates called but .sessionInProgress == YES" 라는 오류가 자주 발생하여 강제로 sparkle 프로세스를 종료하고 새 업데이트 요청을 실행하기 위해 추가하였습니다. 만약 이 오류가 자주 발생하지 않는다면 무시해도 좋습니다.
//Terminal 명령어
dart run auto_updater:generate_keys
//info.plist에 추가
<key>SUPublicEDKey</key>
<string>${YourPrivateKey}</string>
//Info.plist에 추가해야할 전체 코드
<key>SUAutomaticallyUpdate</key>
<false/>
<key>SUAllowsAutomaticUpdates</key>
<true/>
<key>SUEnableAutomaticChecks</key>
<false/>
<key>SUEnableInstallerLauncherService</key>
<true/>
<key>SUPublicEDKey</key>
<string>${YourEDKey}</string>
저는 postman에서 Mock server를 만들어 appcast.xml을 응답받는 api를 만들어 사용했습니다. 실제 개발에서는 실서버를 구축하여 사용해야겠죠?
만약 저와 같이 mock server를 만들어서 한다면 필수사항!!!
appcast.xml을 받는 api의 header의 key는 Content-Type이고 value는 application/xml이어야 합니다.
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>App Updates</title>
<link>${mock_server_baseurl}/appcast.xml</link>
<description>You are ready to use New Version!</description>
<language>en</language>
<item>
<title>App 3.1.0</title>
<sparkle:version>289</sparkle:version>
<sparkle:shortVersionString>3.2.0</sparkle:shortVersionString>
<sparkle:criticalUpdate>true</sparkle:criticalUpdate> //이걸하면 remindme later과 같은 창이 뜨지 않는 일명 강제 업데이트가 가능하다 필수 사항은 아님
<description>Initial Release</description>
<pubDate>Fri, 26 Feb 2025 12:00:00 +0000</pubDate>
<enclosure url="${pkg_download_url}"
sparkle:edSignature="${your_ed_signature}" length="${your_length}"
sparkle:os="macos"
type="application/octet-stream"
/>
</item>
</channel>
</rss>
추출 명령어 ~~sing_update ${추출하여 저장하는 경로}
/sparkle/2.7.0/bin/sign_update /Users/Desktop/${App_name}.pkg(fastlane을 돌리고 output 결과 추출한 경로 대입)
<key>com.apple.security.app-sandbox</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>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.temporary-exception.files.absolute-path.read-write</key>
<array>
<string>/Applications/Luke.app</string>
</array>
위 코드의 역할 → Sparkle이 업데이트를 수행하는데 필요한 예외 권한 부여하는 역할입니다.
여기서 잠깐!

이렇게 하면 그림과 같이 Macos desktop App을 시작하자마자 새로운 버전이 있으니 업데이트 하라는 안내창을 볼 수 있습니다. install 버튼을 누르면 새로운 버전의 앱이 설치되고 기존 앱에 덮어 씌워져서 사용자는 새로운 버전의 앱을 사용할 수 있게됩니다.
Windows는 패키징 부터 라이브러리를 사용해서 msix로 패키징 해주어야합니다. Msix는 Microsoft store가 배포시 권장하는 파일 확장자 형식입니다. 다른 걸로 해도 무관하겠지만 저는 권장사항에 맞게 msix로 진행했습니다.
$cert = New-SelfSignedCertificate -DnsName www.yourwebsite.com -Type CodeSigning -CertStoreLocation Cert:\CurrentUser\My
$CertPassword = ConvertTo-SecureString -String "my_passowrd" -Force -AsPlainText
Export-PfxCertificate -Cert "cert:\CurrentUser\My\$($cert.Thumbprint)" -FilePath "d:\selfsigncert.pfx" -Password $CertPassword
msix로 패키징하기 위해서는 앱에 적용할 인증서가 필요합니다. 일단 저는 개인 인증서를 생성해서 앱에 적용해주었습니다.
msix_config:
display_name: ${your_display_name}
publisher_display_name: ${your_publisher_name} //증명서의 publisher과 동일해야한다
identity_name: ${your_identity_name}
msix_version: 1.0.0.0 //업데이트 하고자 하는 파일은 버전을 이전보다 높아져야 덮어씌워진다
certificate_path: "${your_certificate_route}"
certificate_password: "${certificate_password}"
sign_msix: true
이때 증명서의 publisher과 identity name이 앱에 적용한 증명서와 동일해야 새로운 버전의 앱이 덮어 씌워질 수 있습니다. (이거 때문에 중복 설치되는 과정을 겪고 얻어낸 결과입니다..)
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>App Updates</title>
<link>${your_base_url}/appcast.xml</link>
<description>you are ready to use new version</description>
<language>en</language>
<item>
<title>App 3.2.0</title>
<pubDate>Fri, 26 Feb 2025 12:00:00 +0000</pubDate>
<enclosure url="${your_download_url}"
sparkle:dsaSignature="${your_dsaSignature}"
sparkle:version="3.2.0+289"
sparkle:installerArguments="/quiet /norestart"
sparkle:os="windows"
length="0"
type="application/octet-stream" />
</item>
</channel>
</rss>
openssl dsaparam -out dsaparam.pem 2048
openssl gendsa -out dsa_private.pem dsaparam.pem
openssl dsa -in dsa_private.pem -pubout -out dsa_public.pem
Get-Content update_signature.b64
//이때 출력값을 dsaSignature값으로 넣는다

이렇게 하면 그림과 같이 Windows desktop App을 시작하자마자 새로운 버전이 있으니 업데이트 하라는 안내창을 볼 수 있습니다. install 버튼을 누르면 새로운 버전의 앱이 설치되고 기존 앱에 덮어 씌워져서 사용자는 새로운 버전의 앱을 사용할 수 있게됩니다.
출처 - auto_updater 공식문서
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>auto_updater_example</title>
<description>Most recent updates to auto_updater_example</description>
<language>en</language>
<item>
<title>Version 1.1.0</title>
<!-- For the macOS item, it is recommended to add 'sparkle:version' and 'sparkle:shortVersionString' to the item node, rather than as part of the enclosure. -->
<sparkle:version>2</sparkle:version>
<sparkle:shortVersionString>1.1.0</sparkle:shortVersionString>
<sparkle:releaseNotesLink>
https://your_domain/your_path/release_notes.html
</sparkle:releaseNotesLink>
<pubDate>Sun, 16 Feb 2022 12:00:00 +0800</pubDate>
<enclosure url="1.1.0+2/auto_updater_example-1.1.0+2-macos.zip"
sparkle:edSignature="pbdyPt92pnPkzLfQ7BhS9hbjcV9/ndkzSIlWjFQIUMcaCNbAFO2fzl0tISMNJApG2POTkZY0/kJQ2yZYOSVgAA=="
sparkle:os="macos"
length="13400992"
type="application/octet-stream" />
</item>
<item>
<title>Version 1.1.0</title>
<sparkle:releaseNotesLink>
https://your_domain/your_path/release_notes.html
</sparkle:releaseNotesLink>
<pubDate>Sun, 16 Feb 2022 12:00:00 +0800</pubDate>
<enclosure url="1.1.0+2/auto_updater_example-1.1.0+2-windows.exe"
sparkle:dsaSignature="MEUCIQCVbVzVID7H3aUzAY5znpi+ySZKznkukV8whlMFzKh66AIgREUGOmvavlcg6hwAwkb2o4IqVE/D56ipIBshIqCH8rk="
sparkle:version="1.1.0+2"
sparkle:os="windows"
length="0"
type="application/octet-stream" />
</item>
</channel>
</rss>