ホームページ開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareで自動アップデート機能を試す(Sparkle編)

 Xojo / Real Studio Trial and Error

CocoaのDeclareで自動アップデート機能を試す(Sparkle編)

目次
 はじめに

 以下は、Xojo Cocoaビルドについての話題です。

 アプリケーションの自動アップデート機能である、Sparkleについて、調べてみました。

 なお検証には、Xojo 2021 Release 2.1を用いています。(Mac mini 2018 + macOS 12.2.1 Monterey)


 経緯

 Mac向けの自動アップデート機能としては、Sparkleがよく知られていますが、Xojoにそのまま適用できるのかは何とも言えない状況でした。

 参考サイト(1):Sparkle: open source software update framework for macOS

 Sparkle自体については、多くの情報がアップされているので、それらを参考にさせて頂きながら、まずはダウンロードして実態を調べてみることにしました。
 結果、Sparkleの機能はFrameworkにまとめられていて、Xcodeではプロジェクトに追加するだけでコード記述は不要、となっていることが確認できました。

 となれば、XojoのDeclareがFrameworkを扱える(とリファレンスには書いてある)ことは既知なので、なんとかなりそうです。
 まずはdylibと同じ使い方で試したところ、うまくいきませんでした。
 そこで調べたら、以下がヒットしました。

 参考サイト(2):Loading 3rd Party Frameworks in Xojo iOS – Xojo Programming Blog

 ここから、Frameworkはdylibと異なり、dlopen()という一手間が掛かる、ということを知りました。
 これさえ押さえておけば、以降はシステムが提供するFrameworkと同等に扱える、ということのようです。
(注:記事ではデバッグビルド限定になっていますが、その理由はよく分かりません(iOSだから?)。以下の実装ではデバッグ/リリース共通にしましたが、可否は不明。)

 Frameworkの使い方については解決しましたので、次はSparkleの使い方です。
 上述の通り、標準的にはInterface Builder(xib)を使うので、コードでの記述の仕方は(例によって)見当たらず、結局、公式サイトで公開されているAPIリファレンスを参考に、以下の手順でいけそうな感触を得ました。(注:以下は2.xの場合。1.xとは使用するクラスが異なる)
 1. SPUStandardUpdaterControllerのインスタンスを生成し、スタート
 2. 自動チェックオプションがオンで、前回確認から、指定された時間が経過していたら、
  2.1. バックグラウンドでアップデートオプションがオンなら、バックグラウンドでのアップデート処理を実行
  2.2. オフなら、アップデート確認(ダイアログを表示)を実行
 3. オプションに関わらず、メニューが選択されたらアップデート確認(ダイアログ表示)を実行

 参考サイト(3):SPUStandardUpdaterController Class Reference

 当初はインスタンス生成だけすればいいのかと思っていたのですが、そうはいきませんでした。
 なので、自分で実装しましたが、この方法でいいのかは定かではありません。
 なお、アップデート処理(ダウンロードとインストール)自体は、Xcode同様、Sparkleが全てやってくれます。

 あとこれは、Sparkleとは直接関係ありませんが、参考サイト(2)には(自分にとって)もう一つ有益な情報がありました。
 それは、ビルドステップのCopy Filesを使うと、ビルド後に手動でファイルをコピーする必要がなくなる、という点です。
 早速、以下の実装フェーズでも使わせて頂きました。

 さらにもう一点。
 自動アップデート機能にはもう一つ、Xojoに特化したKaju(次回レポート予定)があるのですが、そこで使われていたのが、info.plistファイルをカスタマイズする、というものです。(これも知らんかった…)
 こちらも、以下の実装フェーズで使わせて頂きました。

 参考サイト(4):GitHub - ktekinay/Kaju: Xojo code for implementing self-updating apps

 さて、Sparkleを実装したアプリの作成手順は、さまざまなサイトで紹介されていますが、(Xojo固有の手続き含めて)ここで改めて整理をしておきます。
  1. 対象となるアプリのプロジェクトに、Sparkle.frameworkをセット(手順は、Xojoでの実装を参照)
  2. Window1にメソッド、プロパティ、メニューハンドラーを追加(必要なら、環境設定ウィンドウもカスタマイズ)
  3. Sparkle付属のgenerate_keysを起動し、キーをメモ。
  4. 対象となるアプリのプロジェクトに、(上述のキーを含む)追加分のInfo.plistファイル(詳細は、Xojoでの実装を参照)をドラッグ&ドロップ
    (ここまでは仕込みの時に一回だけ)

  5. 対象アプリにバージョンをセット(Build Settings>Sharedで、Versionが付くもの全て)
  6. 対象アプリをビルドし、zip圧縮(圧縮後、ファイル名をバージョンを含んだものに変更し、アプリごとに纏めておく)
  7. Sparkle付属のgenerate_appcastを起動し、appcast.xmlファイル書き出し
  8. zipappcast.xmlをサーバーにアップロード(別途FTPソフトで実行)

  9. 対象アプリをアップデートしたら、5〜8を繰り返す
 以上を踏まえ、テストアプリを作ってみることにします。仕様は以下の通りとしました。

 Xojoでの実装
