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

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

## ソースを自動生成すると困ること
フォルダをパースしてインストーラーのソースを得る場合、便利ではあるのですがデメリットもあります。自動的にパースして作ったコンポーネントのGUIDやComponentエレメント、FileエレメントのIdなども自動で割り振られるので、例えばインストールするファイルのショートカットを作るなど、ファイルやコンポーネントの情報を使用するのが非常に面倒な作業になります。このような場合は、

- ビルドするたびにパースするのではなく、最初にパースした結果をソースに直接組み込み手動でメンテナンスする
- パースしたくないファイルをターゲットのフォルダから取り除いて、取り除いたファイルだけ手作業で追加する
- スクリプトでパース結果からコンポーネントやファイルのIdを取得する

などの手段を採ることになります。今回は、これらの方法について説明していきます。

このほかにも、フォルダをパースすることで毎回新たにコンポーネントが作り直され、コンポーネントの仕様（特にGUID）が変わってしまいます。その結果、

- マイナーアップグレード版のインストーラーを作る際には、コンポーネントの仕様を変えられない
- 他製品と共有するコンポーネントも仕様を変えられない

といった理由で、インストーラーのビルド方法にひと工夫加える必要があります。前者は、マイナーアップグレード版作成時には前回自動生成したソースをそのまま使用できるよう、ビルド環境を作成しておく必要があります。マイナーアップグレード版は作らない、と決めてしまえば考える必要はなくなります。後者は、共有するコンポーネントを自動生成の対象から外して、手作業でソースに追加する必要があります。

## 最初に１回だけパースする

