WiX Toolset

3件の投稿

WiX Toolset – Bootstrapper

WiXは少しお休みして、現代史の勉強をしようと思っていたのですが、Bootstrapper Projectを習得したいという欲求の方が強く、結局WiXの勉強を継続しました。

完璧ではないのですが、概要は理解したのでご報告します。

Bootstrapper というのは、インストール・プロジェクトの途中で、別のインストールプログラムをインストールしようというものです。

現在、Visual StudioではWixをベースにしたBootstrapper Projectをサポートしています。私もこのBootstrapper Projectを使いました。

さて、今Accessを必要とするアプリケーション・プログラムがあるとします。
すなわち、このプログラムが動作するためには、ターゲットコンピュータに予めAccessがインストールされていなければいけません。

もし、ターゲットコンピュータにAccessがインストールされていなければ、所定の場所(WEB)から無料のAccessRuntimeをダウンロードして、ターゲットコンピュータにインストールします。

今、アプリケーションのインストールプログラム(MyAppli.ms)は完成しているとして、ここでやるべきことは、
1. ターゲットコンピュータに必要なAccessがインストールされているかどうか調べ
2. なければAccessruntimeをダウンロードし、インストールする
3. Accessruntimeのインストールが成功する、あるいはもともとAccessがインストールされていれば、
4. 自分のアプリケーションをインストールする。
というものです。

Visual Studioを起動して、
プロジェクト・テンプレートのWindows Installer XMLからBootstrapper Projectを選択します。
その中心部分(といっても僅かですが)は以下のようなものです。

	<Bundle Name="Bootstrapper1" Version="1.0.0.0" Manufacturer="" UpgradeCode="98647ef2-65fe-4702-8b60-f59824fb2642">
		<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense" />

		<Chain>
			<!-- TODO: Define the list of chained packages. -->
			<!-- <MsiPackage SourceFile="pathtoyour.msi" /> -->
		</Chain>
	</Bundle>

この<Chain>要素にExeあるいはMsi形式のセットアッププログラムを記述します。
書かれた順番にインストールが実行されます。

AccessRuntimeExe形式なのでExePackage 要素を使って、またMyAppliMsi形式なのでMsiPackage 要素を使って、以下のように書きます。

	
      <ExePackage Id="AccessRuntime2013"
                  DownloadUrl="http://www.aa.bb.cc/XXX/AccessRuntime_x86_ja-jp.exe"
                  SourceFile="AccessRuntime_x86_ja-jp.exe"
                  Compressed="no" Permanent="yes"
                  InstallCondition="NOT VersionNT64"
                  DetectCondition="NOT VersionNT64 AND (LocalServer3214 OR LocalServer3215)" />

      <RollbackBoundary />

      <MsiPackage SourceFile="MyAppli.msi" DisplayInternalUI="yes"
                  Permanent="yes" Visible="yes"/>

ここでMyAppli.msiのインストールはあまり問題がないと思います。

実は、WiXのBootstrapper Projectには問題があって、Chain要素に書かれたプログラムをインストールするだけでなく、自分自身もターゲットコンピュータにインストールするのです。

すなわち、上の例でいえばBootstrapper1もインストールされて、Windowsの「プログラムと機能」画面に、Bootstrapper1プログラムも表示されます。

そして悪いことに、Bootstrapper1をアンインストールすると、上の例で、AccessRuntimeMyAppliもアンインストールされてしまいます。

これを避けるため、ExePackage 要素とMsiPackage 要素の属性で、Permanent=”yes” を宣言しておきます。

これによって、Bootstrapper1をアンインストールしても、Chain要素のプログラムがアンインストールされることはありません(の筈です)。

WiX4では修正されるようですが、期待しています。

また、<RollbackBoundary />はこれに続くインストールで、不具合が起こってもそれ以前の部分は、Rollbackしないことを指示しています。

すなわち、MyAppliでインストールが中断されても、AccessRuntimeのRollbackはしない指示です。

 

