Electron: autoUpdaterでアプリを自動アップデートさせる方法 (Mac/Windows)

## TL; DR

* Electronの`autoUpdater`モジュールを使うとMac/Windowsでアプリを自動アップデートさせられる
* Mac/Windowsそれぞれ違うアップデートのリリース方法が必要
	* Mac: アップデート情報を返すWeb APIを用意する
	* Windows: grunt-electron-installer で作ったファイルをS3とかにアップロードする
* あとは`autoUpdater`のAPIを呼び出すだけ

## はじめに

Electronには `autoUpdater`という、アプリの自動アップデート用モジュールが入っている。これを使うとMac/Windowsでアプリを自動アップデートさせることができる。

Wantedlyが開発しているメッセージングアプリの[Sync](https://www.wantedly.com/sync)デスクトップ版では、実際に`autoUpdater`を使っているが、Mac/Windowsそれぞれで違うセットアップが必要であったりして意外と使うには手間がかかった。

そこで、Electronの`autoUpdater`を使ってMac/Windows上で実際に自動アップデートを行う方法がどうなるかをまとめてみた。

## Mac

Macの`autoUpdater`には、内部的に[Squirrel.Mac](https://github.com/Squirrel/Squirrel.Mac)が使われている。

### アプリを署名する

アプリを Developer ID certificate (iOS開発などでApple Developer Program に登録していれば取得できる) で署名する必要がある。
[electron-packager](https://github.com/maxogden/electron-packager)のオプションでも対応している。
署名しないとアップデートのインストールに失敗する。


### アプリ (`.app`) をzip圧縮する

`.app` を直接zip圧縮する。

```例
zip -r -y myapp-1.0.0.zip myapp.app
```

### Web APIを用意する

https://github.com/Squirrel/Squirrel.Mac#server-support

Squirrel.Macがアップデートをチェックするのに使うWeb APIを用意する。
アップデートがある場合、レスポンスとして、zipファイルのダウンロードURLとメタデータを返す。


##### リクエスト例

```
GET releases.myapp.com/latest?version=1.0.0&platform=darwin
```

パラメータ等はSquirrel.Macが付加するわけではないので自由 (アプリのバージョンなどを元にする)。


##### レスポンス例 (アップデートあり)

200 OK

```json:レスポンス
{
  "url": "https://url/to/myapp-1.0.1.zip",
  "name": "my release",
  "notes": "my notes",
  "pub_date": "2015-07-01T04:46:15.898Z"
}
```

##### レスポンス例 (アップデートなし)

204 No Content

### `autoUpdater` 設定

アップデートチェック用APIのURLを`setFeedURL`で設定する。

```js
const {autoUpdater} = require("electron");
autoUpdater.setFeedURL("https://releases.myapp.com/latest?version=1.0.0&platform=darwin");
```

## Windows


Windows上での`autoUpdater`では [Squirrel.Windows](https://github.com/Squirrel/Squirrel.Windows) が使われているので、それに対応するようアプリをリリースする。


### [grunt-electron-installer](https://github.com/atom/grunt-electron-installer) でインストーラとNuGetパッケージ用ファイルをつくる

Squirrel.Windows ではアップロードを確認・ダウンロードするのにNuGetパッケージを使っている。
自前でNuGetパッケージを用意することも出来ると思われるが、 grunt-electron-installer を使うと NuGetパッケージに必要なファイルを自動生成してくれる。
さらに、Atomなどのインストーラと似たようなシンプルでかっこいいインストーラも生成してくれるので便利。

gruntのタスクとして提供されているが、[gulpなどから使うことも可能](http://qiita.com/seanchas_t/items/66ebc90a9f7d86017bb0)。

この際、(特にproductionでは) Authenticode でコード署名をしておく必要がある。
コード署名をしていないと、アプリがマルウェアに誤判定される・ダウンロードしたインストーラを実行するときに怖い警告が表示される、など[様々な面倒なことが起こる](https://github.com/atom/grunt-electron-installer#sign-your-installer-or-else-bad-things-will-happen)。


#### 設定例

```js
'create-windows-installer': {
  ia32: {
    appDirectory: '/tmp/build/myapp', // electron-packagerなどでパッケージしたアプリのディレクトリ
    outputDirectory: '/tmp/build/installer',
    authors: 'My App Inc.',
    exe: 'myapp.exe'
  }
}
```

##### 生成されるファイル例

`RELEASES`: リリース一覧のテキストファイル
`Setup.exe`: インストーラ
`myapp-1.0.0-full.nupkg`: アップデートのnupkg


### アップロード

生成された`RELEASES` / `*.nupkg` ファイルをそのまま (S3などに) アップロードすることで、NuGetパッケージをホストすることになる(らしい)。
なので、必ずしもアップデート用にサーバを用意する必要はない。

### `autoUpdater` 設定

`RELEASES`と`*.nupkg`ファイルがあるディレクトリを`setFeedURL`で設定する。

```js
const {autoUpdater} = require("electron");
autoUpdater.setFeedURL("https://releases.myapp.com/windows");
```

## `autoUpdater` を使う (共通)

リリースしたアップデートを実際にアプリ側で適用するには、`autoUpdater` モジュールからアップデート処理を呼び出す必要がある。

### `checkForUpdates()`

`autoUpdater.checkForUpdates()` を呼び出すとアップデートがチェックされる。アップデートが見つかった場合 **即座に** ダウンロードが始まる。

### `quitAndInstall()`

アップデートがあった時、`autoUpdater.quitAndInstall()` を呼ぶと即座にアップデートをインストールして再起動できる。 (呼び出さなくてもアプリの次回の起動時に自動的にインストールされる)

### イベント

アップデートのチェック結果ごとに `"update-available"`、`"update-not-available"`、`"error"` イベントが来る。
アップデートのダウンロードが終わると`"update-downloaded"`イベントが来る。

### 例


```js
const {autoUpdater} = require("electron");
autoUpdater.setFeedURL(<先ほどのURL>);
autoUpdater.checkForUpdates();

autoUpdater.on("update-downloaded", () => {
  index = dialog.showMessageBox({
    message: "アップデートあり",
    detail: "再起動してインストールできます。",
    buttons: ["再起動", "後で"]
  });
  if (index === 0) {
    autoUpdater.quitAndInstall();
  }
});
autoUpdater.on("update-not-available", () => {
  dialog.showMessageBox({
    message: "アップデートはありません",
    buttons: ["OK"]
  });
});
autoUpdater.on("error", () => {
  dialog.showMessageBox({
    message: "アップデートエラーが起きました",
    buttons: ["OK"]
  });
});
```

## まとめ

プラットフォームごとにセットアップをすると、`autoUpdater`を使ってクロスプラットフォームなアプリの自動アップデートを実現できる。
正直面倒な手順なので、将来的に改善されるべきだとは思う(特に、プラットフォームごとにやり方が違うのは良くない)。
一旦セットアップしてしまえば、簡単にユーザに最新のアプリを使ってもらうことができるので便利。