Winsparkleを使用してWindowsアプリケーションに自動更新機能を追加する

デスクトップアプリを開発していると機能追加やバグ修正の度にユーザーにアップデートを通知してインストーラーを配布、インストールしてもらうというのが非常に手間に感じてしまいます。
開発者側もおっくうに感じるし、ユーザー側にとっても頻繁にマニュアル更新を行うのは面倒に思うことでしょう。

ということで本記事では自作のデスクトップソフトウェアに自動更新機能を追加する方法について紹介します。

## WinSparkle
[Winsparkle](https://github.com/vslavik/winsparkle)はWindows用のソフトウェア自動更新ライブラリです。

C++のライブラリですが、PythonやGOなどのバインディングも存在します。

今回はC++のシンプルなコンソールアプリケーションを作成して、Winsparkleによる自動更新機能を実装します。
（次回以降の記事ではPython製のソフトウェアにWinsparkleを組み込む方法について書く予定です。）

また、Winsparkleではソフトウェアのインストーラーとappcastというxmlファイルをインターネット上に公開することによって、利用可能なソフトウェアバージョンを周知します。
本記事ではPythonでローカルHTTPサーバーを立ててテストする方法を紹介します。
次回の記事で、AWSのCloudFrontとS3を使用した静的ファイルサーバー上にこれらをサーブして使用する方法についても説明します。

本記事のサンプルプログラムのレポジトリです。
https://github.com/hsuzuki-gcs/AutoUpdateExample

## C++プロジェクトの作成
作業前にVisual Studio InstallerでC++デスクトップ開発にチェックが入っているか確認します。入っていない場合はチェックして更新を行ってください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/4e3add1a-a941-22d8-96c3-69f34e17f7d6.png)

Visual Studioを起動して新規プロジェクトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/84183d5b-4980-4750-115e-b5e1f91d1e20.png)

C++　Console APPを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/21d14073-ba55-f1eb-1c56-9e3b05762ce9.png)

プロジェクト名を入力します。今回はAutoUpdateExampleとしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/e8c73e2b-2d2e-0d14-b053-692b1ea547c1.png)

プロジェクトが作成されます。デフォルトでは実行するとHello Wolldというテキストを出力するプログラムが用意されています。デバッグを実行するかビルドして作成されたexeファイルを実行することで確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/bd8323f3-d3d7-cb5c-6db1-ff06e54d8d29.png)

## WinSparkleの追加
Winsparkleをプロジェクトに追加していきます。
WinsparkleはNugetによって配布されています。ソリューションエクスプローラーからプロジェクトを右クリックしてManage Nuget Package...をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/5752d9fb-17d5-9ef1-7d2b-a3d04bb2dea0.png)

BrowseからWinSparkleを探してインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/24fe602a-efc0-bb91-acae-c706c5ed91fa.png)


インストールが完了したらAutoUpdateExample.cppにコードを追加してWinsparkleの挙動を確認してみましょう。
``` C++:AutoUpdateExample.cpp
#include "winsparkle.h" 
#include <iostream>

int main()
{
    win_sparkle_set_appcast_url("https://winsparkle.org/example/appcast.xml");

    win_sparkle_set_app_details(L"guncys.com", L"Auto Update Example", L"1.0");

    win_sparkle_init();

    win_sparkle_check_update_with_ui();

    std::cout << "Hello World Version 1.0\n";

    do
    {
        std::cout << '\n' << "Press a key to continue...";
    } while (std::cin.get() != '\n');

    win_sparkle_cleanup();

    return 0;
}
```

以下コードの説明です。

### 1. ライブラリのインクルード
まずは、Winsparkleライブラリを使用するためにヘッダーをインクルードします。
```C++
#include "winsparkle.h"
```