さて少し厄介なのは、Accessがターゲットコンピュータにすでにインストールされているかどうかを調べる部分です。

WiXでは、ターゲットコンピュータのレジストリを調べる機能が用意されています。

Microsoft Officeには、ProgIDが付けられています。

例えば、Accessであれば、Access.Application.14のようです。ProgIDは製品版でもRuntimeでも同じです。

Access2010のProgIDAccess.Application.14、Access2013のProgIDAccess.Application.15です。

またOffice製品は、CLSIDもつけられていて、このCLSIDからターゲットコンピュータのどこにAccessがインストールされているを知ることができます。

以下にサンプルを示します。(後日記:以下の部分を<BootstrapperApplicationRef>要素の直後に挿入します。多分)

    <util:RegistrySearch
      Variable="varClsId3214"
      Root="HKLM"
      Key="SOFTWAREClassesAccess.Application.14CLSID"
      Result="value"/>
    <util:RegistrySearch
      Variable="LocalServer3214"
      Root="HKLM"
      Key="SOFTWAREClassesCLSID[varClsId3214]LocalServer32"
      Result="exists"/>

最初のRegistrySearchAccess.Application.14CLSIDの値を求め、この値を変数varClsId3214にセットし、次のRegistrySearchで、varClsId3214をキーとして、LocalServer32(当該ソフトのインストール先)の存在を確認します。

LocalServer3214の値が”1″ならAccessがインストールされているし、”0″ならインストールされていません。

この値は変数LocalServer3214にセットされます。

したがって、LocalServer3214が”1″なら、Accessインストールを飛ばしてMyAppliをインストールし、”0″ならAccessがインストールされていないので、
ExePackage 要素のDownloadUrl要素に示されたURLから、AccessRuntime_x86_ja-jp.exeをダウンロードし、ターゲットコンピュータにインストールしてから、MyAppliのインストールをします。

以上、WiXについて私が当初目標にしていたところまで勉強を進めました。
WiXで唯一残念なのは、上で書いたようにBootstrapper Projekutで自分自身をインストールすることですが、多分これは次のバージョンV4で修正してくれると思います。

それも間もなくリリースしてくれそうなので、期待しています。

最後に一つだけ追加します。

自分が開発したプログラムの依存ライブラリーも、ターゲットコンピュータにコピーしなければいけませんが、これはVSの参照設定を見ればいいと思っていました。

一方、VS2010では簡単なセットアップ・プロジェクト構築ができますが、ここには依存ライブラリーを収集する機能があります。

ところが、VSの参照ライブラリーとVS2010の依存ライブラリーが少し違います。
どちらがいいのか分かりませんが、VS2010のセットアップ・プロジェクトなら間違いないかなと思っています。

さて、WiXでのセットアッププログラムの作成をご紹介してきました。
一通り勉強してみて、WiXで大抵のことはできる、InstallShieldInstallAwareを使う必要はないと確信しました。

ただし、以前ご紹介した参考書は役に立ちましたが、特にBootstrapの記述が十分ではないし、他にもドキュメントが少ないので、完璧に理解したというわけにはいきません。

とはいえ、WiXは私自身長年習得したいと思っていた技術でしたので、今回一通り勉強できて、また「使える」という感想を持つことができて、ほっとしています。

細かいことは書かないで来ました。
細かいところまで解説する知識がないし、エネルギーがなかったからですが、もう少し勉強して、自信が持てて、更に気分が向いたら、別の場所でWiXの解説をしたいと思います。

WiX Toolset – CustomActionの組込

ユーザにプロダクトキーの入力を求め、正当性を検査するダイアログUserRegistrationDlgを作成します。

出来上がりは次のような画面です。

