##Sparkle is...
配布したプログラムのアップデートは大きく使い勝手に影響しない限り,なかなかやってもらえないものです。
MacAppStoreに公開するならば,アップデートはそちらにおまかせすることもできます。
AppStoreに公開せず,自サイトで配布する,企業内で配布するようなアプリケーションには,自前でアップデータを実装しなくてはなりません。
ここを楽したいときはSparkle
というフレームワークを使います。
証明書の作成やアップデート情報配信用フィードの作成は,こちらの記事を参考にしてください。
MacApp用アップデート管理システムSparkleの使い方 by econa77
この記事について,
- OSX 10.11以降をターゲットにする場合はHTTPS通信必須
- length はアーカイブのサイズ (
wc -c < archive.zip
)
という2点を補足しておきます。特に社内サーバに置いて更新を見に行かせるなどを考えている場合は注意してください。
##用意するもの
- Xamarin.Mac 2.0系
- Objective Sharpie
- めげない気持ち
この記事で使ったコードはすべてgithubにあります。
SparkleSample
##Native binding
###Sparkleをビルドする
CocoaPods経由でもできるはずですが,ビルドがどうも通らないためクローンして作っていくことにします。証明書を作る段階でリポジトリにあるシェルスクリプトが必要になるので,こちらの方が手っ取り早いかもしれません。
Sparkleをクローンして,そのディレクトリに移動します。
まずはmake release
です。一時フォルダにSparkle-[version].tar.bz2
が作られ,Finderに表示されます。このアーカイブを適当な場所に展開し,展開先でターミナルを開きます。
###Objective Sharpie
Objective Sharpieをインストールすると,/usr/local/bin/sharpie
が使えるようになります。これでまずはバインディング情報を生成します。自分でP/Invoke宣言書いてると死ぬので。
% sharpie bind --output=Sparkle.XamMac --namespace=Sparkle --sdk=macosx10.11 ./Sparkle.framework/Headers/*.h
--sdk
に指定できるSDKは,sharpie xcode -sdks
で取得できます。手元ではこんな感じ。
% sharpie xcode -sdks
sdk: appletvos9.0 arch: arm64
sdk: iphoneos9.1 arch: arm64 armv7
sdk: macosx10.11 arch: x86_64 i386
sdk: watchos2.0 arch: armv7
成功すると,Sparkle.XamMac ディレクトリ以下にApiDefinitions.cs
とStructsAndEnums.cs
が生成されます。
これをつかってバインディングライブラリを作っていくわけですが・・・ もちろんこのままでは使えません。
###ApiDefinitions.cs の修正
sharpie
コマンドはBinding Analysisレポートで,自動ではいまいち拾いきれなかった場所を明示してくれます。
ApiDefinitions.cs
で[Verify]
属性がついているものは,すべて修正が必要です。Sparkleの場合は属性を削除するだけで大丈夫です。後述のbmac
でエラーになった箇所を直していく形になります。
それなりに罠が潜んでいるので,githubに置いてあるApiDefinitions.cs
を参考になさってください。
###バインディングライブラリのビルド
bmac
を使ってライブラリをビルドしていきます。こんな感じのMakefileでいけます。
BUILD_FLAGS=-unsafe -target:library -nowarn:436 -nowarn:219
LIBRARY=./Sparkle.framework/Versions/A/
BMAC=/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/bin/
BASE=/Library/Frameworks/Xamarin.Mac.framework/Versions/Current/lib/mono/Xamarin.Mac/
BINDING=./Sparkle.XamMac/
all: prepare osx
prepare:
cp -R ./Sparkle.framework $(BINDING)
osx:
$(BMAC)bmac --ns=Sparkle -o $(BINDING)Sparkle.dll \
--baselib=$(BASE)Xamarin.Mac.dll --api=$(BINDING)ApiDefinitions.cs \
-s=$(BINDING)StructsAndEnums.cs \
--compiler=mcs --unified-mobile-profile
bmac
がビルドを行っている部分です。
ここでビルドが失敗した場合は,ApiDefintions.cs
の記述に誤りがありますので直していきます。頻出エラーとその対策を少しだけ。
- CS0246 参照解決できない。自動生成された型名を見直す。g.csでエラーが出た場合は,BaseType指定が足りない。[BaseType(typeof(NSObject))]などをつける。
- CS0111 同じ引数を持つメソッドがある。名前を適当に見直す。
- CS0579 StaticAttributeは一回しか使えない。たいていは消してよい。
もう後は Build&Pray です。
うまくできるとこんな感じ。
##ライブラリを使う
まずはXamarin.Macプロジェクトを作っていきます。今回はStoryboardを使いたくないので,ターゲットをOSX 10.9に設定しました。
プロジェクトのビルド後イベントを追加します。実行ファイルと同じディレクトリに,ネイティブのSparkle.framework
をコピーします。
ウィンドウやローカライズといったリソースがない場合はダイナミックライブラリだけコピーしても構いません。Sparkleの場合はリソースファイルが必要なので,全てコピーします。
Native References
というのもありますが,(現時点では)これは 罠 です。特に何も起きませんので,使わなくていいです。
まずはこのライブラリが読み込めるか確かめましょう。Main.cs
を編集します。
using AppKit;
using System;
using System.IO;
using System.Reflection;
using ObjCRuntime;
namespace SparkleSample
{
static class MainClass
{
static string GetCurrentExecutingDirectory()
{
string filePath = new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath;
return Path.GetDirectoryName(filePath);
}
static void Main(string[] args)
{
if (Dlfcn.dlopen(Path.Combine(GetCurrentExecutingDirectory(), "Sparkle.framework", "Sparkle"), 0) == IntPtr.Zero)
{
Console.Error.WriteLine("Unable to load the dynamic library.");
Environment.Exit(1);
}
NSApplication.Init();
NSApplication.Main(args);
}
}
}
Dlfcn.dlopen
は,必ずNSApplication.Init()
前に呼びます。
ここまでできたら,デバッグ実行してウィンドウが表示されることを確認します。これでライブラリを使うことができるようになりました。
###Sparkleで更新を確認
前掲した記事を参考に,証明書ファイルとフィードURLの設定をInfo.plistに追加します。
で,MainWindowControllerに適当に書きます。
using System;
using Foundation;
using AppKit;
using Sparkle;
using System.Globalization;
namespace SparkleSample
{
public partial class MainWindowController : NSWindowController
{
public MainWindowController(IntPtr handle)
: base(handle)
{
}
[Export("initWithCoder:")]
public MainWindowController(NSCoder coder)
: base(coder)
{
}
public MainWindowController()
: base("MainWindow")
{
}
private static DateTime nsRef = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
public override void AwakeFromNib()
{
base.AwakeFromNib();
updater = new SUUpdater();
updater.AutomaticallyChecksForUpdates = false;
UpdateDateLabel();
}
public new MainWindow Window
{
get { return (MainWindow)base.Window; }
}
SUUpdater updater;
partial void CheckUpdates(NSObject sender)
{
updater.CheckForUpdates(new NSObject());
UpdateDateLabel();
}
void UpdateDateLabel()
{
var lastUpdated = nsRef.AddSeconds(updater.LastUpdateCheckDate?.SecondsSinceReferenceDate ?? 0);
DateLabel.StringValue = TimeZoneInfo.ConvertTimeFromUtc(lastUpdated, TimeZoneInfo.Local).ToString("u");
}
}
}
ふんふん。Check updatesしましょう?
よくできました。
##Conclusion
自分で作成したライブラリもこの方法でかなり楽にバインディングできます。
特にまだXamarinバインディングのないAPIについて,ラッパーライブラリを書いてネイティブ参照で使う,というのはよくやる手だと思います。
というわけでMacの人から贈る,Advent Calender 3日目でした。