### 2. メタデータのセットアップ
続いて`win_sparkle_set_appcast_url()`でappcastを設定します。
ここではwinsparkleがデモとして公開しているurlを指定しています。
Winsparkleはこのappcastから利用可能なソフトウェアバージョンの情報を取得します。
より詳しい説明については[こちら](https://github.com/vslavik/winsparkle/wiki/Appcast-Feeds)


加えて`win_sparkle_set_app_details()`で現在起動しているソフトウェアの詳細を指定します。
ここで指定した情報とappcastから得られた利用可能なバージョン情報を擦り合わせて、アップデートを実行できるかどうか判定しています。

```C++: 
win_sparkle_set_appcast_url("https://winsparkle.org/example/appcast.xml"); 

win_sparkle_set_app_details(L"guncys.com", L"Auto Update Example", L"1.0"); 
```

### 3. ライブラリの初期化
必要な情報をセットした上で`win_sparkle_init()`によってライブラリを初期化します。


winsparkleは更新可能なバージョンがあれば自動でGUIを表示してくれます。
しかし一度更新確認のGUIが表示された後デフォルトで一時間はインターバルを設けてGUIを表示させないという仕様になっています。
実際にユーザーが使用する環境ではこの設定で問題ありませんが、テスト時の動作確認用に`win_sparkle_check_update_with_ui()`によって起動時に毎回アップデートチェックのGUIを表示させます。
実際のGUIのあるソフトウェアに組み込む場合はアップデート確認用ボタンを作成して、クリック時に呼び出されるような形がいいと思います。

```C++: 
win_sparkle_init();

win_sparkle_check_update_with_ui();
```
　  
デフォルトのプログラムではHello Worldを出力してすぐに終了してしまいます。
テキストを出力した後、ユーザーがコマンドプロンプト上で何かを入力するまで待機させます。
最後にプログラムが終了する前に`win_sparkle_cleanup()`を実行します。
```c++
do
{
    std::cout << '\n' << "Press a key to continue...";
} while (std::cin.get() != '\n');

win_sparkle_cleanup();

return 0;
```

##
デバッグを実行すると、WinsparkleのGUIが表示されるようになりました。
現在のバージョンが1.0, Appcastでは1.8.8のインストーラーが配置されているため更新可能というGUIが表示されます。
もちろんここで配布されているインストーラーは全く別のアプリケーションなのでこのままアップデートを実行したところで別のソフトウェアがインストールされるだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/68404023-c0e5-4923-02ad-db4751caf08b.png)

## インストーラーの作成
ここからは作成したプログラムのインストーラーの作成を行います。

インストーラー作成にはＷiXを使用します。
[こちら](https://www.firegiant.com/docs/wix/tutorial/)の公式のチュートリアルに詳細な説明があるため、ざっくりとした手順の解説になります。

Visual StudioのExtensions > Manage Extensionsを選択します。
Manage ExtensionsウィンドウからHeatWaveを探してダウンロードします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/0ead8ef0-8d71-e7e1-0152-fff6f6e52fc8.png)

Visual Studioを終了するとインストールが開始されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/f18d0a35-3700-9b10-dd0a-06e083e4ae58.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/f0cf5193-db69-4fde-65b4-e32f5294f1c7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/5008c08f-f5be-e0a8-ca1e-08bddcc599bf.png)

再起動してソリューションエクスプローラーからプロジェクトの追加を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/866576f4-cc09-ae57-def4-d08ff42c356d.png)

プロジェクト作成画面にWiX用のテンプレートが追加されています。
MSI Packageを選択して作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/27436e19-bf4f-d5cb-8ed1-49154ee81545.png)

Project名を適当に決めておきます。ここではPackageとしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/cb9e67bd-5f40-b6bb-393b-57cc30891b54.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/dfbe63bc-8648-473d-cf9f-f9d799f74e7a.png)