コードは次の通りです。プログラミングの経験がある人は、何をしているか見当がつくと思います。ただし、InvalidPidDlgは不正入力があったときの画面です。

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
 <Fragment>
  <!-- TODO: Put your code here. -->
    <UI>
      <Dialog Id="UserRegistrationDlg" Width="370" Height="270" Title="[ProductName] [Setup]" NoMinimize="yes">
        <Control Id="NameLabel" Type="Text" X="45" Y="73" Width="100" Height="15" TabSkip="no" Text="ユーザー名(&amp;U):" />
        <Control Id="NameEdit" Type="Edit" X="45" Y="85" Width="220" Height="18" Property="USERNAME" Text="{80}" />
        <Control Id="OrganizationLabel" Type="Text" X="45" Y="110" Width="100" Height="15" TabSkip="no" Text="会社名(&amp;O):" />
        <Control Id="OrganizationEdit" Type="Edit" X="45" Y="122" Width="220" Height="18" Property="COMPANYNAME" Text="{80}" />
        <Control Id="CDKeyLabel" Type="Text" X="45" Y="147" Width="50" Height="10" TabSkip="no">
          <Text>CD キー(&amp;K)</Text>
        </Control>
        <Control Id="CDKeyEdit" Type="MaskedEdit" X="45" Y="159" Width="250" Height="16" Property="PIDKEY" Text="[PIDTemplate]" />
        <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="戻る(&amp;B)">
          <Publish Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
        </Control>
        <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="次へ(&amp;N)">
          <Publish Event="DoAction" Value="CheckingPID" Order="1">1</Publish>
          <Publish Event="SpawnWaitDialog" Value="InvalidPidDlg" Order="2"><![CDATA[PIDACCEPTED = "1"]]></Publish>
          <Publish Event="NewDialog" Value="SetupTypeDlg" Order="3"><![CDATA[PIDACCEPTED = "1"]]></Publish>
        </Control>
        <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="キャンセル">
          <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
        </Control>
        <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="WixUI_Bmp_Banner" />
        <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes">
          <Text>あなたのユーザー情報を入力して下さい。</Text>
        </Control>
        <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
        <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes">
          <Text>{WixUI_Font_Title}ユーザー情報</Text>
        </Control>
        <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
      </Dialog>
      <Dialog Id="InvalidPidDlg" Width="260" Height="85" Title="[ProductName] [Setup]" NoMinimize="yes">
        <Control Id="Icon" Type="Icon" X="15" Y="15" Width="24" Height="24"
                 ToolTip="Information icon" FixedSize="yes" IconSize="32" Text="Exclam.ico" />
        <Control Id="Return" Type="PushButton" X="100" Y="57" Width="56" Height="17"
                 Default="yes" Cancel="yes" Text="戻る(&amp;R)">
          <Publish Event="EndDialog" Value="Return">1</Publish>
        </Control>
        <Control Id="Text" Type="Text" X="48" Y="15" Width="194" Height="30" TabSkip="no">
          <Text>入力されたユーザー・キーは無効です。弊社発行のキーを入力してください。</Text>
        </Control>
      </Dialog>
    </UI>
  </Fragment>
</Wix>

ここで注目していただきたいのは、15行目の[Back]PushButtonと18行目の[Next]PushButtonです。これらの[Control]要素の中にある [Publish]要素は、例えばBackコントロールでいえば、このボタンを押せば、新しいダイアログLicenseAgreementDlgに遷移しろという意味です。

[Next]ボタンを押したら、まず前回ご説明したCheckingPIDを実行しろ、その結果[PIDACCEPTED = 1](キー正常)でなければInvalidPidDlgに遷移しろ、[PIDACCEPTED = 1]ならばSetupTypeDlg画面に遷移しろ、ということになります。

20行目、SpawnWaitDialogは条件が「偽」の場合、Value=”InvalidPidDlg”に遷移しろ、という命令です。注意してください。

さて、このダイアログ・コードをメインプログラムに組み込む方法は二つあります。

一つは本体に対してライブラリー・プロジェクトを作成する方法です。VSのWindows Installer XMLからSetup Library Projectを選択して新規のプロジェクトを作成し、メインプロジェクトからは、他のライブラリー同様参照設定して使用します。

