Windows Installer手引書 Part.29 コマンドを使った作業

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

## コマンドを使った作業

今回は、コマンドを使った作業を２つ紹介します。一つは、Vidsual Studioで作ったプロジェクトファイルをコマンドラインでビルドする方法。もう一つは、ComponentエレメントのGUIDを一気に差し替える方法です。

## Visual Studioのプロジェクトファイルをコマンドラインでビルドする

これまでの記事で「Visual Studioを使ってWiX Toolset用のソースを書き、Visual Studioでビルドする」という手段でインストーラーを作成してきました。これは、Visual Studioがソースを書く環境として優れており、容易に成果物を得られる仕組みだからです。しかしいつかはソースが完成し、「定期的にビルドするだけ」というときが訪れます。このタイミングになると、「夜間定期的にバッチファイルでビルドする」とか「複数のツールを組み合わせて、プログラムのビルドからインストーラーまで一気にビルドしたい」など、コマンドラインでインストーラーをビルドする要求が出てきます。こんなときに使用できるのが`MSBuild`コマンドです。コマンドラインでWiX Toolsetのソースをビルドするために、直接`Candle`コマンドと`Light`コマンドを使う方法もあります。しかし、Visual Studioのプロジェクトに設定したプロパティをコマンドライン文字列に書き起こすのは、ミスも入りやすく、メンテナンス性も良くありません。Visual Studioを使ってインストーラーを開発するのなら、是非`MSBuild`コマンドを使いましょう。

`MSBuild`コマンドの使い方は非常に簡単です。WiX Toolsetのプロジェクトファイル（拡張子が.wixprojのファイル）またはソリューションファイル（拡張子が.slnのファイル）を置いたディレクトリで下記のコマンドを実行してください。

```
%windir%\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe
```