### メタデータの編集
Package.wxsを以下のように変更します。
```XML:Package.wxs
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
  <Package Name="AutoUpdateExample" Manufacturer="Guncys" Version="1.0" UpgradeCode="71e484be-a932-4ee0-a206-6cd1c22f8d2d">
    <MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />

    <Feature Id="Main">
      <ComponentGroupRef Id="ExampleComponents" />
    </Feature>
  </Package>
</Wix>

```

`<Package Name="AutoUpdateExample" Manufacturer="Guncys" Version="1.0"　UpgradeCode="71e484be-a932-4ee0-a206-6cd1c22f8d2d">`　
２行目でソフトウェア名、メーカー名、バージョン等の情報を設定しています。

Folder.wxsでインストールディレクトリを以下のように指定してあるため、今回であれば`C:\Program Files\Guncys AutoUpdateExample`以下にプログラムがインストールされるようになります。
```xml: Folder.wxs
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
  <Fragment>
    <StandardDirectory Id="ProgramFiles6432Folder">
      <Directory Id="INSTALLFOLDER" Name="!(bind.Property.Manufacturer) !(bind.Property.ProductName)" />
    </StandardDirectory>
  </Fragment>
</Wix>
```

### プロジェクト参照の追加
続いてインストールするべきファイルを指定します。
まずは、ソリューションエクスプローラーからPackageプロジェクトのDependenciesを選択してAdd Project References...をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/f6708488-f0da-b7f5-255d-832d4796ebfd.png)

Reference ManagerからAutoUpdateExampleにチェックを入れてOKします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/953ddb1a-0c7a-22c6-7967-5385cc615013.png)

ここでエクスプローラーから追加するべきファイルを確認しておきます。
必要なのは実行ファイルであるAutoUpdateExample.exeとWinSparkle.dllです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/439616bd-64ad-f803-b679-0d88d07c2067.png)


ちなみにWinSparkle.dllはnugetパッケージ上でプログラムのビルド時にアウトプットフォルダーにコピーされるよう`%ソリューションフォルダー%\packages\WinSparkle.0.8.1\build\native\WinSparkle.targets`で指定されているため存在しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/0d36233e-6d7c-cd31-098e-8f302c4a3063.png)

### インストールファイルの指定
AutoUpdateExample.exeとWinSparkle.dllがインストールされるように、ExampleComponent.wxsを変更します。
HeatWave拡張機能では参照するプロジェクトのアウトプットディレクトリを検索パスに追加してくれるため、ファイル名を指定するだけでOKです。
詳しくは[こちら](https://www.firegiant.com/docs/wix/tutorial/sprint2/replace-placeholder-component/)
```XML:ExampleComponent.wxs
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
  <Fragment>
    <ComponentGroup Id="ExampleComponents" Directory="INSTALLFOLDER">
      <Component>
        <File Source="AutoUpdateExample.exe" />
      </Component>
      <Component>
        <File Source="WinSparkle.dll" />
      </Component>
    </ComponentGroup>
  </Fragment>
</Wix>

```

以上で基本的な設定は完了です。プロジェクトをビルドします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/d128fbbf-e28b-d8e9-3f95-693bb3dcf4df.png)

MSIインストーラーが作成されました。インストーラーをクリックして実行してみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/91083c09-9cdb-8796-4b0c-e75370c7fb24.png)

デフォルトでWiXにはインタラクティブなGUIは存在しないため、うまく動作しているのか不安になりますが、インストール済みアプリに登録され、必要なファイルがインストールされているのが確認できます。
設定から Apps > Installed AppsでAutoUpdateExampleが登録されているのを確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/48a7d051-dd9e-fa09-b9c0-c5030b4cdd3e.png)

`C:\Program Files\Guncys AutoUpdateExample`にファイルがインストールされています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/21b09df4-dc42-5c4e-537a-43f695b213b3.png)

EXEを実行すると問題なく動作しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/8b6aa466-adae-2fa0-2c25-ec2597b9884f.png)

