MacApp用アップデート管理システムSparkleの使い方（Xamarin.Mac編）

##Sparkle is...
配布したプログラムのアップデートは大きく使い勝手に影響しない限り，なかなかやってもらえないものです。
MacAppStoreに公開するならば，アップデートはそちらにおまかせすることもできます。
AppStoreに公開せず，自サイトで配布する，企業内で配布するようなアプリケーションには，自前でアップデータを実装しなくてはなりません。
ここを楽したいときは`Sparkle`というフレームワークを使います。

証明書の作成やアップデート情報配信用フィードの作成は，こちらの記事を参考にしてください。
[MacApp用アップデート管理システムSparkleの使い方](http://qiita.com/econa77/items/20105ab32740f669f43e) by econa77

この記事について，

* OSX 10.11以降をターゲットにする場合はHTTPS通信必須
* length はアーカイブのサイズ (`wc -c < archive.zip`)

という2点を補足しておきます。特に社内サーバに置いて更新を見に行かせるなどを考えている場合は注意してください。

##用意するもの

* Xamarin.Mac 2.0系
* [Objective Sharpie](https://developer.xamarin.com/guides/ios/advanced_topics/binding_objective-c/objective_sharpie/)
* めげない気持ち

この記事で使ったコードはすべてgithubにあります。
[SparkleSample](https://github.com/ailen0ada/SparkleSample)

##Native binding
###Sparkleをビルドする
CocoaPods経由でもできるはずですが，ビルドがどうも通らないためクローンして作っていくことにします。証明書を作る段階でリポジトリにあるシェルスクリプトが必要になるので，こちらの方が手っ取り早いかもしれません。
[Sparkle](https://github.com/sparkle-project/Sparkle)をクローンして，そのディレクトリに移動します。
まずは`make release`です。一時フォルダに`Sparkle-[version].tar.bz2`が作られ，Finderに表示されます。このアーカイブを適当な場所に展開し，展開先でターミナルを開きます。

###Objective Sharpie
Objective Sharpieをインストールすると，`/usr/local/bin/sharpie`が使えるようになります。これでまずはバインディング情報を生成します。自分でP/Invoke宣言書いてると死ぬので。

```bash
% sharpie bind --output=Sparkle.XamMac --namespace=Sparkle --sdk=macosx10.11 ./Sparkle.framework/Headers/*.h
```

`--sdk`に指定できるSDKは，`sharpie xcode -sdks`で取得できます。手元ではこんな感じ。

```bash
% 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でいけます。

```bash
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** です。
うまくできるとこんな感じ。
![RevealFinder](https://qiita-image-store.s3.amazonaws.com/0/34563/b4d29101-e4ff-83ef-70ec-a1cfa80c6cb9.png)


##ライブラリを使う
まずはXamarin.Macプロジェクトを作っていきます。今回はStoryboardを使いたくないので，ターゲットをOSX 10.9に設定しました。
![CreateProject](https://qiita-image-store.s3.amazonaws.com/0/34563/d89d7d02-4880-9f4b-ecd4-8865b596d074.png)

参照に作った`Sparkle.dll`を追加します。
![AddSparkleDll](https://qiita-image-store.s3.amazonaws.com/0/34563/8f74c33b-f336-1791-a3e5-609a21262260.png)

プロジェクトのビルド後イベントを追加します。実行ファイルと同じディレクトリに，ネイティブの`Sparkle.framework`をコピーします。
ウィンドウやローカライズといったリソースがない場合はダイナミックライブラリだけコピーしても構いません。Sparkleの場合はリソースファイルが必要なので，全てコピーします。
![PostBuildEvent](https://qiita-image-store.s3.amazonaws.com/0/34563/46f057bf-90cd-c71d-1d4a-1e5f4cf2764e.png)

`Native References`というのもありますが，（現時点では）これは **罠** です。特に何も起きませんので，使わなくていいです。
![NativeReferences](https://qiita-image-store.s3.amazonaws.com/0/34563/6780199e-4e04-7929-31e4-b2bd93e97775.png)

まずはこのライブラリが読み込めるか確かめましょう。`Main.cs`を編集します。

```csharp: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に追加します。
![Info.plist](https://qiita-image-store.s3.amazonaws.com/0/34563/cac6a181-0316-359e-12ee-fdaa4d2a65de.png)

適当にコントロールを貼っておきます。
![Controls](https://qiita-image-store.s3.amazonaws.com/0/34563/0c346d53-b6de-003e-8349-3049dbcfa84a.png)

で，MainWindowControllerに適当に書きます。

```csharp:MainWindowController.cs
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");
        }
    }
}
```

デバッグビルドで実行した場合は署名がないので警告が。
![InsecureError](https://qiita-image-store.s3.amazonaws.com/0/34563/ff736101-67d8-3844-ada9-a83e1bd9190a.png)

ふんふん。Check updatesしましょう？
![SoftwareUpdate](https://qiita-image-store.s3.amazonaws.com/0/34563/0f931d24-a58f-a3e3-8ca2-80a43ed48732.png)
よくできました。

##Conclusion
自分で作成したライブラリもこの方法でかなり楽にバインディングできます。
特にまだXamarinバインディングのないAPIについて，ラッパーライブラリを書いてネイティブ参照で使う，というのはよくやる手だと思います。

というわけでMacの人から贈る，Advent Calender 3日目でした。