下準備
今回は下準備として、コマンドラインに与えたメッセージをコマンドプロンプトとダイアログボックスに表示するサンプルプログラムを作ります。 これをインストーラーの外部コマンドとして実行することを考えます。
#pragma comment(lib, "User32.lib")
#include <windows.h>
#include <stdio.h>
int main(int argc, char *argv[]){
if(argc > 1){
printf("Message : %s\n", argv[1]);
MessageBox(0, argv[1], "SimpleMsgBox", MB_OK);
}
return 0;
}
これを、例えばVisual Studio 2019がインストールされていれば、スタートメニューからx86 Native Tools Command Prompt for VS 2019
を起動して、下記コマンドでコンパイルし、SimpleMsgBox.exe
を得ます。
C:\>cl SimpleMsgBox.c
実行してみます。
C:\>SimpleMsgBox "This is TEST."
Message : This is TEST.
これに加えて、以下のようなダイアログボックスが表示されればOKです。
実行するプログラムをMSIのBinaryテーブルに格納する
インストール時に使用するプログラムをインストーラーに同梱する方法はいくつかありますが、まずMSI内に格納する方法を説明します。前準備で作成したSimpleMsgBox.exe
をカスタムアクションで実行させてみます。
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="F0B93737-A75E-4062-AD3B-5A8D2FD64CEB" Name="Part15_01" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="d66378d7-b106-43f3-851a-c715827569b4">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate />
<Binary Id="msgBox_exe" SourceFile="SimpleMsgBox.exe" />
<CustomAction Id="aShowMsg" BinaryKey="msgBox_exe" ExeCommand=""This is test."" Return="check" Execute="immediate" />
<InstallExecuteSequence>
<Custom Action="aShowMsg" After="InstallInitialize"></Custom>
</InstallExecuteSequence>
<Feature Id="ProductFeature" Title="Part15_01" Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
</Product>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="Part15_01" />
</Directory>
</Directory>
</Fragment>
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<Component Id="cExe" Guid="{BA64B660-AD44-4F55-BEA3-5A57E6B73BAF}" >
<File Id="fExe" Source="C:\Windows\System32\calc.exe" KeyPath="yes"/>
</Component>
</ComponentGroup>
</Fragment>
</Wix>
9行目のBinary
エレメントでMSIにSimpleMsgBox.exe
を格納し、10行目のCustomAction
エレメントのBinaryKey
アトリビュートにそのIDを指定します。ExeCommand
アトリビュートはコマンドラインオプションを指定するのですが、Binary
エレメントで格納したプログラムを実行する際には必ず必要で、コマンドラインオプションがなければ、ExeCommand=""
のように空の文字列を指定します。今回の例ではスペース文字を含む文字列をコマンドラインオプションとして渡すため、文字列を"
でくくる必要がありますが、XMLで表記する際は、"
でくくります。
実行すると、コマンドプロンプトとダイアログボックスが表示されるのが観測できます。SimpleMsgBox.exe
はコンソールアプリなのでコマンドプロンプトが表示されますが、Windowsデスクトップアプリとして作成すれば表示されません。
インストールしたファイルをカスタムアクションで実行する
コンポーネントでインストールしたファイルをカスタムアクションで実行できます。このアクションのためにファイルを配置するシーケンスの実行順が変わるわけではないので、アクションの実行タイミングに大きな制約があります。コンポーネントによるファイルの配置は、InstallFilesアクションによりdifferdのタイミングで実行されますので、これ以降に実行する必要があります。
ここでは例として、SimpleMsgBox.exe
をインストールし、ファイルが配置されたらすぐに実行するようなインストーラーを作成します。SimpleMsgBox.exe
をコンポーネントでインストールするように変更し、CustomAction
エレメント、InstallExecute
エレメントで実行できるようにします。
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="13D7AE55-73BD-49FB-8080-109059450246" Name="Part15_03" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="845310dc-24c4-442c-895d-859f3c0b0936">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate />
<CustomAction Id="aShowMsg" FileKey="fExe" ExeCommand=""This is test."" Return="check" Execute="deferred"/>
<InstallExecuteSequence>
<Custom Action="aShowMsg" After="InstallFiles">NOT Installed</Custom>
</InstallExecuteSequence>
<Feature Id="ProductFeature" Title="Part15_03" Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
</Product>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="Part15_03" />
</Directory>
</Directory>
</Fragment>
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<Component Id="cExe" Guid="{4539C5B7-A2D8-4A58-9979-3906B6C706A3}" >
<File Id="fExe" Source="SimpleMsgBox.exe" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>
ここで気を付けなければならないのは、11行目のcustomエレメントのConditionにNOT Installed
としているところです。これを忘れて常に実行するようにすると、アンインストールできないインストーラーができてしまいます。アンインストール時は、このカスタムアクションを実行するタイミングではSimpleMsgBox.exe
が消されたあとであるため、エラー終了するのです。
コンソールアプリ実行時にコマンドプロンプトを表示しない
WiX Toolsetは、コンソールアプリをカスタムアクションで実行する際、コマンドプロンプトを表示させない機能を持っています。コンソールアプリを直接実行するのではなく、コンソールアプリを実行する特別なカスタムアクションを実行することで、これを実現します。WiX Toolsetの公式マニュアルでは、こちらに説明があります。上の節で作成したインストーラーのカスタムアクションをこの機能で実装し直してみます。
最初にWiX Toolsetのカスタムアクションを使えるようにするために、Visual Studioのソリューションエクスプローラで、ReferencesにWixUtilExtension.dll
(WiX Toolsetのインストール先にあります)を追加します。
そして、下記の通りソースを変更します。変更点は、CustomAction
エレメント、InstallExecuteSequence
エレメントです。
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="26ABF8AF-7EBC-4BAB-AA2A-3FAB2340B4A8" Name="Part15_02" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="1ef29435-8dad-4e82-8073-3bd966e99da4">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate />
<CustomAction Id="setCmdLine" Property="aShowMsg" Value=""[#fExe]" "This is test."" />
<CustomAction Id="aShowMsg" BinaryKey="WixCA" DllEntry="WixQuietExec" Return="check" Execute="deferred" />
<InstallExecuteSequence>
<Custom Action="setCmdLine" After="InstallFiles">NOT Installed</Custom>
<Custom Action="aShowMsg" After="setCmdLine">NOT Installed</Custom>
</InstallExecuteSequence>
<Feature Id="ProductFeature" Title="Part15_02" Level="1">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
</Product>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="Part15_02" />
</Directory>
</Directory>
</Fragment>
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<Component Id="cExe" Guid="{19400105-A137-4138-A192-D502DA9ABE3B}" >
<File Id="fExe" Source="SimpleMsgBox.exe" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>
実行すると、ダイアログボックスだけが表示され、コマンドプロンプトが表示されないことがわかります。利用方法の詳細は公式マニュアルを読んでいただくとして、ここでは要点だけ説明します。
気を付けなければいけないのは、コマンドライン文字列を設定するsetCmdLine
アクションです。WiX ToolsetのWixQuietExec
アクションを利用する場合、immediate
で実行する場合はWixQuietExecCmdLine
プロパティを、deferred
で実行する場合はWixQuietExec
アクションのId
と同名のプロパティを利用する必要があります。この例では、deferred
で実行するため、aShowMsg
プロパティにコマンドライン文字列を格納しています。
また、この例ではコンポーネントでインストールしたファイルをカスタムアクションで実行します。インストールするファイルのインストール先のフルパスは、インストール実行時にならないと決まりません。そのため、コマンドライン文字列も実行時に取得してプロパティに設定する必要があります。このような場合は、カスタムアクションを使ってプロパティにコマンドライン文字列を設定します。この例では、インストール後のファイルへのフルパスを[#fExe]
のようにFileエレメントのId
を使って取得しています。こういった記法は、こちらにまとめられています。
既存のプログラムを利用する
OSが最初から持っているコマンドや他の製品がインストールしたプログラムを、これまで説明した方法で実行する場合は、実行タイミングを気にせず利用できます。しかし、Windows Installerの仕組み上、コマンドが標準出力に吐き出した文字列を、MSIの中で直接利用することができません1。このようなことがしたい場合は、DLL形式のカスタムアクションをC言語2で作成する必要があります。
外部コマンドの戻り値の利用
外部コマンドの実行結果をエラーにしたいときは、カスタムアクションの仕様に合わせて、エラーコードを戻すようにすれば処理されます。
SimpleMsgBox.exe
がエラーメッセージを返すように変更してみます。エラーコードのマクロはmsi.h
に定義されているので、includeします。
#pragma comment(lib, "User32.lib")
#include <windows.h>
#include <stdio.h>
#include <msi.h>
int main(int argc, char *argv[]){
if(argc > 1){
printf("Message : %s\n", argv[1]);
MessageBox(0, argv[1], "SimpleMsgBox", MB_OK);
}
return ERROR_INSTALL_FAILURE;
}
これを使ってインストーラーを作成すると、インストーラーがエラー終了します。
インストール後にカスタムアクションのConditionを変更する
「あぁ、ここをこういうConditionにしておけばアンインストールできたのに」と思っても、あとの祭り。でも大丈夫。ここでは、カスタムアクションのConditionの設定ミスでアンインストールできなくなった製品をアンインストールする方法を説明します3。
ここでは、例として「インストールしたファイルをカスタムアクションで実行する」で使用したMSI(Part15_03)を改造して、アンインストールできないインストーラーを作成します。
<Custom Action="aShowMsg" After="InstallFiles">NOT Installed</Custom>
となっているところを
<Custom Action="aShowMsg" After="InstallFiles"></Custom>
と変更して、エラーによりアンインストールできないことを確認します。アンインストールすると、下図のようなエラーメッセージが表示され、ロールバックされるはずです。
アンインストールで使用されるMSIの場所を探す
インストールを実行すると、Windows Installerは使用したMSIファイルを特別な場所に特別なファイル名でキャッシュします4。Windowsの設定やコントロールパネルからアンインストールを実行すると、このキャッシュされたMSIファイルが使用されます。キャッシュされた自分のMSIの場所は、下記のようにレジストリに書かれています。
キー:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\55EA7D31DB37BF940808010995542064\InstallProperties
名前:LocalPackage
値:C:\Windows\Installer\3661f6f5.msi
キー名の途中にあるS-1-5-18
はインストールを実行したユーザーを示していて、管理者権限のついた第1ユーザーでインストールを実行すると、必ずこの名前になります。また、55EA7D31DB37BF940808010995542064
は、packed GUIDという形式でコード化されたインストーラーの製品コードです。コード化と言っても、下図のようにGUIDの各文字を並べ替えただけのものです。図の上がGUID、下がPackd GUIDです(変換用のスクリプトがここで紹介されています)。MSIのファイル名3661f6f5.msi
はランダムに命名されます。実験すると異なるファイル名が格納されていることでしょう。
OrcaでカスタムアクションのConditionを書き換える
レジストリに書かれているC:\Windows\Installer
のフォルダは隠しフォルダであり、また、書き換えには管理者権限が必要です。Orcaを直接管理者として実行し、「ファイルを開く」ダイアログボックスで、ファイル名欄にフルパスでファイル名を指定すれば、開くのは簡単です。
開いたら、InstallExecuteSequence
のaShowMsg
アクションのConditionを確認します。空欄になっているはずなのでNOT Installed
と入力して保存します。
これでアンインストール可能になりました。