Windows Installer手引書 Part.27 フォルダをパースしてインストーラーのソースを作る１

[**前の記事へ**](https://qiita.com/tohshima/items/b04e658ffd7a14fe4e68)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/e7c44f5f15ff412d9d19)

## ソースを自動生成するメリット
ソフトウェア開発では、たいていはインストーラーよりもインストールするプログラムの作成が先に進みます。そうすると、すでにインストールした後のフォルダ、ファイル構造で動くものがすでに存在することがほとんどです。それならば、そのフォルダ、ファイル構造をパースしてインストーラーのソースファイルを作成できれば便利です。また、インストーラーのビルド時に毎回パースするように構成してしまえば、構造が途中で変わったとしても、何も変えずにインストーラーをビルドできます。これは、大きなメリットです。

## ファイル・フォルダ構造をパースしてみる

下記のようなバッチファイルを実行すると、フォルダをパースして.wxsファイルを得ることができます。heatコマンドを呼ぶだけなので1行で書くこともできますが、長くなるので読みやすさやメンテナンス性、再利用性を考え[^1]バッチファイルを示します。エクスプローラから起動して処理経過を確認することを想定し、最後にpauseコマンドで処理を一時停止させていますが、不要であれば削除しても問題ありません。

```batchfile:parseDirectory.bat
rem 出力するwxsファイルの名前
SET WXS_NAME=GeneratedComponents
rem バッチファイルを置いたフォルダに移動
cd /d %~dp0

rem 引数があることを確認
@echo off
if "%~1"=="" (
echo Error : 引数にパースするディレクトリを指定してください。
pause
exit /b 1
)
@echo on

rem heatコマンドを実行する
"%WIX%bin\heat" dir %1 -o %WXS_NAME%.wxs
pause
rem heatコマンドの戻り値をバッチファイルの戻り値として返す
exit /b %errorlevel%
```

ここでは題材として、インストール済みのWiX Toolset V3.11を使います。このバッチファイルの引数に`WiX Toolset v3.11`フォルダをフルパスで指定して実行するか、`parseDirectory.bat`ファイルアイコンに`WiX Toolset v3.11`フォルダアイコンをドロップしてください。この結果、バッチファイルを置いたディレクトリに`GeneratedComponents.wxs`が生成されます。

生成されたwxsファイルの中身について説明する前に、heatコマンド実行時に表示されるwarningについて説明しておきます。

## COMを使ったアプリのインストール

WiX Toolset V3.11のフォルダをheatコマンドでパースすると、下記のようなwarningが出力されます。ここでは少し脱線して、なぜこのようなwarningが出力されるのか説明します。

```batchfile
heat.exe : warning HEAT5150 : Could not harvest data from a file that was expected to be a SelfReg DLL: C:\Program Files (x86)\WiX Toolset v3.11\SDK\x64\sfxca.dll. If this file does not support SelfReg you can ignore this warning. Otherwise, this error detail may be helpful to diagnose the failure: Unable to load file: C:\Program Files (x86)\WiX Toolset v3.11\SDK\x64\sfxca.dll, error: 193
```

ここで説明する`COM`は、`Component Object Model`の略称です。COMについて説明を始めると分厚い本が書けるくらいの分量になってしまうので、インストールの観点に絞って説明します。例えば、自分が作ったプログラムの機能を外部のプログラムから利用できるようしたいとき、COMを使うことができます[^2]。この機能を提供する側のプログラムを`COMサーバー`と呼びます。COMサーバーを備えたアプリをインストールする場合は、OSにCOMサーバーを登録する必要があります。COMサーバーの機能を開発する際に採られる方法としては、プログラム自身にCOMサーバーを登録／解除する関数を実装し、DLLの関数として使えるようにする方法がありますが、このようにしないケースもあります。先のheatコマンドには、この関数を使ってCOMサーバーのレジストリへの登録情報を`.wxs`ファイルに書き出す機能があります[^3]。しかし、この関数がないケースでは、heatコマンドがレジストリの登録情報を取得できないので、warningを出力するのです。エラーの文章にSelfRegとあるのは、プログラム自身が持つCOMサーバーを登録／解除する関数（機能）のことを指しています。

COMサーバーの開発の現場では、`regsrv32.exe`を使ってCOMサーバーを登録／解除することが多いです。しかし、Windows Installerを使ったインストーラーでは、`regsrv32.exe`を使わずに、コンポーネントでレジストリに項目をインストールすることが一般的ですし、その方が作成が容易です。プログラムの実装担当者がこのことを知らない場合もあるので、注意が必要です[^4]。

## 使いやすいパース結果を得る方法

先ほど紹介したバッチファイルを実行すると、下記のような結果が得られます（長いですので冒頭だけ）。

```xml:GeneratedComponents.wxs
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <DirectoryRef Id="TARGETDIR">
            <Directory Id="dir680D6D3A60FA0E838DD8D86EC9F251F9" Name="WiX Toolset v3.11" />
        </DirectoryRef>
    </Fragment>
    <Fragment>
        <DirectoryRef Id="dir3EDF548A8992E9B52FC2DF62C00ACE0C">
            <Component Id="cmp004ECA889DC81D45E01381BEE3105208" Guid="PUT-GUID-HERE">
                <File Id="fil0FA0B5790D93B7827A63225E7CEB5CC3" KeyPath="yes" Source="SourceDir\SDK\VS2017\lib\x86\deputil.lib" />
            </Component>
        </DirectoryRef>
    </Fragment>
    <Fragment>
        <DirectoryRef Id="dir9845F6667A187805713595749521658C">
            <Component Id="cmp006C3F278C53AFB3B74475BF65DBE045" Guid="PUT-GUID-HERE">
                <File Id="fil9E7FF0942E65279B060DB4C909FDA5F9" KeyPath="yes" Source="SourceDir\SDK\wixui\WixUI_pt-PT.wxl" />
            </Component>
        </DirectoryRef>
    </Fragment>
.....
```

吐き出されたソースを読むと、最初にトップのディレクトリが定義され、それ以降１つのコンポーネントに１つのファイルが含まれる構造になっています。このまま利用しようとすると、色々と問題があることが分かります。

- コンポーネントのGuidに`PUT-GUID-HERE`と書かれていて、すべてのコンポーネントについて異なるGUIDに置換しないと使えない
- 大量にコンポーネントがあるので、すべてをFeatureに登録するのは困難
- インストール先ディレクトリの親ディレクトリがMSIにとっての最も上位層である`TARGETDIR`なので、融通がきかない
- FileエレメントのSource属性は、heatコマンドに与えた引数のディレクトリからの相対パスになっているので、このままだとソースの置き場所が固定され、使いにくい

これらの問題を解決するために、heatコマンドに下表のコマンドラインオプションを追加します。なにも引数を付けずにheatコマンドを実行すれば、すべてのコマンドラインオプションの説明が表示されるので、内容を確認しておくことをお勧めします。今回はharvesting typeとしてdir(harvest a directory)を使用しましたが、他にも色々なものをインストーラーのソースに変換できます。

|オプション文字列|意味|使用例|
|:--|:--|:--|
|-gg|生成するコンポーネントに新規GUIDを割り当てる。|-gg|
|-cg <ComponentGroupName>|出力されるコンポーネント全体を指定したIdのComponentGroupでくくる。|-cg GeneratedComponents|
|-dr|指定したIdのDirectoryエレメントの下に生成したコンポーネントをインストールする|-dr INSTALLDIR|
|-var|FileエレメントのSource属性の親フォルダとして、指定した名前のWiX変数を使う|-var var.SOURCE_DIR |
|-sw|指定した番号のワーニング出力を抑止する。|-sw 5150|
|-ke|ファイルが置かれていないディレクトリも追加する。|-ke|
|-nologo|heatのバージョンやCopyright情報等の出力を抑止する。|-nologo|

これらを適用すると、`parseDirectory.bat`のheatコマンド部分は下記のようになります。

```batchfile
"%WIX%bin\heat" dir %1 -var var.SOURCE_DIR -cg GeneratedComponents -dr INSTALLDIR -nologo -sw 5150 -gg -ke -o %WXS_NAME%.wxs
```

実行結果は、下記のように変わります。また、`-sw 5150`を指定することで、COM登録に関するWarning出力が抑止されるようになります。

```xml:GeneratedComponents.wxs
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <DirectoryRef Id="INSTALLDIR">
            <Directory Id="dirF71D3590D043097F438252FE7BCC731E" Name="WiX Toolset v3.11" />
        </DirectoryRef>
    </Fragment>
    <Fragment>
        <ComponentGroup Id="GeneratedComponents">
            <Component Id="cmpD649648498B61AC10C20C75FD46289E7" Directory="dirF71D3590D043097F438252FE7BCC731E" Guid="{6314EE94-B461-40AF-A075-9419DE2C35E8}">
                <File Id="fil0CECBB09801A128496C5F251B2DF0832" KeyPath="yes" Source="$(var.SOURCE_DIR)\LICENSE.TXT" />
            </Component>
            <Component Id="cmp1EB3F12AAA75F638860A37F2DFA35D8B" Directory="dir4650948A102C0165909269B119439CE7" Guid="{BBB74027-4655-44C7-AA4E-F0826D7BBC35}">
                <File Id="fil17DBCD51BCD3F2A0435E744D9126F8B3" KeyPath="yes" Source="$(var.SOURCE_DIR)\bin\candle.exe" />
            </Component>
            <Component Id="cmpCCDEF90DC9C373C5FD7827022F373604" Directory="dir4650948A102C0165909269B119439CE7" Guid="{49DF18C6-81B4-4ED5-8858-562D3791F2C3}">
                <File Id="filD532342880D7DD8A5DA6A46264E14AC8" KeyPath="yes" Source="$(var.SOURCE_DIR)\bin\candle.exe.config" />
            </Component>
.....
```

これで大分使いやすくなりました。

## インストーラーのソースに組み込む

これまで説明した作業をVisualStudioで実行し、インストーラーに組み込み手順を説明します。まず、MSIファイルをビルドする前に`parseDirectory.bat`を実行する必要があります。プロジェクトのプロパティを開き、Build EventsページのPre-build Event Command Line欄にコマンドを記述します。このままコマンド文字列をタイプしても良いのですが、Build Eventsタブの[Edit Pre-build...]ボタンを押すと開くダイアログで入力すると、Visual Studioで使用可能な環境変数の一覧が出てくるので便利です。
![Part27_BuildEventsSetting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/37ce8e8e-9399-7597-0c83-bd6b535ab903.png)
Pre-build Event Command Lineダイアログで、下図のように入力します。プロジェクトのフォルダなどシステム既定の変数は、下側のペインにリストアップされるので、`parseDirectory.bat`を置いたフォルダを指定します。バッチファイルの引数に与えたフォルダのパスは、パースするフォルダを入力します。
![Part27_Pre-buildSetting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/bb711ad4-55e0-221b-221c-da37a3b4e872.png)
このバッチファイルが実行されると吐き出される`GeneratedComponents.wxs`では、`$(var.SOURCE_DIR)`フォルダ以下にソースが置かれていることを前提としています。そのため、`SOURCE_DIR`変数にソースを置いたフォルダのパスを設定する必要があります。プロジェクトのプロパティを開き、BuildページのDefine preprocessor variablesで下記のように設定します。ここでは、フォルダパスを「"」(double quote)でくくると正しく動作しないので注意が必要です。
![Part27_DefinePreProcessorVar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/48b21c4c-7a70-7a08-675e-0a3d0787e852.png)


そして、プロジェクトに`GeneratedComponents.wxs`を加えます。
![Part27_SolutionExp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83170/7d741334-4341-9caf-3b33-69c6e95ddd02.png)
`Product.wxs`では、`GeneratedComponents.wxs`を利用するための記述を追加します。FeatureエレメントとDirectoryエレメントに注目してください。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="0F1ECEB9-69EF-4190-A65F-6E18B5BB3788" Name="Part27_01" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="c4b37c46-6812-495c-9d9c-b6bc5f3dd02d">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
		<MediaTemplate />

		<Feature Id="ProductFeature" Title="Part27_01" Level="1">
			<ComponentGroupRef Id="GeneratedComponents" />
		</Feature>
	</Product>

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="PartFolder" Name="Part27_01">
		            <Directory Id="INSTALLDIR" />
                </Directory>
			</Directory>
		</Directory>
	</Fragment>
</Wix>
```

Featureエレメント内には、`GeneratedComponents.wxs`を生成する際にコマンドラインオプションに指定した`GeneratedComponents`へのリファレンスを記入します。また、コマンドラインオプションに指定したインストール先ディレクトリのIdである`INSTALLDIR`でDirectoryエレメントを加えます。この例では、`C:\Program Files(x86)\Part27_01`の下にインストールされるようにしています。カスタムアクションやInstallDirDlgを使ってINSTALLDIRにパスを設定し、インストール先を変更することもできます。

## ビルドシステムのメンテナンス性

ここまでの記事を読まれて、2か所に同じフォルダパス`C:\Program Files (x86)\WiX Toolset v3.11`を設定していることを「気持ち悪！」と思ったアナタ。その感覚は正しいです。今回は、すべてをVisualStudio内で処理すると説明をシンプルにできるため、このような構成をとりました。メンテナンス性をよくするために、独自に工夫してみてください。


[**前の記事へ**](https://qiita.com/tohshima/items/b04e658ffd7a14fe4e68)　　[**目次へ**](https://qiita.com/tohshima/items/dc4c4721d50a2ad70c7a)　　[**次の記事へ**](https://qiita.com/tohshima/items/e7c44f5f15ff412d9d19)

[^1]: ちょっとした実験でもバッチファイルを作成することをお勧めします。過去を振り返るのに役立ったり、人に渡すのにも便利です。また、このバッチファイルのように戻り値にエラーか正常終了したか返すようにしておくと、バッチファイルをコマンドのように使えるようになるので、役に立つこともあります。Windowsのコマンドの返り値は、正常終了時に`0`を、エラー時に`0以外`を返すのが基本ですので、このバッチファイルもそれにならっています。

[^2]: ExcelやWordなどのMS Office製品、InstallShieldなどもCOMを使って機能を公開しているので、外部のプログラムから機能を使用することができます。MS Office製品は、VBScriptをマクロとして実装する機能を持っていますが、これとは別にWindows Scripting Host(WSH)上のVBScriptやJScript(マイクロソフトによるJavaScriptの実装の一つ)を使って、コマンドプロンプトから自動処理することができます。InstallShieldは独自のIDEを備え、その上で編集・ビルドを行うことが一般的ですが、VBScriptでプロジェクトファイルを編集し、ビルドまで行うことも可能なのです。WSHはWindowsに最初から入っているので、どの環境でもすぐに使うことができます。また、実験用にMSIのカスタムアクションを作成するのにも役に立ちます。レガシーな技術ですが簡単に使えるので、是非使いこなしましょう。

[^3]: WiX Toolsetのフォルダをスキャンすると、COMの登録情報の抽出に成功している項目もあるので、読んでみると勉強になります。

[^4]: 頑張って`regsvr32.exe`を使ってインストーラーを実装するなら、インストール、アンインストール、ロールバックの３つのカスタムアクションを作って、シーケンスに挿入することになります。さらに、Featureを使ってCOMサーバーがインストールされるケースとされないケースがあるなら、もっと複雑になります。やりたくないですよね。