## ローカルアップデートサーバーの構築
ローカルにアップデートサーバーを構築して自動更新の動作を確認します。
WinSparkleはappcast.xmlがhttpsかhttp経由で配信されている必要があるためローカルでのテストの場合も`file://~/appcast.xml`のような形式でのテストはできません。
ここではpythonでローカルhttpサーバーを立ててappcast.xmlを配信する方法を紹介します。

Pythonがない場合は、Wingetを使用してPythonをインストールします。
```
winget install Python.Python.3.12
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/910d8e2c-2bad-d97c-09a2-82df76ef7b90.png)

ソリューションフォルダーにupdate-serverフォルダを作成して直下のupdate-server.batファイルを作り以下のように記載します。
```winbatch: update-server.bat
@echo off

python -m http.server
```

batを実行して確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/893a2e02-9aa3-93be-d0e5-9a91c4ff7a9c.png)

ブラウザで確認するとupdate-serverフォルダ以下のファイルを提供するhttpサーバーとして動作しているのが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/02552470-73d9-4803-daec-13e35b35c0e4.png)

update-serverフォルダ以下にwinsparkleが必要とするファイルを追加していきます。
Package.msiはPackageプロジェクトのアウトプットディレクトリからコピーしてきてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/a814ff38-0da2-2446-0326-7da562ac8651.png)

各ファイルには以下のように記述してあります。
```xml:appcast.xml
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
  <channel>
    <title>Example app updates</title>
    <link>http://localhost:8000/appcast.xml</link>
    <description>Appcast for Example app updates.</description>
    <language>en</language>
    <item>
      <title>Version 1.0</title>
      <sparkle:releaseNotesLink>http://localhost:8000/relnotes.html</sparkle:releaseNotesLink>
      <pubDate>Sun, 13 Apr 2024 14:27</pubDate>
      <enclosure url="http://localhost:8000/Package.msi" sparkle:version="1.0" type="application/octet-stream"/>
    </item>
  </channel>
</rss>
```
主に重要なのは`<enclosure />`の部分です。利用可能なインストーラーのURLとバージョンが指定されています。

```html:relnotes.html
<html>
	<head>
		<meta http-equiv="content-type" content="text/html;charset=utf-8">
		<title>What's new in AutoUpdateExample 1.0?</title>
		<meta name="robots" content="noindex">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
	</head>
    <body>
        <h3>バージョン 1.0</h3>
        <ul>
        	<li>自動アップデートシステムのテストです。</li>
        </ul>
    </body>
</html>

```
リリースノートでは新しいバージョンの更新内容について記載します。
ここで記載した情報が、WinSparkleのGUI上に表示されます。


ここまでで、ローカルサーバーの構築は完了です。
AutoUpdateExample.cppでローカルサーバーのappcastを参照するように変更します。
```c++:AutoUpdateExample.cpp
// ローカルのアップデートサーバーを指定
win_sparkle_set_appcast_url("http://localhost:8000/appcast.xml"); 
```

この状態でデバッグを実行すると最新バージョンです、とのGUIが表示されます。
起動しているバージョンが利用可能な最新バージョンと一致している場合はこのような表示になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/6f1f1a51-3045-7b6b-21a7-ddb71ec6ebe2.png)

確認のためにapp_detailsのバージョンを0.1に下げてデバッグを実行します。
```c++:AutoUpdateExample.cpp
//確認のためバージョンを1.0 → 0.1に変更
win_sparkle_set_app_details(L"guncys.com", L"Auto Update Example", L"1.0"); 
```

更新可能であることを通知するGUIが表示されました。ローカルサーバーは正しく動作してそうですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492828/4d0f430d-e0ef-7cf3-7fee-a7ac999a8178.png)

確認が完了したらバージョンを1.0に戻しておきます。

## まとめ
中途半端な感じもしますが、WinSparkleを導入するまででの簡単な流れの説明でした。何かの参考になれば幸いです。

次回はappcastをクラウド上で配信する方法について説明する予定です。