もう一つは、単純にメインプロジェクトに、WiXファイルを新規に追加して、メインプロジェクトの一ファイルとして開発します。

コードは全く同じですから、セットアップ・プロジェクトが大規模かどうか等で選択すればいいと思います。

さて、カスタムアクションと特注のダイアログをどのようにメインコードに組み込むか。

最初はカスタムアクションの定義です。

前々回ご紹介したカスタムアクションをビルドすると、ライブラリーCheckPID.CA.dllが得られます。メイン・プログラムの[Product]要素の直下に以下の文を挿入します。

<Binary Id="MyCustomActionDLL" SourceFile=".CheckPID.CA.dll" />
<CustomAction Id="CheckingPID" BinaryKey="MyCustomActionDLL" DllEntry="CheckPID" Execute="immediate" Return="check" />

少しご説明します。まず[Binary]要素を定義します。SourceFileはセットアップ・プロジェクト開発環境にあるカスタム・アクションのパス付ファイル名を指定します。

次はCustomAction の定義です。BinaryKeyはBinary要素のIdです。DllEntryはライブラリーCheckPID.CA.dllのエントリー関数名です。CustomActionのIdは、上にご紹介したUserRegistrationDlgのNextボタンで起動される、セットアッププロジェクトで使われるCustomAction の一意名です。

次はダイアログの組み込みです。同じくメイン・プログラムのProduct要素の直下に以下の文を挿入します。

まず、ユーザ定義のUI・MyWixUI_Mondoを宣言します。

この中で、WixUI_MondoとWixUI_ErrorProgressTextへの参照を宣言。続いて自作のUserRegistrationDlgを参照。次の「LicenseAgreementDlgの[Next]ボタンが押されたら、新しいダイアログUserRegistrationDlgに遷移しろ」、と「SetupTypeDlgダイアログでBackが押されたら、UserRegistrationDlgダイアログに遷移しろ」
を付け加えます。

    <UI Id="MyWixUI_Mondo">
      <UIRef Id="WixUI_Mondo" />
      <UIRef Id="WixUI_ErrorProgressText" />
      <DialogRef Id="UserRegistrationDlg" />
      <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="UserRegistrationDlg" Order="2">
        LicenseAccepted = "1"
      </Publish>
      <Publish Dialog="SetupTypeDlg" Control="Back" Event="NewDialog" Value="UserRegistrationDlg">
        1
      </Publish>
    </UI>

そのほか、アイコン指定や、グローバル変数(WiXではプロパティといいます)を定義します。以下、カスタムアクション関連のコードを再掲載します。

この中で、PIDTemplateはプロダクトキー入力用MaskedEditのテンプレートで、ここに入力された文字列からPIDKEYの値を取得します。

一番下の[WixVariable Id=”WixUILicenseRtf”] は使用許諾書の設定、
[WixVariable Id=”WixUIBannerBmp” ]はセットアップ画面に使う画像を指定しています。

<Binary Id="Exclam.ico" SourceFile="$(var.ImageFilesPath)warning.ico" />
<Property Id="PIDTemplate"><![CDATA[12345<^^^^ ^^^^ ^^^^ ^^^^>@@@@@]]></Property>
<Property Id="PIDACCEPTED" Value="123" />
<Binary Id="MyCustomActionDLL" SourceFile=".CheckPID.CA.dll" />
<CustomAction Id="CheckingPID" BinaryKey="MyCustomActionDLL" DllEntry="CheckPID" Execute="immediate" Return="check" />
<UI Id="MyWixUI_Mondo">
  <UIRef Id="WixUI_Mondo" />
  <UIRef Id="WixUI_ErrorProgressText" />
  <DialogRef Id="UserRegistrationDlg" />
  <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="UserRegistrationDlg" Order="2">
   LicenseAccepted = "1"
  </Publish>
  <Publish Dialog="SetupTypeDlg" Control="Back" Event="NewDialog" Value="UserRegistrationDlg">
   1
  </Publish>