【ソースコードのコピー&ペーストについて】
・ソースコード(グレー背景部分の全文)をコピーし、指定のオブジェクトにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
・ペーストはオブジェクトに行って下さい。オブジェクト内のEvent Handlers/Methods/Properties等にペーストしても、うまくいかない場合があります。
・それでもペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. Xojoで新規プロジェクトを作成
  2. MainMenuBarの、任意のメニューに、メニュー項目(Name:ApplicationUpdate、Super:ApplicationMenuItem、Text:アップデートを確認...)を追加
  3. Window1に、CheckBox(Name:CheckBox1, Caption:自動的にアップデートを確認する)を追加
  4. 以下をCheckBox1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      // SPUStandardUpdaterControllerからSPUUpdaterを取得
      Declare Function updater Lib "Foundation" Selector "updater" (receiver As Ptr) As Ptr
      Dim updt As Ptr = updater(upController)
      
      // 自動チェックオプションをセット
      Declare Sub setAutomaticallyChecksForUpdates Lib "Foundation" Selector "setAutomaticallyChecksForUpdates:" (receiver As Ptr, flg As Boolean)
      setAutomaticallyChecksForUpdates(updt, me.Value)
    End Sub
    
  5. 以下をWindow1にペースト
    Sub Open() Handles Open
      // 3rd Party製Frameworkを使えるようにする
      Declare Function dlopen Lib "/usr/lib/libSystem.dylib" (name As CString, flags As Int32) As Ptr
      Call dlopen("@executable_path/../Frameworks/Sparkle.framework/Sparkle", 1 Or 8)
      
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      // SPUStandardUpdaterControllerのインスタンス生成
      upController = NSClassFromString("SPUStandardUpdaterController")
      Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr
      upController = alloc(upController)
      Declare Function initWithUpdaterDelegate Lib "Cocoa" Selector "initWithUpdaterDelegate:userDriverDelegate:" (receiver As Ptr, deleupdate As Ptr, deledriv As Ptr) As Ptr
      upController = initWithUpdaterDelegate(upController, nil, nil)  // Updaterは自動的に開始
      
      // SPUStandardUpdaterControllerからSPUUpdaterを取得
      Declare Function updater Lib "Cocoa" Selector "updater" (receiver As Ptr) As Ptr
      Dim updt As Ptr = updater(upController)
      
      // 自動チェックオプションを取得
      Declare Function automaticallyChecksForUpdates Lib "Cocoa" Selector "automaticallyChecksForUpdates" (receiver As Ptr) As Boolean
      Dim flg As Boolean = automaticallyChecksForUpdates(updt)
      CheckBox1.Value=flg  // チェックボックスにセット
      
      if flg then  // 自動チェックオプションがオンなら
        if DateCompare() then  // チェック間隔を過ぎていたら
          
          // バックグラウンドでアップデートオプションを取得
          Declare Function automaticallyDownloadsUpdates Lib "Cocoa" Selector "automaticallyDownloadsUpdates" (receiver As Ptr) As Boolean
          Dim flg2 As Boolean = automaticallyDownloadsUpdates(updt)
          if flg2 then  // オプションがオンなら
            // ダイアログを表示せず、バックグラウンドでアップデート
            Declare Sub checkForUpdatesInBackground Lib "Cocoa" Selector "checkForUpdatesInBackground" (receiver As Ptr)
            checkForUpdatesInBackground(updt)
          else
            // アップデートを確認(ダイアログを表示)
            Declare Sub checkForUpdates Lib "Cocoa" Selector "checkForUpdates" (receiver As Ptr)
            checkForUpdates(updt)
          end if
          
        end if
      end if
    End Sub
    
  6. 以下をWindow1にペースト(メニューハンドラー)
    Function ApplicationUpdate() As Boolean
      // アップデートを確認
      Declare Sub checkForUpdates Lib "Foundation" Selector "checkForUpdates:" (receiver As Ptr, sender As Integer)
      checkForUpdates(upController, ApplicationUpdate.Handle(MenuItem.HandleType.CocoaNSMenuItem))
      
      Return True
    End Function
    
  7. 以下をWindow1にペースト
    Protected Function DateCompare() As Boolean
      // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。
      Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
      
      // SPUStandardUpdaterControllerからSPUUpdaterを取得
      Declare Function updater Lib "Cocoa" Selector "updater" (receiver As Ptr) As Ptr
      Dim updt As Ptr = updater(upController)
      
      // 前回アップデートを確認したdateを取得
      Declare Function lastUpdateCheckDate Lib "Cocoa" Selector "lastUpdateCheckDate" (receiver As Ptr) As Ptr
      Dim date0 As Ptr = lastUpdateCheckDate(updt)
      if date0=nil then  // 時刻が未設定ならfalseを返す
        return false
      end if
      
      // 自動チェック時間間隔オプションを取得
      Declare Function updateCheckInterval Lib "Cocoa" Selector "updateCheckInterval" (receiver As Ptr) As Double
      Dim vv As Double = updateCheckInterval(updt)
      
      // 現在のdateを取得
      Dim date1 As Ptr = NSClassFromString("NSDate")
      Declare Function date Lib "Cocoa" Selector "date" (receiver As Ptr) As Ptr
      date1 = date(date1)
      
      // 現在時刻が次回確認時刻(前回アップデートを確認したdate+時間間隔)を過ぎていたらtrueを返す
      Declare Function compare Lib "Cocoa" Selector "compare:" (receiver As Ptr, time As Ptr) As Integer
      Dim result As Integer = compare(date1, date0)
      if result=1 then
        return true
      else
        return false
      end if
    End Function
    
  8. 以下をWindow1にペースト(できなければPropertyに、Name:upController、Type:Ptr、を追加)
    Protected Property upController As Ptr
    
 コードの実装は以上です。引き続き、ファイル関連の処理を実施。
  1. 以下の内容でテキストファイルを作成し、名前をInfo.plistとして保存
    (<サーバーのURL><generate_keysが出力したキー>は各自の環境に合わせてセットして下さい。)
    <?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>NSAppTransportSecurity</key>
    	<dict>
    		<key>NSAllowsArbitraryLoads</key>
    		<true/>
    	</dict>
    	<key>SUFeedURL</key>
    	<string><サーバーのURL>/appcast.xml</string>
    	<key>SUPublicEDKey</key>
    	<string><generate_keysが出力したキー></string>
    </dict>
    </plist>
    
  2. プロジェクトに、Info.plistをドラッグ&ドロップ

  3. プロジェクト左ペインのBuild Settings>macOS上で右クリックし、Add to "Build Settings">Build Step>Copy Filesを選択
  4. InspectorのDestinationにFramework Folderを指定。
  5. 中央ペインにSparkle.frameworkをドラッグ&ドロップ
 ビルドしてサーバーにアップし、バージョンを上げて再度ビルドして実行してみたところ、自動アップデートが機能することを確認しました。
