Windows Installer手引書 Part.31 セットアップランチャーを作る

[**前の記事へ**](https://qiita.com/tohshima/items/7b65e253ce2d94dc4c23)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/4c4e293c87b937ca5f14)

## セットアップランチャーを使うメリット

MSIファイル単独でも、UI言語を多言語化したインストーラーを作ることもできますし、CABファイルやMSTファイルをMSIファイルに格納すればインストーラーのファイルを一つにすることができます。配布するファイルが1つであれば、製品をリリースする際の確認も容易ですし、ユーザーもどのファイルを起動すれば良いのか悩むこともなく、メリットを享受することができます。それでも、直接MSIファイルをユーザーが開くのではなく、MSIファイルを起動するための「セットアップランチャー」を用意するメリットは多くあります。これまでの経験では、トラブル対応のために役立ったことも多くありますし、そもそもセットアップランチャーがなければ実現できない機能もあります。また、アンインストール時にもMSIでインストールした製品をランチャーでアンインストールするようにしておくと、大きく役に立ちます。MSIファイルを扱うmsiexec.exeは色々なコマンドラインオプションを指定することができますが、それは単純にMSIファイルをダブルクリックしただけでは、すべての機能を使えないことを意味します。Windows Installerの機能を使いこなしたインストーラーを作成するには、セットアップランチャーを用意するしかないのです。

## セットアップランチャー作成に適したツール

どのようにセットアップランチャーを作成するのが良いでしょう。世の中にあるWindows向けのアプリを作成するためのツールは何でも使えます。過去にはAdobe Directorを使ったものも見たことがあります。ただ、我々が作ろうとしているのはインストーラーです。私は、Windows Installerの仕組みを使わないインストーラー作成ツールでセットアップランチャーを作成することをお勧めします。当然ながら最初からインストーラーの作成に必要な機能を持ち合わせているので、圧倒的に実装が楽になります。インストール時のUI表示をセットアップランチャー側で行い、実際のインストール処理をMSIが行う、といったこともできるようになります。そうすると、セットアップランチャーを作成するツールも候補が絞られてきます。私が推すツールはInno Setupです。

WiX ToolsetにはPackage Bundleという複数のMSI形式のインストーラーをまとめてインストールさせる仕組みが存在するので、これをセットアップランチャーとして使う手もあります。このツールは少しクセがあって、自由度も高くはないので、使う機会は少ないと思います[^1]。

## 32bit版インストーラーと64bit版インストーラーを一つにする

ここから、Inno Setupを使ったセットアップランチャーの事例として、32bit版インストーラーと64bit版インストーラーを一つのexe形式のプログラムにまとめてみます。MSI形式のインストーラーはWindows Installerの仕様上、32bit版アプリ用と64bit版アプリ用を一つにできません。いまどき32bit版Windowsを使うケースは減っていますが、題材として分かりやすいので最初に紹介したいと思います。

最初に32bit版インストーラーと64bit版インストーラーのMSI/CABファイルを用意します。両アプリで共通のファイルがある、という想定でCABファイルは32bit/64bit共用のファイルも用意します。最近のインストーラーはユーザーがダウンロードするケースがほとんどでしょうから、1byteでも小さくしましょう。また、今回はUI関連の話を省いて説明をシンプルにするため、MSIファイルを作る際にWixUI Dialog Libraryは使用しません。ようこそ画面と完了画面はInno Setupのダイアログを使用します。

|ファイル名|32bit/64bit|説明|
|:--|:--|:--|
|App32.msi|32bit|32bit版インストーラー本体|
|a32b.cab|32bit|32bit版インストーラー用Cabinetファイル|
|App64.msi|64bit|64bit版インストーラー本体|
|a64b.cab|64bit|64bit版インストーラー用Cabinetファイル|
|aCom.cab|共用|64bit/32bit共用Cabinetファイル|

Inno Setupでは、下記の動作を実装します。
1. これらのファイルをテンポラリのフォルダにインストール
2. インストール先環境に応じて32bit版か64bit版のインストーラーを起動
3. テンポラリのファイルを削除

Inno Setupのソースを示します。

```Pascal:App3264.iss
[Setup]
DisableWelcomePage=False
DisableReadyPage=True
DisableReadyMemo=True
AppName=App3264
AppVersion=1.0.0
PrivilegesRequired=lowest
CreateAppDir=False
ShowLanguageDialog=no
Uninstallable=no
OutputBaseFilename=App3264Setup

[Files]
Source: "App32.msi"; DestDir: "{tmp}"; Flags: deleteafterinstall; Check: not IsWin64
Source: "a32b.cab"; DestDir: "{tmp}"; Flags: deleteafterinstall; Check: not IsWin64
Source: "App64.msi"; DestDir: "{tmp}"; Flags: deleteafterinstall; Check: IsWin64
Source: "a64b.cab"; DestDir: "{tmp}"; Flags: deleteafterinstall; Check: IsWin64
Source: "aCom.cab"; DestDir: "{tmp}"; Flags: deleteafterinstall

[Run]
Filename: "msiexec"; Parameters: "/i {tmp}\App32.msi"; WorkingDir: "{tmp}"; Check: not IsWin64; BeforeInstall: hideISSWin; AfterInstall: showISSWin
Filename: "msiexec"; Parameters: "/i {tmp}\App64.msi"; WorkingDir: "{tmp}"; Check: IsWin64; BeforeInstall: hideISSWin; AfterInstall: showISSWin

[Code]
//-------------------------------------------------------------------
// [RUN]セクションでMSI起動時に呼ぶイベントハンドラ
procedure hideISSWin();
begin
  // Inno Setupのダイアログを非表示にする
  WizardForm.Hide();
end;
//-------------------------------------------------------------------
// [RUN]セクションでMSI終了時に呼ぶイベントハンドラ
procedure showISSWin();
begin
  // wpReadyの表示を抑制する
  WizardForm.Show();
end;
```

[Files]セクションでは、

- インストール先を{tmp}に
- deleteafterinstallフラグの指定によりインストール終了時に{tmp}のファイルを削除、
- Checkフラグでインストール先環境が32bitか64bitか判定

といったことをしています。
[Run]セクションでは、

- msiexecを起動し、指定したMSIファイルをインストール
- MSIファイルは[Files]セクションで{tmp}フォルダ下に展開されるファイルを指定
- Checkフラグでインストール先環境が32bitか64bitか判定
- BeforeInstallでInno Setupのウィンドウを一時隠して、MSIのウィンドウが隠れないようにする
- AfterInstallでMSIのインストールが終了後に、Inno Setupのウィンドウを元通り表示させる

とします。
Inno Setupでは、[Files]セクションや[Run]セクションに`32bit`、`64bit`というフラグが用意されていますが、`64bit`のフラグが使われると32bit版Windowsで起動できなくなるので使用できません。

64bit版Windowsで実行したときの画面遷移は以下のように、ようこそ画面のあと64bit版のMSIファイルが呼ばれ、完了画面に遷移します。
![Part30_3264onWin64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/fc5600e6-a527-6658-8dc8-5cfddd8c4616.png)
32bit版Windowsで実行したときの画面遷移は以下のように、ようこそ画面のあと32bit版のMSIファイルが呼ばれ、完了画面に遷移します。
![Part30_3264onWin32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/03faecc5-0c72-d80e-b6a6-3a367b525f3e.png)

## 複数のインストーラーをセットアップランチャーから一気に起動する

次の事例は、２つの子インストーラーをチェックボックス付きでリスト表示し、各々インストールするかどうかを選択できるようにしてみます。

最初に2種類のMSIファイルとCABファイルのセットを用意します。

|ファイル名|説明|
|:--|:--|
|Product1.msi|製品１インストーラー本体|
|PRD11.cab|製品１インストーラー用Cabinetファイル|
|Product2.msi|製品２インストーラー本体|
|PRD21.cab|製品２インストーラー用Cabinetファイル|

Inno Setupでの実装方法は、基本的に先ほどの32bit版インストーラーと64bit版インストーラーを一つにした事例と同じです。２つのインストーラーの選択肢を表示するために、[Components]セクションを使って２つのコンポーネントに振り分けて、ユーザーがインストールするかどうかを選択できるようにします。

Inno Setupのソースを示します。

```Pascal:TwoProducts.iss
[Setup]
DisableWelcomePage=False
DisableReadyPage=True
DisableReadyMemo=True
AppName=TwoProducts
AppVersion=1.0.0
CreateAppDir=False
Uninstallable=no
OutputBaseFilename=TwoProductsSetup

[Files]
Source: "Product1.msi"; DestDir: "{tmp}"; Flags: deleteafterinstall; Components: Product1
Source: "PRD11.cab"; DestDir: "{tmp}"; Flags: deleteafterinstall; Components: Product1
Source: "Product2.msi"; DestDir: "{tmp}"; Flags: deleteafterinstall; Components: Product2
Source: "PRD21.cab"; DestDir: "{tmp}"; Flags: deleteafterinstall; Components: Product2

[Run]
Filename: "msiexec"; Parameters: "/i Product1.msi"; WorkingDir: "{tmp}"; Components: Product1; BeforeInstall: hideISSWin; AfterInstall: showISSWin
Filename: "msiexec"; Parameters: "/i Product2.msi"; WorkingDir: "{tmp}"; Components: Product2; BeforeInstall: hideISSWin; AfterInstall: showISSWin

[Components]
Name: "Product1"; Description: "Install Product 1"; Types: DefaultType
Name: "Product2"; Description: "Install Product 2"; Types: DefaultType

[Types]
Name: "DefaultType"; Description: "DefaultType"; Flags: iscustom

[Code]
procedure hideISSWin();
//-------------------------------------------------------------------
// [RUN]セクションでMSI起動時に呼ぶイベントハンドラ
begin
  // Inno Setupのダイアログを非表示にする
  WizardForm.Hide();
end;
//-------------------------------------------------------------------
// [RUN]セクションでMSI終了時に呼ぶイベントハンドラ
procedure showISSWin();
begin
  // wpReadyの表示を抑制する
  WizardForm.Show();
end;
```

[Components]セクションで`Product1`と`Product2`の２つのコンポーネントを定義し、これらを使って[Files]セクションと[Run]セクションの中で割り当てるコンポーネントを指定します。[Components]セクションが定義されると、インストールするコンポーネントを選択するwpSelectComponentsダイアログが表示されるようになります。また、wpSelectComponentsダイアログでタイプを選択するデフォルトのコンボボックスが表示されないように、[Types]セクションに一つだけタイプを定義します。

ビルドしたインストーラーを実行すると、以下のように画面が遷移します。wpSelectComponentsダイアログでチェックを入れた項目に対応したMSIファイルが順次呼ばれて、インストールされます。
![Part30_TwoSequence.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/b45dbda2-244a-3a6f-e0f4-8514f58fbada.png)

## インストール済みか検出したらREINATALLモードに切り替える

セットアップランチャーを使うメリットの一つが、インストール先環境の状況によりmsiexecに与えるコマンドラインオプションを変更できることです。[前回](https://qiita.com/tohshima/items/7b65e253ce2d94dc4c23#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%B8%88%E3%81%BF%E3%81%8B%E6%A4%9C%E5%87%BA%E3%81%97%E3%81%A6reinatall%E3%83%A2%E3%83%BC%E3%83%89%E3%82%92%E4%BD%BF%E3%81%86%E6%89%8B%E3%82%82%E3%81%82%E3%82%8B)説明した、下記の仕様を実現します。

>初めて起動したなら、
>`msiexec /i Part31_03.msi`
>のように起動する。インストール済み製品のインストーラーをもう一度起動したのなら、
>`msiexec REINSTALL="ALL" REINSTALLMODE="omus" /i Part31_03.msi`
>のように起動する。

インストール済み製品のインストーラーをもう一度起動したとき、通常はメンテナンスモードでインストーラーが起動するところ、上書きインストールに動作を変えてみます。ここまでの例とは異なり、今回はMSI側にUIを表示させ、Inno Setup側ではインストーラーの起動に専念させます。セットアップランチャーがやるべきことは、以下のようになります。

1. インストールしようとしているMSIと同じ製品コードの製品がインストールされていなければ、特別にコマンドラインオプションを追加せずにインストーラーを起動する。
2. インストールしようとしているMSIと同じ製品コードの製品がインストールされていたら、そのバージョンを取得して、インストールしようとしているMSIのバージョンと比較する。同じであれば、REINSTALLプロパティ、REINSTALLMODEプロパティをコマンドラインオプションに追加してインストーラーを起動する。
3. バージョンが違っていれば特別にコマンドラインオプションを追加せずにインストーラーを起動する。

セットアップランチャーは、まず自分がインストールしようとしているMSIファイルの製品コードとバージョンを知る必要があります。製品コードとバージョンはWiX ToolsetのソースとInno Setupのソースコードの両方に同じ値を持つ必要がありますが、両方に直接値を書き込むとメンテナンス性が悪くなります。そこで、製品コードとバージョンをWiX Toolset側に定義し、Inno Setupでセットアップランチャーをビルドする前に、WiX ToolsetでビルドされたMSIファイルからスクリプトで取得することにします。以降のスクリプトではWindows InstallerのAutomation Interfaceを利用します。マイクロソフトのドキュメントは下記の場所にあります。

**Automation Interface**
https://docs.microsoft.com/en-us/windows/win32/msi/automation-interface

セットアップランチャーから呼ぶMSIファイルが`bin\Debug`フォルダに置かれているとすると、下記のJScriptプログラムで製品コードとバージョンをファイルに出力することができます。

```javascript:generateIncludeFile.js
var databasePath = "bin\\Debug\\Part31_03.msi";
var includeFileName = "defineGenerated.txt";
var queryProductCode = "SELECT `Value` FROM `Property` WHERE Property.Property='ProductCode'";
var queryVersion = "SELECT `Value` FROM `Property` WHERE Property.Property='ProductVersion'";
var msiOpenDatabaseModeReadOnly = 0;
var outStr = "";
var installer = WScript.CreateObject("WindowsInstaller.Installer");
var database = installer.OpenDatabase(databasePath, msiOpenDatabaseModeReadOnly);
var view, record;
// 製品コードを得る
view = database.OpenView(queryProductCode);
view.Execute();
record = view.Fetch();
outStr += "#define MSIPRODUCTCODE \"" + record.StringData(1) + "\"\r\n";
// バージョンを得る
view = database.OpenView(queryVersion);
view.Execute();
record = view.Fetch();
outStr += "#define MSIPRODUCTVERSION \"" + record.StringData(1) + "\"\r\n";
// ファイルに書き出す
WScript.echo(outStr);
var fso = WScript.CreateObject("Scripting.FileSystemObject");
var fp = fso.CreateTextFile(includeFileName, true);
fp.Write(outStr);
fp.Close();
WScript.Quit(0);
```

このスクリプトを実行するには、下記のコマンドを実行します。

```batchfile
cscript generateIncludeFile.js
```

実行の結果吐き出されるファイルは、下記のようなものになります。`MSIPRODUCTCODE`が製品コード、`MSIPRODUCTVERSION`がバージョンです。

```text:defineGenerated.txt
#define MSIPRODUCTCODE "{A359626A-9001-4309-BC04-2FD2F1E85AF8}"
#define MSIPRODUCTVERSION "1.0.0"
```

Inno Setup側では、MSIファイルのインストールを起動する前に、自分と同じ製品コードの製品がインストールされているか調べます。インストールされていたら自分と同じバージョンか調べて、msiexecのコマンドラインオプション文字列を決定します。Inno SetupでWindows InstallerのAutomationを利用する際には、下記の公式ドキュメントが参考になります。

**Pascal Scripting: Using COM Automation objects**
https://jrsoftware.org/ishelp/topic_scriptautomation.htm

Inno Setupのソースを以下に示します。

```pascal:Part31Setup.iss
#include "defineGenerated.txt"
#define BIN_PATH "bin\Debug"
#define LAUNCH_MSI "Part31_03.msi"
#define LAUNCH_CAB "cab1.cab"

[Setup]
DisableReadyPage=True
DisableFinishedPage=True
AppName=Part31
AppVersion={#MSIPRODUCTVERSION}
PrivilegesRequired=lowest
UsePreviousPrivileges=False
CreateAppDir=False
Uninstallable=no
OutputBaseFilename=Part31Setup

[Files]
Source: "{#BIN_PATH}\{#LAUNCH_MSI}"; DestDir: "{tmp}"; Flags: deleteafterinstall
Source: "{#BIN_PATH}\{#LAUNCH_CAB}"; DestDir: "{tmp}"; Flags: deleteafterinstall

[Run]
Filename: "msiexec"; Parameters: "/i {tmp}\{#LAUNCH_MSI}"; WorkingDir: "{tmp}"; Check: NOT existSameMsi; BeforeInstall: hideISSWin
Filename: "msiexec"; Parameters: "/i {tmp}\{#LAUNCH_MSI} REINSTALL=""ALL"" REINSTALLMODE=""omus"""; WorkingDir: "{tmp}"; Check: existSameMsi; BeforeInstall: hideISSWin

[Code]
var
  existSameMsiFlag: Boolean;  // インストールしようとしているMSIをインストール先に検出したらTrue
//-------------------------------------------------------------------
// 起動して一番最初に呼ばれるイベントハンドラ 
function InitializeSetup(): Boolean;
var
  installer, product: Variant;
begin
  existSameMsiFlag := False;
  installer := CreateOleObject('WindowsInstaller.Installer');
  try
    product := installer.OpenProduct('{#MSIPRODUCTCODE}');
  except
    // インストールしようとしているMSIと同じ製品コードが見つからなければ、InitializeSetup()関数を終了する
    Result := True;
    exit;
  end;
  if SameText(product.ProductProperty('ProductVersion'), '{#MSIPRODUCTVERSION}') then begin
    // インストールしようとしているMSIと同じ製品コードで同じバージョンなら、検出できたことにする
    existSameMsiFlag := True;
  end;
  Result := True;
end;
//-------------------------------------------------------------------
// UIが初期化されて一番最初に呼ばれるイベントハンドラ
procedure InitializeWizard();
begin
  // wpReadyの表示を抑制する
  WizardForm.Show;
end;
//-------------------------------------------------------------------
// [RUN]セクションでMSI起動時に呼ぶイベントハンドラ
procedure hideISSWin();
begin
  // Inno Setupのダイアログを非表示にする
  WizardForm.Hide();
end;
//-------------------------------------------------------------------
// [RUN]セクションの２つの項目のうち、どちらを使用するか判断するイベントハンドラ
function existSameMsi(): Boolean;
begin
  // インストールしようとしているMSIをインストール先に検出したかどうか返す
  Result := existSameMsiFlag;
end;
```

以上のようにいくつかのビルドやスクリプトの実行を経て成果物を得る場合は、ソースから一気にすべてをビルドするバッチファイルを用意すると便利です。

- WiX ToolsetのソースはVisual Studioで作成
- Inno Setupのバージョンは6
- インストーラーのビルド環境は64bit版Windows

という条件では、下記のようなバッチファイルでビルドができます。コマンドプロンプトから実行すると実行後にコマンドプロンプトが閉じてしまいますので、エクスプローラからダブルクリックして使用してください。

```batchfile:build.bat
rem +-------------------------------------------
rem | インストーラーを一気にビルドするスクリプト
rem +-------------------------------------------
set MSBLD_EXE="%windir%\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe"
set ISCC_EXE="%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe"
set THIS_PATH="%~dp0"
rem バッチファイルのディレクトリに移る
cd %THIS_PATH%
rem 
call :BuildMsi
call :MakeIncludefile
call :BuildSetup
@echo off
echo ========== Sucsess to build. ==========
pause
exit

rem ---------- MSI/CABをビルド ----------
:BuildMsi
%MSBLD_EXE% Part31_03.wixproj /t:Rebuild /p:Configuration=Debug /p:Platform=x86||call :errHandling
exit /b

rem ---------- Inno Setup用のIncludeファイルを作成 ----------
:MakeIncludefile
cscript //nologo generateIncludeFile.js||call :errHandling
exit /b

rem ---------- セットアップランチャーをビルド ----------
:BuildSetup
%ISCC_EXE% Part31Setup.iss||call :errHandling
exit /b

rem ---------- エラーが発生したらビルドを止める ----------
:errHandling
@echo off
echo ============
echo Error Found.
cd %THIS_PATH%
pause
exit
```

このインストーラーでインストールした場合、初回インストール時には通常のインストールと変わりませんが、再度インストーラーを起動すると、マイナーアップグレード時と同等のUIが表示され、上書きインストールされます。画面遷移は以下のようになります。
![Part31_2ndSeq.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/a3d382ef-a69d-9d16-8f54-d2a3ebb15bbf.png)


[^1]: とはいえ、どうしても使わなければならないときもあります。いずれ解説したいと思います。

[**前の記事へ**](https://qiita.com/tohshima/items/7b65e253ce2d94dc4c23)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/4c4e293c87b937ca5f14)