</UI>
<WixVariable Id="WixUILicenseRtf" Value=".使用許諾契約書.rtf" />
<WixVariable Id="WixUIBannerBmp" Value="$(var.ImageFilesPath)myImage.bmp" />

WiX Toolset – CustomAction

インストールの途中でプロダクトキーの入力を求め、正当なインストールかどうか調べるプログラムを作成します。

ソフトメーカーがどのようなプロダクトキー作成アルゴリズムを使っているのか、日本語の情報を見つけることができませんでしたが、Code Projectのサイトにそれらしいものがあります(私自身内容を確認していません)。

今はプロダクトキーの作成方法が論点ではないので、こちらは簡単にして、WiXでCustomActionをどのように作成して、どのように組み込むかをご説明します。

CustomActionは様々な方法で作成できます。PACKT本に詳しく書かれていますが、今回はVBでDllを作成します。

アプリケーションの注文が来たら、ユーザ名を使ったプロダクトキーを作成・返送し、ユーザがソフトをインストールするときに、名前とキーを使ってインストールが正規かどうかチェックします。

入力用のダイアログ画面ではユーザ名、会社名、プロダクトキーの入力用TextBoxがあって、ユーザが入力した値が、USERNAME、COMPANYNAME、PIDKEYグローバル・プロパティにセットされます。

これをチェックする簡単なアクションプログラムを下に示します。セットアップ本体とCustomActionとは、sessionを使って簡単にデータの授受ができます。

プロダクトキーはにユーザ名を16桁のHexで表示したものです。Hexアレイの順番を入れ替えるとか、ダミーの文字を入れるとかするともう少しもっともらしくなります。

VBのプロジェクトを以下のように作成します。Visual Studio 2013で「新しいプロジェクト」をクリック、Windows Installer XMLタブから、VB Custom Action Projectをクリックします。

自動生成されたメインのファイル(CustomAction.vb)を以下のように変更し、ビルドするとCheckPID.CA.dllファイルが作成されます

Imports System.IO
Imports System.Text
Imports Microsoft.Deployment.WindowsInstaller
Public Class CustomActions2
    <CustomAction()> _
    Public Shared Function CheckPID(ByVal session As Session) As ActionResult
        Dim strName As String = session("USERNAME")
        Dim strPid As String = session("PIDKEY")
        Dim O_CheckID = New clsProductKey2("Shift_JIS")
        session("PIDACCEPTED") = O_CheckID.CheckKey(strName, strPid)
        Return ActionResult.Success
    End Function
End Class
Public Class clsProductKey2
    Private enc As Encoding
    Private Const strPpack As String = "m6bk9UoUXzgTvmwv"
    Public Sub New(ByVal encStr As String)
        enc = Encoding.GetEncoding(encStr)
    End Sub
    Public Function CheckKey(P_Name As String, P_ProductKey As String) As String
        Dim strFromName As String = MakeKey(P_Name)
        If strFromName.Equals(DelDelimiter(P_ProductKey, " ")) _
            Or strFromName.Equals(DelDelimiter(P_ProductKey, "-")) Then
            Return "1"
        Else
            Return "0"
        End If
    End Function
    Public Function DelDelimiter(p_strHyphen As String, p_strDelimiter As String) As String
        Dim strArray() As String = p_strHyphen.Split(p_strDelimiter)
        Dim strHex As String = ""
        For Each strOneChar As String In strArray
            strHex &= strOneChar
        Next
        Return strHex
    End Function
    Public Function MakeKey(p_Name As String) As String
        Dim strPackedName As String = p_Name & strPpack
        Dim nameBytes() As Byte = enc.GetBytes(strPackedName)
        Dim str As String = ""
        Dim i As Integer
        For i = 0 To nameBytes.Length - 1
            If i < 8 Then
                str &= String.Format("{0:X2}", nameBytes(i))
            Else
                Exit For
            End If
        Next
        Return str
    End Function
End Class

次回は、プロダクトキー入力ダイアログと、これらをどのようにセットアッププログラムに組み込むか、ご説明いたします。