これだけでビルドできてしまいます。ただし、開発現場ではきちんとプロジェクトファイル、構成、プラットフォームまで指定しておくと、後々の拡張に役立つことがあります。[MSBuild コマンド ライン リファレンス](https://docs.microsoft.com/ja-jp/visualstudio/msbuild/msbuild-command-line-reference?view=vs-2019)を見ると、多くのコマンドラインオプションがありますが、WiX Toolset向けのプロジェクトのソースをビルドするなら、使用するオプションは限られます。`Part29_01`という名前でデフォルト設定で作成したプロジェクトは、下記のバッチファイルでビルドすることができます。

```batchfile:build.bat
set BULDCMD=%windir%\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe
%BULDCMD% Part29_01.wixproj /p:Configuration=Debug /p:Platform=x86
exit /b %errorlevel%
```

さらにMSBuildのコマンドラインオプションに`/t:Rebuild`を加えると、すべてのソースが常にコンパイル/リンクされるようになります。

## ComponentエレメントのGUIDを一気に差し替える

新しいバージョンのインストーラーを作る際、前のバージョンは残しつつ、新しいバージョンを同じPCの別の場所にインストールできるようにする場合があります。インストーラーの仕組み上は別製品のインストーラーとなりますが、前のバージョンのインストーラーのソースを改造して作ることになるでしょう。インストールするファイルを置いたフォルダをビルドごとにパースしてソースを得ている場合は、毎回ComponentエレメントのGUIDが変わるので変更点は限られ、それほど手間ではありません。しかし、手作業で大量のComponentエレメントのGUIDを変更しなければならないとしたら、非常に面倒な作業になります。

基本的にWiX ToolsetのソースファイルのエンコードはUTF-8ですので、[Microsoft ActiveX Data Objects(ADODB)のStreamオブジェクト](https://docs.microsoft.com/ja-jp/sql/ado/reference/ado-api/stream-object-properties-methods-and-events?view=sql-server-ver15)を使って行単位でGUIDを差し替えるスクリプトを作成してみました。ADODBを使用するとウィルス対策ソフトが警告を出すことがあるので、このような場合は許可（場合によっては例外設定）する必要があります。

```javascript:chgGuidForUpg1.js
var inFileName = "GeneratedComponents.wxs";
var outFileName = "GeneratedComponents_chg.wxs";
var adReadLine = -2;    // 行単位で読み込み
var adWriteLine = 1;    // 行単位で書き込み
var adSaveCreateOverWrite = 2;  // ファイルを上書き
// オブジェクト生成
var iFp = new ActiveXObject("ADODB.Stream");
var oFp = new ActiveXObject("ADODB.Stream");
// 入力ファイル初期化
iFp.charset = "utf-8";
iFp.Open();
// 出力ファイル初期化
oFp.charset = "utf-8";
oFp.Open();
iFp.LoadFromFile(inFileName);
do{
    // 1行読み込む
    var readStr = iFp.ReadText(adReadLine)
    // 「Guid="{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx」を検索する
    if(/Guid="\{[0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}/.test(readStr)){
        // 新たにGUIDを取得して、使う部分だけ切り出す
        var newGuid = WScript.CreateObject("Scriptlet.TypeLib").Guid.substr(0, 38);
        // GUIDを新しい値に差し替える
        readStr = readStr.replace(/\{[0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12}\}/, newGuid);
        WScript.Echo(readStr);
    }
    // 1行書き出す
    oFp.WriteText(readStr, adWriteLine);
}while(!iFp.EOS);
oFp.SaveToFile(outFileName, adSaveCreateOverWrite);
iFp.Close();
oFp.Close();
```

例えば、他製品と共有するコンポーネントが存在すると、そのコンポーネントのGUIDは変更ができません。このスクリプトは行単位で処理を進めているため、GUIDを差し替えるかどうかの判定が必要な場合は、ComponentエレメントのGIUD属性と同じ行にあるDirectory属性などで除外するファイルが所属するコンポーネントを検出する必要があります。もし、ファイル名で判別するならComponentエレメントとは別の行にファイル名が書かれているので、ここで紹介した方法を使うのはかなり面倒になります。対策方法として、[前回](https://qiita.com/tohshima/items/e7c44f5f15ff412d9d19)使用した[MSXML-DOM](https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms766487(v=vs.85))を使って処理する方法が考えられます。以下のスクリプトは、MSXML-DOMを使って`msi.chm`、`WiX.chm`をインストールするComponent以外のComponentのGUIDを新たなものに差し替えます。

```javascript:chgGuidForUpg2.js
var inFileName = "GeneratedComponents.wxs";
var outFileName = "GeneratedComponents_chg.wxs";
var i = 0;
var j = 0;
// オブジェクト生成
var dom = new ActiveXObject("Msxml2.DOMDocument");
// 同期化
dom.async = false;
// ファイルの読み込み
dom.load(inFileName);
var root = dom.documentElement;
// Componentエレメントのコレクションを得る
var cElements = root.getElementsByTagName("Component");
for (i = 0; i < cElements.length; i++) {
    // GUIDを差し替えることを示すフラグの初期化：trueで差し替える
    var changeGuid = true;
    // ターゲットのComponent下のFileエレメントのコレクションを得る
    var fElements = cElements[i].getElementsByTagName("File");
    for(j = 0; j < fElements.length; ++j){
        var sourceValue = fElements[j].getAttribute("Source");
        if(/msi.chm/.test(sourceValue)){    // FileエレメントのSource属性にmsi.chmが含まれていたら
            changeGuid = false;             // GUIDを差し替えない
        }
        if(/WiX.chm/.test(sourceValue)){    // FileエレメントのSource属性にWiX.chmが含まれていたら
            changeGuid = false;             // GUIDを差し替えない
        }
    }
    // ComponentエレメントのGUIDを差し替える
    if(changeGuid){
        var newGuid = WScript.CreateObject("Scriptlet.TypeLib").Guid.substr(0, 38);
        cElements[i].setAttribute("Guid", newGuid);
        WScript.echo(cElements[i].xml);
    }
}
// ファイルの書き込み
dom.save(outFileName);
```

以上、ファイルを1行ずつ読み込みテキスト処理する方法（chgGuidForUpg1.js）と、DOM解析させる方法（chgGuidForUpg2.js）の２種類示しましたが、構造的な変化に強い後者の方法の方が優れています。状況に応じて方法を選択してください[^1]。

[^1]: 最初は１行ずつ取得してテキスト処理した方がシンプルで理解し易いものができると思っていましたが、作ってみると意外や意外。DOMを使って処理した方が行数も少なく、確度の高い処理ができると思いました。試しに作ってみるものですね。セキュリティ警告が出る理由を開発メンバーに説明する必要もなくなるので、やはりDOMを使って処理したいですね。

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