[前回](https://qiita.com/tohshima/items/f8b0fa5a94189953b050)紹介したバッチファイルで`.wxs`ファイルを生成し、コピー＆ペーストでコンポーネントの定義を`Product.wxs`等に移動し、整えていく作業を行います。特に難しい話はありません。もちろん、前回紹介したように、生成された`.wxs`ファイルをそのまま使っても問題ありませんが、メンテナンス性や可読性を考えて最適な方法を選択してください。

## パースするソースから自動生成したくないファイルを取り除く

パースさせたくないファイルがあるなら、おそらく一番簡単な方法はパースするフォルダからパースさせたくないファイルを取り除いておくことでしょう。そのためには、パースさせたくないファイル以外を一旦仮の場所に一式コピーしてしまい、これをパースさせます。実現方法はいくつかあると思いますが、メンテナンス性と処理時間のトレードオフで方法を選択するとよいと思います。バッチフィルでフォルダ構造を保って複数のファイルをコピーする場合は、xcopyコマンドを使うと簡単です。xcopyコマンドはコピーを除外するファイルを指定する場合、除外するファイルをリストアップしたテキストファイルを別途用意します。以下のバッチファイルの引数にコピー元のフォルダを指定すると、バッチファイルを置いたフォルダに、`msi.chm`と`WiX.chm`以外のファイルをコピーし、これをもとに`.wxs`ファイルを生成します。

```bat:parseDirectory.bat
rem コピーを除外するファイルのリスト：ファイル名をスペースで区切って並べる（ワイルドカード使用可能）
set EXCLUDE_FILES=msi.chm WiX.chm
rem 出力するwxsファイルの名前
set WXS_NAME=GeneratedComponents
rem xcopyコマンドに与えるコピーを除外するファイルのリストを格納するファイルの名称
set EXCLUDE_LIST=excludeList.txt
rem バッチファイルの引数に指定したパスからコピー元のディレクトリ名を取得する
set TARGET_DIR_NAME=%~nx1
rem バッチファイルを置いたフォルダパスを取得する
set HOME_DIR=%~dp0
rem バッチファイルを置いたフォルダに移動
cd /d "%HOME_DIR%"

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

rem 前回のビルドで使用したフォルダが残っていたら一旦削除する
rmdir /s /q "%TARGET_DIR_NAME%" 2>nul
rem コピーを除外するファイルのリストを生成する
pushd %1
dir /s /b %EXCLUDE_FILES% > "%HOME_DIR%%EXCLUDE_LIST%"
popd
rem プロジェクトのフォルダにパースするフォルダをコピーする
mkdir "%TARGET_DIR_NAME%"
xcopy %1 "%TARGET_DIR_NAME%" /E /EXCLUDE:%EXCLUDE_LIST%

rem heatコマンドを実行する
"%WIX%bin\heat" dir "%TARGET_DIR_NAME%" -var var.SOURCE_DIR -cg GeneratedComponents -dr INSTALLDIR -nologo -sw 5150 -gg -ke -o %WXS_NAME%.wxs
pause
rem heatコマンドの戻り値をバッチファイルの戻り値として返す
exit /b %errorlevel%
```

このバッチファイルでは、バッチファイル内で除外するファイルを指定したり、ワイルドカード（*や?を使って複数のファイルを指定する仕組み）を指定できるようにするために、少し面倒な方法で`excludeList.txt`を生成していますが、この除外用のファイルを最初から別途用意しておくならもっとシンプルにすることが可能です。

パースから除外したファイルは、別途手作業でコンポーネントを作成して追加しておきます。

## パースしたソースから指定したファイルIdを取得する

パースするディレクトリのファイル数が数千個になると、上記の方法ではコピーに掛かる時間がそれなりに増えてきます。インストーラー実装の初期段階で試行錯誤を繰り返していると、これが苦痛になってきます。そこで、すべてのファイルをパースした後、指定したファイルのFileエレメントのIdをプログラムで取得する方法を紹介します。慣れた言語があればそれを使うのが効率的ですが、別の人が作業を引き継ぐ可能性があるなら、あらたにソフトウェアを導入する必要がない方が面倒がありません。私は、バッチファイルとWindows Scripting Host(WSH)の組み合わせに慣れている[^1]ので、これらを使って説明を進めます。

パースすることで得られた`.wxs`ファイルはXMLファイルなので、WSHからMSXMLの機能を呼び出して処理します。WSHもMSXMLも最初からWindowsに入っているので、利用するために新たに何かをインストールする必要はありません。マイクロソフトのドキュメントは[MSXML - DOM](https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms766487(v=vs.85))のあたりを読みながら実装します。

以下のコードでは、引数に与えた`.wxs`ファイルから`WiX.chm`ファイルのFileエレメントのIdと所属するComponentエレメントのIdをインクルード用のファイル`AdditionalDefine.wxi`に書き出します。

```javascript:makeWxiFile.js
// 実行方法：cscript //Nologo makeWxiFile.js [.wxsファイルへのパス] 
// 引数チェック
if (WScript.Arguments.length == 0) {
    WScript.echo("引数にパースする.wxsファイルを指定する必要があります。");
    WScript.quit(1);
}
// 初期化
var outFileName = "AdditionalDefine.wxi";   // Product.wxs用インクルードファイル名
var inFileName = WScript.Arguments(0);
var ForWriting = 2;
var chmCompId = "";
var chmFileId = "";
var i = 0;
// オブジェクト生成
var dom = new ActiveXObject("Msxml2.DOMDocument");
var fso = new ActiveXObject("Scripting.FileSystemObject");
// 同期化
dom.async = false;
// パース
dom.load(inFileName);
var root = dom.documentElement;
// Fileエレメントのコレクションを得る
var fElements = root.getElementsByTagName("File");
for (i = 0; i < fElements.length; i++) {
    // WiX.chmが属するComponent IDとFile IDを取り出す
    if (/WiX.chm/.test(fElements[i].getAttribute("Source"))) {
        chmFileId = fElements[i].getAttribute("Id");
        chmCompId = fElements[i].parentNode.getAttribute("Id");
        WScript.echo("ChmFileId:"+chmFileId+"\tChmCompId:"+chmCompId);
        break;
    }
}
// インクルードファイルに書き出す
var oFp = fso.OpenTextFile(outFileName, ForWriting, true);
oFp.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
oFp.WriteLine("<Include>");
oFp.WriteLine("\t<?define ChmFileId = \"" + chmFileId + "\"?>");
oFp.WriteLine("\t<?define ChmCompId = \"" + chmCompId + "\"?>");
oFp.WriteLine("</Include>");
oFp.Close();
WScript.quit(0);
```

`parseDirectory.bat`をターゲットのフォルダを直接スキャンするよう変更し、`makeWxiFile.js`を呼び出します。

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

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

rem heatコマンドを実行する
"%WIX%bin\heat" dir %1 -var var.SOURCE_DIR -cg GeneratedComponents -dr INSTALLDIR -nologo -sw 5150 -gg -ke -o %WXS_NAME%
@echo off
set RESULTCODE=%errorlevel%
if %RESULTCODE% neq 0 (
echo Error : heatコマンドが失敗しました。
pause
exit /b %RESULTCODE%
)
@echo on

rem インクルードファイルを作成する
cscript //Nologo makeWxiFile.js %WXS_NAME%
rem makeWxiFile.jsの戻り値をバッチファイルの戻り値として返す
pause
exit /b %errorlevel%
```

引数に`WiX Toolset v3.11`のフォルダを与えて実行すると、例えば下記のような出力を得ることができます。`fil61C6139B62355C43D22BBFBBFCA63777`のような部分がビルドごとにランダムな値に変更されます。ここにdefineされた値が、`WiX.chm`ファイルのFileエレメントのIdと所属するComponentエレメントのIdになります。

```xml:AdditionalDefine.wxi
<?xml version="1.0" encoding="utf-8"?>
<Include>
	<?define ChmFileId = "fil61C6139B62355C43D22BBFBBFCA63777"?>
	<?define ChmCompId = "cmp396AD52D18AAA6801807E47BD2121C73"?>
</Include>
```

これを使ってスタートメニューに`WiX.chm`へのリンクを作ってみます。ここで注目すべきところは、先頭で`AdditionalDefine.wxi`をインクルードしているところ。そしてShortcutエレメントのTarget属性で`ChmFileId`を指定しているところです。

```xml:Product.wxs
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <?include AdditionalDefine.wxi ?>
	<Product Id="209BE79A-F1C4-4614-9B77-020037AF94B1" Name="Part28_01" Language="1033" Version="1.0.0" Manufacturer="tohshima" UpgradeCode="15c76093-8369-4379-9fdb-20227bad0023">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

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

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

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLDIR" Name="Part28_01" />
			</Directory>
			<Directory Id="ProgramMenuFolder">
                <Directory Id="ProductMenuFolder" Name="Part28_01"/>
            </Directory>
		</Directory>
	</Fragment>

	<Fragment>
		<ComponentGroup Id="ProductComponents">
            <Component Id="cChmLnk" Guid="{7E01C5CF-6F01-4386-AB01-5B86003BD2C8}" Directory="ProductMenuFolder">
                <RegistryKey Root="HKCU" Key="Software\Part28_01" Action="createAndRemoveOnUninstall">
                    <RegistryValue Type="string" Name="forShrtCut" Value="1" KeyPath="yes"/>
                </RegistryKey>
                <Shortcut Id="sChmLnk" Name="WiX.chm" Target="[#$(var.ChmFileId)]"/>
				<RemoveFolder Id="DeleteShortcutFolder" Directory="ProductMenuFolder" On="uninstall"/>
            </Component>
		</ComponentGroup>
	</Fragment>
</Wix>
```

[前回](https://qiita.com/tohshima/items/f8b0fa5a94189953b050)の設定にならい、プロジェクトにGeneratedComponents.wxsを加え、プロジェクトのプロパティで`SOURCE_DIR`を定義し、ビルド前にparseDirectory.batを実行するようにしておけば、インストーラーをビルドできるようになります。

[^1]: イマドキの開発者だと、PowerShellとかになるんでしょうか。

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