WPFの配布と更新について_InnoSetupとAutoUpdater.NET

WPFアプリを作り、それをインストーラーとして配布して、バージョンアップがあった時は、更新するようにしたい。
Visual Studioだとmsixがありますが、VSCodeだと手間そうなので他の方法を試してみました。

こちらの方の記事で、AutoUpdater.NETを知り試してみました！
[.NET9のWPFアプリを爆速で作る　アップデート編](https://qiita.com/umbreon-nu/items/8105d55e6262118648e4)

[追記更新]
最終的なissコードを追記ました。
⇒[最終的なiss全体](#最終的なiss全体)

# 準備
- .NET9 vscode
- Inno Setup ver6.4.1

# WPFアプリ
とりあえず適当なWPFアプリを作ります。
その時にバージョン情報を入れておきます。
これを入れておかないと、バージョン情報で更新するかどうかの判定ができなくなるため。

```xml
<PropertyGroup>
    ～
    <Version>0.3.4.0</Version>
</PropertyGroup>
```

- バージョン情報
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2383487/7bc306fb-ddd7-403c-8640-28a67f7b3e95.png)


# Inno Setupの変更箇所
おおよそはウィザードでの初期表示そのままですが、以下を変更してみました。

## 1. スタートアップに追加
TasksとIconsに追記することで、スタートアップへ追加するチェックボタンを表示できる。
アプリを常駐させたい時とかに使える。

```
[Tasks]
Name: "startupicon"; Description: "スタートアップにショートカットを作成";

[Icons]
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startupicon
```

これだとWarningがでるので`[Setup]`に`PrivilegesRequired=none`を追記しておきます。
> `Warning: The [Setup] section directive "PrivilegesRequired" is set to "admin" but per-user areas (userstartup) are used by the script. Regardless of the version of Windows, if the installation is running in administrative install mode then you should be careful about making any per-user area changes: such changes may not achieve what you are intending. See the "UsedUserAreasWarning" topic in help file for more information.`

## 2. 規定でチェックを入れる
- デスクトップ・スタートアップへのショートカットのTasksの所で`Flags: unchecked`を消すと規定でレ点が入ります
- `checked`なのかなと思ったけどそんなのなかった

## 3. インストーラーの出力フォルダをissと同じ場所にする
- [Setup]で`OutputDir=.`に変更する

## 4. インストーラーへのバージョン情報追加
- [Setup]に`VersionInfoVersion={#MyAppVersion}`を追加する

## 5. アプリとインストーラーのバージョンを合わせる
- アプリ側で変更してインストーラーの方で間違いそうなので以下で対応
- アプリのバージョン情報が`1.1.1.1`のように「.」区切りの4つの数値でないと取得できないかもしれません

```
; パスは""ではなく''で囲む
#define MyAppPath 'mypath\path\'
#define MyAppVersion GetVersionNumbersString(MyAppPath + MyAppExeName)

[Setup]
AppVersion={#MyAppVersion}
VersionInfoVersion={#MyAppVersion}
```

## 最終的なiss全体

<details><summary>コード全体</summary>

```
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!

#define MyAppName "TaskReminder"
; #define MyAppVersion "0.0.0.0"
#define MyAppPublisher "My Company, Inc."
#define MyAppURL "https://www.example.com/"
#define MyAppExeName "TaskReminder.exe"
#define MyAppAssocName MyAppName + " File"
#define MyAppAssocExt ".myp"
#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt
; パスは""ではなく''で囲む
#define MyAppPath 'C:\Users\path\path\path\'
#define MyAppVersion GetVersionNumbersString(MyAppPath + MyAppExeName)

[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{CC81B2B5-0F95-4968-948B-D51EBD4329C9}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
; AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
; on anything but x64 and Windows 11 on Arm.
ArchitecturesAllowed=x64compatible
; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
; meaning it should use the native 64-bit Program Files directory and
; the 64-bit view of the registry.
ArchitecturesInstallIn64BitMode=x64compatible
ChangesAssociations=yes
DisableProgramGroupPage=yes
; Uncomment the following line to run in non administrative install mode (install for current user only).
PrivilegesRequired=none
OutputDir=.
OutputBaseFilename=TaskReminderSetup
SetupIconFile={#MyAppPath}logo.ico
SolidCompression=yes
VersionInfoVersion={#MyAppVersion}
WizardStyle=modern

[Languages]
Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
Name: "startupicon"; Description: "スタートアップにショートカットを作成";

[Files]
Source: "{#MyAppPath}{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[Registry]
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startupicon

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
```

</details>

# AutoUpdater.NET
こちらの方の記事を参考にさせて頂きました。
[.NET9のWPFアプリを爆速で作る　アップデート編](https://qiita.com/umbreon-nu/items/8105d55e6262118648e4)
- dotnet add package Autoupdater.NET.Officialで追加

## アプリとxmlのバージョンを合わせる
アップデートはアプリ自体のバージョン情報とUpdateInfo.xmlのバージョン情報で比較されるので、この変更を間違ったらアップデートが入らなくなってしまいます。
ビルド時にバージョンを合わせるようにしてみます。

1. 別でクラスライブラリをつくる
    - dotnet new classlib -n VersionSyncTask
    - cd VersionSyncTask
    - dotnet add package Microsoft.Build.Framework
    - dotnet add package Microsoft.Build.Utilities.Core

    <details><summary>VersionSyncTask</summary>
    
    ```VersionSync.cs
    using System.Xml.Linq;
    using Microsoft.Build.Framework;
    
    namespace VersionSyncTask
    {
        public class VersionSync : Microsoft.Build.Utilities.Task
        {
            [Required]
            public string CsprojPath { get; set; } = "";
    
            [Required]
            public string XmlPath { get; set; } = "";
    
            public override bool Execute()
            {
                try
                {
                    // .csprojファイルのバージョンを取得
                    XDocument csprojDoc = XDocument.Load(CsprojPath);
                    var versionElement = csprojDoc?.Root?.Element("PropertyGroup")?.Element("Version");
                    string version = versionElement!.Value;
    
                    // UpdateInfo.xmlファイルのバージョンを更新
                    XDocument xmlDoc = XDocument.Load(XmlPath);
                    var xmlVersionElement = xmlDoc.Root?.Element("version");
                    xmlVersionElement!.Value = version;
    
                    // UpdateInfo.xmlファイルを保存
                    xmlDoc.Save(XmlPath);
    
                    Log.LogMessage(MessageImportance.High, "バージョンが同期されました: " + version);
                    return true;
                }
                catch (Exception ex)
                {
                    Log.LogErrorFromException(ex);
                    return false;
                }
            }
        }
    }
    ```

    </details>

    - `CsprojPath`と`XmlPath`はcsprojから呼び出す時に指定します
    - dotnet publish -c Release

1. ビルドしたdllをwpfのプロジェクトへ保存する
1. UpdateInfo.xmlをwpfプロジェクトへ保存する
1. csprojへtaskを追記する

    - これでプロジェクトのバージョン情報とxmlを合わせることができました
    ```xml
      <!-- バージョンの同期 -->
      <UsingTask TaskName="VersionSyncTask.VersionSync" AssemblyFile="VersionSync\VersionSyncTask.dll" />
      <Target Name="SyncVersion" BeforeTargets="BeforeBuild">
        <VersionSync CsprojPath="$(MSBuildProjectFile)" XmlPath="VersionSync\UpdateInfo.xml" />
      </Target>
    ```
    
1. 更新したxmlファイルをビルドフォルダへコピーする
    ```xml
    <ItemGroup>
    ～
        <!-- ビルド時にコピーするファイル -->
        <None Update="VersionSync\UpdateInfo.xml">
            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </None>
    </ItemGroup>
    ```

# インストーラーの発行
インストーラーへのコンパイルもできるように以下をつくっておきます。
コマンドパレットから、Tasks: Run Taskでつくったのを実行します。
```tasks.json
"tasks": [
    ～
    {
        "label": "compile-inno-setup",
        "type": "shell",
        "command": "C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe",
        "args": [
            "C:\\Users\\path\\app.iss"
        ],
    }
]
```


# 参考
- [Inno Setup Downloads](https://jrsoftware.org/isdl.php)
- [AutoUpdater.NET](https://github.com/ravibpatel/AutoUpdater.NET)
- [Visual Studio 2022 Installer Projects と AutoUpdater.NET を使ってネットワーク経由でアップデートする方法](https://knkomko.hatenablog.com/entry/2023/02/14/002422)
- [Inno Setupに新設されたFile Version functions解説](https://qiita.com/tohshima/items/4a9064bb61913142e8bf#getversionnumbersstring)