S Shot1
注:リリースノートは記述していないので、当該フィールドは非表示になっている。

 おわりに

 アップロードしたアプリは、codesign(Authorityを含む)/Notarizeしなくても起動してしまいます。
 公開されているアプリでも自動アップデート時には警告メッセージは表示されないので、標準的な手続きなのかもしれませんが。
(codesignとEdDSA signaturesの関係がイマイチよく分かっていないのですが、EdDSA signaturesがcodesignを回避している、という理解で良いのでしょうか?)

 サーバーはhttpでも接続できてしまっています。https必須はXcodeの場合だけ?かもしれませんが、良否は不明です。

 あと、パッケージの形態はzipにしましたが、新規ユーザー向けにはdmgの方が使い勝手がいいので、そうなると二本立てになって煩雑さが増してしまいます。
 Sparkleのドキュメントによると、dmgでも(/Applicationsにsymlinkしておけ、とかあるので)いけそうですが、テストはしていません。
 dmgでよければパッケージの管理は楽になりますが、いずれにしても、ヘルパーアプリを自作する等して、管理/処理の簡素化を図った方がよさそうです。

 なお、経緯のところでも述べましたが、XojoではXcodeのようにお任せでは適切に処理されない?ことも考えられるので、(今回は実験目的なので、深入りはしませんでしたが、)利用する場合は十分テストしておく必要があるかと思われます。


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):Sparkle: open source software update framework for macOS
 参考サイト(2):Loading 3rd Party Frameworks in Xojo iOS – Xojo Programming Blog
 参考サイト(3):SPUStandardUpdaterController Class Reference
 参考サイト(4):GitHub - ktekinay/Kaju: Xojo code for implementing self-updating apps


 更新履歴

 2022.03.08 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]