TrickPalace
  1. http://www.trickpalace.net/
  2. windows/
  3. installer/

インストーラの自作

information

TrickPalace

ここでは簡単なインストーラを自作する為のノウハウを紹介いたします。

対象とする環境は Windows です。

Wraith the Trickster 全然体系立って書けてないので、適当に必要なとこだけ拾い読みしてください。

Installer SDK

download

downloadここで紹介しているコード等の一部を利用しやすいようにまとめておきましたので必要であればダウンロードしてご利用ください。 ライセンスについてはいつも通り自己責任で好き勝手にしちゃって構わんです、はい。

Wraith the Trickster readme.txt にはこの Installer SDK を利用してインストーラを作成する際の To Do リストを用意していますので、必要に応じてご参照してください。

Microsoft Cabinet SDK

Microsoft Cabinet SDK とは

Microsfot Cabinet SDK は Microsoft が無償で提供している .cab ファイル関連のコマンド、DLL、ドキュメントなどの一式をまとめくれている SDK で、インストーラを自作するにあたって是非とも抑えておきたい SDK ですので 『Microsoft Cabinet SDK について』から入手してください。   

ここではインストーラを作成する為に必要な範囲で Microsoft Cabinet SDK について簡単に紹介いたしますが、より詳細に知りたい場合はドキュメントやサンプルコードなども Microsoft Cabinet SDK に付属していますのでそちらを参照してください。

Wraith the Trickster Microsoft Cabinet SDK とは別に Windows がまだ 16 ビット OS だった古来から存在する compress コマンドとその compress コマンドで圧縮されたファイルを解凍する為の LZOpenFile()/LZInit(), LZState(), CopyLZFile(), LZDone(), LZClose(), LZSeek(), LZRead() といった Windows API 群も存在します。

UNICODE対応について

.cab ファイルフォーマットとしては一応、UTF-8 を利用することで UNICODE ベースのファイル名にも対応していて cabinet.dll もその形で UNICODE 対応しているのですが、.cab ファイル関連のコマンドラインツールは UNICODE 対応ができていないようです。 ですが幸い、一般的にインストールするファイルに UNICODE でなければ正確に表現できない文字を含むファイル名やディレクトリ名を用いることはありませんし、ファイルを展開する先のフルパスに UNICODE でなければ正確に表現できない文字が含まれる場合でもカレントディレクトリを予め移動することでどーにかなるので、Microsoft Cabinet SDK が UNICODE に対応できていないことは大きな問題になることはないと思います。

cabarc.exe

cabarc.exe を使えば簡単に .cab ファイルの作成および展開が行えます。 .cab ファイルの作成はコマンドラインで...

cabarc -r -p N hoge.cab hogedir\*
...あるいは...
cabarc -r -p N hoge.cab hogefile1 hogedir\hogefile2 hogedir2\*
...のようなコマンドを実行するだけです。一応コマンドライン引数の指定を説明しておきますと、"-r" はサブディレクトリも対象にするという指定で、 "-p" はディレクトリ情報を保持するという指定で(この指定がないと全ファイルがフラットに格納されます)、"N" が .cab ファイル作成の指定で、"hoge.cab" が出力先の .cab ファイル名で、"hogedir\*", "hogefile1", "hogedir\hogefile2", "hogedir2\*" が圧縮対象のファイル・ディレクトリとなります。

Wraith the Trickster 後で、.cab ファイルに対してコードサイニングを行う場合は...
cabarc -r -p -s 6144 N hoge.cab hogedir\*
...のように "-s 6144" を指定して code signature を格納する領域を確保しておいてください。(...捕捉事項として記述しておきましたが、一般的に .cab ファイルへのコードサイニングは ActiveX コントロールの作成・配布の際に行われるもので、このページで解説しているインストーラに対するコードサイニングとは関係ありません。)

.cab ファイルの展開はコマンドラインで...

cabarc -p X hoge.cab
...のようなコマンドを実行するだけでお手軽にできるのですが、インストール対象の環境に cabarc が存在する可能性は低いのでインストール目的の場合 .cab ファイルの展開は他の方法を考えた方がよさそうです。

Wraith the Trickster 上の install.c では...
cabarc N hoge.cab hogedir\*
...のような形で生成されたディレクトリ情報が保持されていない .cab ファイルを前提としていますのでご注意ください。

makecab.exe

makecab.exe は .cab ファイル作成専用コマンドで、いろいろときめ細かい指定ができるのですが、cabarc.exe で基本的に事足りるんで説明は省きます。makecab.exe を詳細に知りたい場合は Microsoft Cabinet SDK 内のドキュメントを漁ってください。英文のドキュメントですが、比較的楽に読めます。

extract.exe

extract.exe は .cab ファイル展開専用コマンドです。コマンドラインで...

extract /E hoge.cab
...のようなコマンドを実行するだけでお手軽にファイルを展開できます。さらにこいつはちょっと変わった使い方ができまして、コマンドラインで...
copy /B extract.exe+hoge.cab hoge.exe
...てなコマンドで .cab ファイル(hoge.cab)と結合させてやると、アラ不思議、自己解凍ファイル(hoge.exe)の一丁あがりです。出来上がった自己解凍ファイルを実行するとカレントディレクトリにファイルをブチ蒔けます。

expand コマンド

expand コマンドは Microsoft Cabinet SDK には含まれませんが、 .cab 関連のコマンドとして便宜上触れておきます。 expand コマンドは .cab ファイルだけでなく compress でコマンド圧縮されたファイルにも対応している展開専用コマンドです。extract.exe とは違い .cab ファイル内に記憶されているディレクトリ情報を無視します。この為、expand.exe で一度に全てのファイルを展開しようとするとフラットに全て同じディレクトリにファイルを展開し、元々は違うディレクトリに存在していた同名のファイルを展開中に上書きしてしまいます。 使い方としては、コマンドライン上で...

expand hoge.cab -F:* hogedir
...のようなコマンドを実行するだけでお手軽にファイルを展開できます。"hoge.cab" は展開元の .cab ファイルで、"*" は抽出するファイルの指定(この場合はワイルドカードにより全てのファイル)で、"hogedir" は出力先のディレクトリとなります。

cabinet.dll

cabinet.dll は .cab ファイルに関する一通りの機能を提供してくれます。 圧縮機能に関するヘッダとインポートライブラリはそれぞれ FCI.h と FCI.lib で解凍機能に関するヘッダとインポートライブラリはそれぞれ FDI.h と FDI.lib となります。 インストーラを作成する上では必ずしも cabinet.dll を利用する必要はないので詳細な説明は省きます。 cabinet.dll を詳細に知りたい場合は Microsoft Cabinet SDK 内のドキュメントやサンプルコードを漁ってください。

Wraith the Trickster cabinet.dll で .cab ファイル作成時に UTF-8 を使う場合はファイル属性に _A_NAME_IS_UTF(0x80) を付加し、また展開時にはファイル属性に _A_NAME_IS_UTF(0x80) があることでそのファイル名を UTF-8 として扱います。

各ファイルの OS 別対応表

各ファイルのそれぞれの OS でのデフォルトでインストール状況は次のようになっています。
ファイル\OSWindows 95Windows 98Windows MeWindows NT 4.0Windows 2000Windows XP HomeWindows XP ProfessionalWindows Server 2003
cabarc.exe×××××××
makecab.exe××××××××
extract.exe××××
expand.exe×××
cabinet.dll××

Wraith the Trickster いろいろと調べて上の表は作成しましたが、実際にそれぞれの OS の素の状態の実機で確認したわけじゃないのでところどころ怪しいところがあります。例えば、cabarc.exe も某ドキュメントには Windows XP professional にはインストールされているとの記述があるのに実際に確認したらデフォルトじゃ入ってなかったし。

.inf ファイル

.inf ファイルとは

最近では主にドライバのインストールなんかによく使われるアレです。 .inf ファイルのファイル形式そのものは .ini ファイルと同様にテキストファイル(UNICODEでも可)でセクション別に各種情報が記述されます。 少々癖が強いですが、使いこなせればインストーラを作成する上でかなり強力な味方となってくれます。

ここでは簡単に .inf ファイルの説明をしていきますが、詳細についてはマイクロソフトのサイトの「Working with Setup Information (.inf) Files」あたりでも参照してください。

Wraith the Trickster .inf ファイルはホント、無駄に強力だったりするかと思えば、すっごくヘタレな部分もあるし、構文もめっちゃ癖が強いし、特徴をしっかり把握して活用しましょう。

.inf ファイルの実行

.inf ファイル( の DefaultInstall セクション )をプログラム中から実行するには次のようなコードで実行できます。

void execute_information_file(LPCTSTR inf_file_path, int mode)
{
    //  inf_file_path == .inf ファイルのフルパスで且つショートパスであること。
    //  mode は、再起動が必ずしもは必要でない場合は 132 に、必ず再起動が必要な場合は 130 にしてください。
    TCHAR buffer[MAX_PATH];
    LPCTSTR execute_section_name = _T("DefaultInstall");
    wsprintf(buffer, _T("%s %d %s"), execute_section_name, mode, inf_file_path);
    InstallHinfSection(NULL, NULL, buffer, 0);
}
Wraith the Trickster Windows XP で動作確認してみたところ、 InstallHinfSection の ANSI 版は正常に動作することが確認できませんでした。 私がどこかでチョンボをかましていただけかも知れませんが。
また、コマンドライン等から実行するには次のようなコマンドで実行できます。
RUNDLL32 SETUPAPI.DLL,InstallHinfSection DefaultInstall 132 .inf ファイルのフルパス(で且つショートパス)
Wraith the Trickster 上記のコード or コマンド中の DefaultInstall を別のセクション名に変えればそのセクションを実行できます。
上記のコード or コマンド中に "132" というマジックナンバーが出現しますが、これは下の表の "128" と "+4" を意味します。
mode意味
0システムが用意した .inf ファイル。
128.inf ファイルがあるディレクトリをデフォルトのパスとして使用。
+0常にコンピュータをリブートしない。
+1常にコンピュータをリブートする
+2常にユーザに確認をとってからリブートする。
+3コンピュータをリブートする必要があれば、ユーザの同意を得ずにリブートする。
+4コンピュータをリブートする必要があればユーザに確認をとってからリブートする。

LDID

LDID は Logical Disk IDentifier の略で CSIDL の前身みないなもんで、.inf ファイル内ではこいつを使ってフォルダを指定します。
LDID意味サンプル パスコメント
0Null LDID - can be used to create a new LDIDフルパスで指定したい場合にこの LDID を利用します。
1実行中の .inf ファイルがあるフォルダ
10Machine folder (maps to the Windows folder on a server-based setup.)
11システムフォルダC:\WINDOWS\system32
12IOSubsys folderC:\WINDOWS\system32\drivers
13Command folderC:\WINDOWS\system32\unknown最近の Windows ではもう使われていないフォルダっぽいです。
17InfフォルダC:\WINDOWS\Inf
18ヘルプフォルダC:\WINDOWS\Help
20フォントフォルダC:\WINDOWS\Fonts
21ViewersC:\WINDOWS\system32\viewers最近の Windows ではもう使われていないフォルダっぽいです。
22VMM32C:\WINDOWS\system32\unknown最近の Windows ではもう使われていないフォルダっぽいです。
23Color folderC:\WINDOWS\system32\spool\drivers\color
24Windowsフォルダあるドライブのルートフォルダ。C:\
25Windowsフォルダ。C:\WINDOWS
26Guaranteed boot device for Windows (Winboot)C:\WINDOWS\system32\unknown最近の Windows ではもう使われていないフォルダっぽいです。
28Host WinbootC:\WINDOWS\system32\unknown最近の Windows ではもう使われていないフォルダっぽいです。
30ブートドライブのルートフォルダ。C:\
31Root folder for host drive of a virtual boot driveC:\WINDOWS\system32\unknown最近の Windows ではもう使われていないフォルダっぽいです。

Wraith the Trickster 非常に残念なことに .inf ファイルは Program Files フォルダなんて概念がなかった時代からの遺産なんで、Program Files フォルダを示す LDID はありません。

.inf ファイルのサンプル

簡単なサンプルを用意しましたのでまず軽く目を通してみてください。分かりやすくする為に予約語的な部分を赤字にしておきました。

;
; ←半角のセミコロンからその行末までがコメントとなります。
;

[Version]
Signature = "$CHICAGO$" ; or "$Windows NT$"

[DefaultInstall]
CopyFiles  = MyApp FileList
AddReg     = Register Uninstaller
UpdateInis = Create MyApp Links

[Uninstall]
DelFiles   = MyApp FileList
DelReg     = UnRegister Uninstaller
UpdateInis = Delete MyApp Links

[MyApp FileList]
%InfFile%
myapp.exe

[DestinationDirs]
MyApp FileList = 0,%ProgramDir%\%CompanyDir%\%AppDir%

[Create MyApp Links]
setup.ini,progman.groups,,"myappfolder=%CompanyDir%"
setup.ini,myappfolder,,"%AppName%,%ProgramDir%\%CompanyDir%\%AppDir%\myapp.exe"

[Delete MyApp Links]
setup.ini,progman.groups,,"myappfolder=%CompanyDir%"
setup.ini,myappfolder,,"%AppName%"

[Register Uninstaller]
HKLM,SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\%CompanyDir%_%AppDir%,"DisplayName",,"%AppName%"
HKLM,SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\%CompanyDir%_%AppDir%,"DisplayIcon",,"%ProgramDir%\%CompanyDir%\%AppDir%\myapp.exe,0"
HKLM,SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\%CompanyDir%_%AppDir%,"UninstallString",,"RUNDLL32 SETUPAPI.DLL,InstallHinfSection Uninstall 132 %ProgramDir%\%CompanyDir%\%AppDir%\%InfFile%"

[UnRegister Uninstaller]
HKLM,SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\%CompanyDir%_%AppDir%

[Strings]
AppName="My Application"
AppDir="MyApp"
CompanyDir="MyCompany"
ProgramDir="C:\Progra~1"
InfFile="setup.inf"
...たったこれだけの内容の .inf ファイルで「ファイルのコピー」「スタート メニューの登録」「アンインストーラの登録(レジストリへの登録)」およびアンインストーラとして「ファイルの削除」「スタート メニューからの削除」「アンインストーラの登録解除(レジストリの削除)」と言ったインストーラ・アンインストーラとして一通りの処理を行ってくれます。 以降、.inf ファイルの詳細についてこのサンプルをベースに説明させていただきます。

行コメント

;
; ←半角のセミコロンからその行末までがコメントとなります。
;

[Version]
Signature = "$CHICAGO$" ; or "$Windows NT$"
";" が出現するとその行の ";" 以降はコメントとして扱われ、無視されます。

Version セクション

[Version]
Signature = "$CHICAGO$" ; or "$Windows NT$"
先頭にあるこの部分ですが、所謂「おまじない」だと思ってこのままあるいは "$CHICAGO$" の部分を "$Windows NT$" に変えて記述してください。

Wraith the Trickster CHICAGO ってのは Windows 95 (の開発コードネーム)のことです。あと、この Version セクションが正しく定義されていないと .inf ファイルは動かないから面倒臭がらずにちゃんと定義してね。

String セクション

[Strings]
AppName="My Application"
AppDir="MyApp"
CompanyDir="MyCompany"
ProgramDir="C:\Progra~1"
InfFile="setup.inf"
いきなり順番が前後して最後の部分ですが、String セクションでは文字列定数を定義できます。ここで定義した文字列を使用する場合は...
[DestinationDirs]
MyApp FileList = 0,%ProgramDir%\%CompanyDir%\%AppDir%
...のように定数名を % で囲った形で使用します。

Wraith the Trickster ProgramDir="C:\Progra~1" を見て嫌〜な予感を抱いた方、正解です。.inf ファイルまわりではロングファイル名にちゃんと対応できてなかったりするのでショートファイル名を指定する必要があります。 また、.inf では Program Files フォルダのパスを直接指定せざるを得ないので、.inf ファイルを呼び出すプログラム側で SHGetSpecialFolderPath() などを使って Program Files フォルダのパスを正しく取得した上で GetShortPathName() でショートネームに変換したパスを .inf ファイルに追記する形で指定してやるとよいでしょう。

DefaultInstall セクション

[DefaultInstall]
CopyFiles  = MyApp FileList
AddReg     = Register Uninstaller
UpdateInis = Create MyApp Links
インストールの指令を記述するセクションです。C/C++でいうところの main() 関数に相当します。 ここではファイルのコピー(CopyFiles)、レジストリへの登録(AddReg)、.iniファイルの編集(UpdateInis)を指示しています。

Wraith the Trickster プログラムから .inf ファイルを利用してインストールを実施する場合は必ずしも "DefaultInstall" である必要はないのですが、なにかトラブルがあった際に簡単に確認ができますし、 なにより第三者がその .inf ファイルを見た時の可読性を意味をなく下げる必要もないので特別な理由でもない限りは "DefaultInstall" のままにしておきましょう。

ファイルのコピー

ファイルのコピーは三つの記述により実施されます。

一つ目は DefaultInstall セクションあるいはその他の実行指示されたセクションにある...

CopyFiles  = MyApp FileList
...のような記述で、これはコピーするファイルのリストが記述されたセクションを指します。 ファイルのリストが記述されたセクションはカンマ区切りで複数指定することも可能です。 また、@ を先頭に付けることで直接ファイル名を記述することもできます。

二つ目は CopyFiles で指定された...

[MyApp FileList]
%InfFile%
myapp.exe
...のようなファイルのリストが記述されたセクションです。

三つ目は、DestinationDirs セクションで...

[DestinationDirs]
MyApp FileList = 0,%ProgramDir%\%CompanyDir%\%AppDir%
...のような記述でファイルのリストが記述されたセクション別にコピー先のディレクトリを指定します。= のすぐ後にある数字は LDID で、カンマ以降で LDID で指定されたディレクトリのサブディレクトリを指定します。サブディレクトリの指定は不要であれば省略可能です。

レジストリへの登録(アンインストーラの登録)

レジストリへの登録は二つの記述により実施されます。

Wraith the Trickster ここで紹介している .inf ファイルのサンプルではレジストリヘの登録機能を用いアンインストーラの登録を行っています。 この.infファイルベースのアンインストーラではアプリケーション構成の「変更」はできないので NoModify 値も指定したいところですが、 残念ながら .inf ファイルのレジストリ編集機能では REG_DWORD 型の値を登録できないので NoModify 値の登録は省略しています。アンインストーラの登録は Windows XP 以降限定でもいいなら REG コマンドの利用も考えたほうがいいかもしれません。

一つ目は DefaultInstall セクションあるいはその他の実行指示されたセクションにある...

AddReg     = Register Uninstaller
...のような記述で、これはレジストリへの登録内容が記述されたセクションを指します。 レジストリへの登録内容が記述されたセクションはカンマ区切りで複数指定することも可能です。

二つ目は AddReg で指定された...

[Register Uninstaller]
HKLM,SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\%CompanyDir%_%AppDir%,"DisplayName",,"%AppName%"
HKLM,SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\%CompanyDir%_%AppDir%,"DisplayIcon",,"%ProgramDir%\%CompanyDir%\%AppDir%\myapp.exe,0"
HKLM,SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\%CompanyDir%_%AppDir%,"UninstallString",,"RUNDLL32 SETUPAPI.DLL,InstallHinfSection Uninstall 132 %ProgramDir%\%CompanyDir%\%AppDir%\%InfFile%"
...のようなレジストリへの登録内容が記述されたセクションです。先頭の HKLM は HKEY_LOCAL_MACHINE のことで他にも HKCR(==HKEY_CLASSES_ROOT), HKCU(==HKEY_CURRENT_USER), HKU(==HKEY_USERS) などが指定できます。カンマで区切られた二番目のパラメータはレジストリキーパス、三番目はレジストリ値の名前、四番目はオプション指定、五番目はレジストリに登録される値の内容となります。オプションの指定は...
オプション意味
0REG_SZ型で、既に指定のレジストリ値が存在する場合は上書きする。(デフォルト)
1REG_BINARY型で、既に指定のレジストリ値が存在する場合は上書きする。
2REG_SZ型で、既に指定のレジストリ値が存在する場合は上書きしない。
3REG_BINARY型で、既に指定のレジストリ値が存在する場合は上書きしない。
...のようになっています。REG_BINARY で登録する場合は...
???,SOFTWARE\Sample\Sample,"hoge",1,FF,EE,DD,CC,BB,AA,99,88
...のように1バイトずつカンマ区切りで指定します。128 バイトを超える指定をする場合は行末に \ を記述し継続行指定を行い、1行あたり128バイトを超えないようにします。

Wraith the Trickster .inf ファイルの AddReg で、HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce 及び HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce\Setup への登録を Windows XP Professional で動作確認したところなぜか、再起動する前にファイルのコピー( CopyFiles )等が終了した段階で即実行されました(レジストリに登録されず...すぐ消えただけ?)。 また、CopyFiles でコピー先のファイルが使用中だった為にコピーがリブート時まで遅延されている状態でもリブート前に実行されるので注意が必要です。...う〜ん、なにか根本的なところで指示をミスってる or 単に .inf ファイルがそーゆーモノなだけかも。

スタート メニューへの登録

スタート メニューへの登録は二つの記述により実施されます。

一つ目は DefaultInstall セクションあるいはその他の実行指示されたセクションにある...

UpdateInis = Create MyApp Links
...のような記述で、これは .ini ファイルの編集内容が記述されたセクションを指します。 .ini ファイルの編集内容が記述されたセクションはカンマ区切りで複数指定することも可能です。

二つ目は UpdateInis で指定された...

[Create MyApp Links]
setup.ini,progman.groups,,"myappfolder=%CompanyDir%"
setup.ini,myappfolder,,"%AppName%,%ProgramDir%\%CompanyDir%\%AppDir%\myapp.exe"
...のような .ini ファイルの編集内容が記述されたセクションです。 先頭の setup.ini は編集を行う対象の .ini ファイルの指定で、カンマで区切られた二番目のパラメータは対象の .ini ファイル内でのセクション名、三番目は旧エントリーの内容で、四番目が新しいエントリーの内容となります。

以上は UpdateInis での .ini ファイルの編集方法に過ぎませんが、ここでミソなのは setup.ini の編集内容です。 最初の progman.groups セクションへの編集内容の "myappfolder=%CompanyDir%" の %CompanyDir% はスタート メニュー下に作成するグループ(フォルダ)の名前で myappfolder はそのグループに作成するショートカットのリストが記述されるセクション名となります。 二番目の myappfolder セクションへの書き込み内容はカンマ区切りで最初のパラメータがショートカット名、二番目のパラメータがリンク先のファイル、三番目のパラメータがアイコンファイル、四番目のパラメータがアイコンファイル中のインデックスとなります。(アイコンファイルとそのインデックスは省略可能です。)

Wraith the Trickster 余談ですが、この setup.ini の記述から実際にショートカットの作成を行うのは system32 ディレクトリの grpconv.exe のようです。

アンインストール セクション

[Uninstall]
DelFiles   = MyApp FileList
DelReg     = UnRegister Uninstaller
UpdateInis = Delete MyApp Links
アンインストールの指令を記述するセクションです。Uninstall というセクション名はユーザ定義の名称なので Uninstall というセクション名である必要はありません。 ここではファイルの削除(DelFiles)、レジストリの削除(DelReg)、.iniファイルの編集(UpdateInis)を指示しています。 このアンインストールの指令は先述の「レジストリへの登録(アンインストーラの登録)」の UninstallString 値で指定したコマンドで呼び出されます。

ファイルの削除

最初の CopyFiles が DelFiles に置き換わっているだけでファイルのコピーと同じです。なお、削除されるのは CopyFiles でのコピー先に相当するファイルです。

レジストリの削除

最初の AddReg が DelReg に置き換わっているだけでほぼレジストリの登録と同じです。登録と違い、削除するだけですのでキーあるいは値名までの指定だけで充分です。

スタート メニューからの削除

やることは .ini ファイル編集という点では登録と同じです。 違うのはショートカットの作成指示の部分で、削除するだけなのでショートカット名のみを指示します。リンク先以降は指定しないことで削除を意味します。 全てのショートカットを消せば自動でグループも削除してくれます。

その他の機能

.ini ファイルは他にも次のようなこともできますので必要に応じて調べてみてください。

インストーラ作成

リソースとしてアーカイブをインストーラに埋め込む

Microsoft Cabinet SDK の cabarc.exe を利用してインストールする対象のファイルを一つの .cab ファイルにまとめインストーラのリソースとして埋め込みます。 .cab ファイルをリソースに埋め込むには .rc ファイルで次のように記述してください。

install_cab CAB_FILE "install.cab"
..."install_cab" はインストールプログラムがこのリソースを参照する際に使用される名前になり、"CAB_FILE" はユーザ定義型のCAB_FILE型のデータであることを示し、"install.cab" はこのリソースとしてインクルードされる .cab ファイルです。

Wraith the Trickster Install SDK 内の install.rc には記述済みです。

少しでもコンパクトなインストーラにする為に

アーカイブの展開を行うコードは当然、圧縮できない部分となるとなりますので、まず、C++ は諦めて C 言語で我慢しましょう。 それから C 言語を使うといっても標準ライブラリの使用も厳禁です。 Windows API に多少標準ライブラリ代わりに使えるモノが用意されていますのでそちらで代用し、標準のライブラリをリンクしないようにします。

さらにスタートアップルーチンをとっても重い標準のスタートアップルーチンではなく自作のモノに置き換えます。 VC ではソースコード中に...

#pragma comment(linker, "/entry:\"WinMainCRTStartup\"")			
...のような記述を入れておくだけで自前の void __cdecl WinMainCRTStartup(void) 関数をスタートアップルーチンとして使用できます。 bcc の場合は若干面倒なのですが...
;How to Compile
; tasm /ml bccboot.asm
;

ideal
        p386n
        model nt flat

        procdesc WinMainCRTStartup        stdcall

codeseg
startup:
        call    WinMainCRTStartup
        ret

        end     startup
...のようなアセンブラのコードを用意、コンパイルし、(C0W32.OBJ, C0W32W.OBJ, C0D32.OBJ, C0D32W.OBJ, C0D32X.OBJ, C0X32.OBJ, C0X32W.OBJ などの)標準のスタートアップモジュールの代わりにリンクすれば自前の void __stdcall WinMainCRTStartup(void) 関数をスタートアップルーチンとして使用できます。

Wraith the Trickster cf. 軽い実行可能ファイルの作り方, 実行ファイルのサイズを小さくする

リリース用ファイルのアーカイブ展開

リソースに埋め込んだアーカイブのファイルへの出力と、その展開は次のようなコードでできます。


//  リソースの書き出し
void output_resource(HMODULE module_handle, LPCTSTR resource_name, LPCTSTR resource_type, LPCTSTR output_filename)
{
    //
    //  リソースの取得
    //
    HRSRC   resource_info   = FindResource(module_handle, resource_name, resource_type);
    HGLOBAL mem_handle      = LoadResource(module_handle, resource_info);
    LPVOID  lp_resource     = LockResource(mem_handle);
    DWORD   resource_size   = SizeofResource(module_handle, resource_info);

    //	
    //  ファイルへ出力
    //
    HANDLE  resource_file   = CreateFile(output_filename, GENERIC_WRITE, FILE_SHARE_READ |FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    DWORD   write_size      = 0;
    WriteFile(resource_file, lp_resource, resource_size, &write_size, NULL);
    CloseHandle(resource_file);
}

//  .cab ファイルの展開
void develop_cab_file(LPCTSTR dir_path, LPCTSTR cab_file, int nShow)
{
    TCHAR option[MAX_PATH];
    lstrcpy(option, _T("/E "));
    lstrcat(option, cab_file);

    SHELLEXECUTEINFO sei = { 0, };
    sei.cbSize = sizeof(sei);
    sei.fMask = SEE_MASK_NOCLOSEPROCESS;
    sei.hwnd = NULL;
    sei.lpVerb = NULL;
    sei.lpFile = _T("EXTRACT");
    sei.lpParameters = option;
    sei.lpDirectory = dir_path;
    sei.nShow = nShow;
    ShellExecuteEx(&sei);

    WaitForSingleObject(sei.hProcess, INFINITE);

    CloseHandle(sei.hProcess);
}

特殊フォルダのパスの取得

わざわざ説明するまでもないような気もしますが網羅性を高める為に一応触れておきますと、 システム的に意味のある各種フォルダのパスは SHGetSpecialFolderPath() あたりを使えば...

LPTSTR get_shell_folder_path(int nFolder, LPTSTR path)
{
    HWND hwndOwner = NULL;
    BOOL fCreate = FALSE;
    if (SHGetSpecialFolderPath(hwndOwner, path, nFolder, fCreate))
	{
        return path;
    }
    else
    {
        return NULL; // faild
    }
}
...みたいなコードで取得できます。参考までにディレクトリパスが取得可能な主だった CSIDL のリストを挙げておきます。
CSIDLサンプル パス
CSIDL_APPDATAC:\Documents and Settings\wraith\Application Data
CSIDL_CDBURN_AREAC:\Documents and Settings\wraith\Local Settings\Application Data\Microsoft\CD Burning
CSIDL_COMMON_ADMINTOOLSC:\Documents and Settings\All Users\スタート メニュー\プログラム\管理ツール
CSIDL_COMMON_APPDATAC:\Documents and Settings\All Users\Application Data
CSIDL_COMMON_DESKTOPDIRECTORYC:\Documents and Settings\All Users\デスクトップ
CSIDL_COMMON_DOCUMENTSC:\Documents and Settings\All Users\Documents
CSIDL_COMMON_FAVORITESC:\Documents and Settings\All Users\Favorites
CSIDL_COMMON_MUSICC:\Documents and Settings\All Users\Documents\My Music
CSIDL_COMMON_PICTURESC:\Documents and Settings\All Users\Documents\My Pictures
CSIDL_COMMON_PROGRAMSC:\Documents and Settings\All Users\スタート メニュー\プログラム
CSIDL_COMMON_STARTMENUC:\Documents and Settings\All Users\スタート メニュー
CSIDL_COMMON_STARTUPC:\Documents and Settings\All Users\スタート メニュー\プログラム\スタートアップ
CSIDL_COMMON_TEMPLATESC:\Documents and Settings\All Users\Templates
CSIDL_COMMON_VIDEOC:\Documents and Settings\All Users\Documents\My Videos
CSIDL_COOKIESC:\Documents and Settings\wraith\Cookies
CSIDL_DESKTOPC:\Documents and Settings\wraith\デスクトップ
CSIDL_DESKTOPDIRECTORYC:\Documents and Settings\wraith\デスクトップ
CSIDL_FAVORITESC:\Documents and Settings\wraith\Favorites
CSIDL_FLAG_DONT_VERIFYC:\Documents and Settings\wraith\デスクトップ
CSIDL_FONTSC:\WINDOWS\Fonts
CSIDL_HISTORYC:\Documents and Settings\wraith\Local Settings\History
CSIDL_INTERNET_CACHEC:\Documents and Settings\wraith\Local Settings\Temporary Internet Files
CSIDL_LOCAL_APPDATAC:\Documents and Settings\wraith\Local Settings\Application Data
CSIDL_MYMUSICC:\Documents and Settings\wraith\My Documents\My Music
CSIDL_MYPICTURESC:\Documents and Settings\wraith\My Documents\My Pictures
CSIDL_MYVIDEOC:\Documents and Settings\wraith\My Documents\My Videos
CSIDL_NETHOODC:\Documents and Settings\wraith\NetHood
CSIDL_PERSONALC:\Documents and Settings\wraith\My Documents
CSIDL_PRINTHOODC:\Documents and Settings\wraith\PrintHood
CSIDL_PROFILEC:\Documents and Settings\wraith
CSIDL_PROGRAM_FILESC:\Program Files
CSIDL_PROGRAM_FILES_COMMONC:\Program Files\Common Files
CSIDL_PROGRAMSC:\Documents and Settings\wraith\スタート メニュー\プログラム
CSIDL_RECENTC:\Documents and Settings\wraith\Recent
CSIDL_RESOURCESC:\WINDOWS\resources
CSIDL_SENDTOC:\Documents and Settings\wraith\SendTo
CSIDL_STARTMENUC:\Documents and Settings\wraith\スタート メニュー
CSIDL_STARTUPC:\Documents and Settings\wraith\スタート メニュー\プログラム\スタートアップ
CSIDL_SYSTEMC:\WINDOWS\system32
CSIDL_TEMPLATESC:\Documents and Settings\wraith\Templates
CSIDL_WINDOWSC:\WINDOWS

Wraith the Trickster 素の Windows 95 だとこのまわりの API がひとつも用意されていないんで、その場合はレジストリの HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders キー や HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders キー を参照します。

パス文字列の作成

Windows のファイル関連の API はディレクトリ区切りの \ が複数連続してもひとつの \ として扱い、 "C:\WINDOWS", "C:\\WINDOWS", "C:\\\WINDOWS" のどれも同じパスとして処理してくれます。 この挙動は Windows のバグなんだか仕様なんだか知らないのですが、とにかくこの挙動のおかげでパス文字列の作成する際に... 例えば、あるディレクトリのパス文字列の最後に \ がついているかどうかを意識せずに無条件に \ を付加してパス文字列を合成するなどして楽ができます。

Wraith the Trickster インストーラを軽くする為にライブラリ等の使用を制限された状況下だと文字列の最後が \ で終わってるかどうかなんて気にしたくないしねぇ。しかも Shift JIS だと 2 バイトコードのせいで最後のバイトだけチェックすりゃいいってわけでもないし。

MoveFileEx()

Windows 2000 以降でなければ使用できないのですが、インストーラ、アンインストーラを作成する上で便利なのが、Windows API の MoveFileEx() 。今現在使用中のファイルであっても MOVEFILE_DELAY_UNTIL_REBOOT を指定していればリブート時に上書きしたり削除したりと言ったことができます。

権限取得

インストール・アンインストールまわりでは各種権限取得を行わなければならいことがあり、それは以下のようなコードで行います。

//  権限アクセス関数
bool access_privilege(const wchar_t *prvlg, bool is_get) {
    HANDLE token = NULL;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES |TOKEN_QUERY, &token)) {
        const wchar_t *server_name = L"";
        TOKEN_PRIVILEGES tknPrvlgs = { 0, };
        if(LookupPrivilegeValue(server_name, prvlg, &(tknPrvlgs.Privileges[0].Luid))) {
            tknPrvlgs.PrivilegeCount = 1;
            tknPrvlgs.Privileges[0].Attributes = is_get ? SE_PRIVILEGE_ENABLED: 0;
            AdjustTokenPrivileges(token, false, &tknPrvlgs, sizeof(TOKEN_PRIVILEGES), 0, 0);
            if (ERROR_SUCCESS == GetLastError()) {
                CloseHandle(token);
                return true;
            }
        }
    }
    if (token)
    {
        CloseHandle(token);
    }
    return false;
}
//  権限取得関数
inline bool get_privilege(const wchar_t *prvlg) {
    return access_privilege(prvlg, true);
}
//  権限変換関数
inline bool release_privilege(const wchar_t *prvlg) {
    return access_privilege(prvlg, false);
}

リブート

Windows の再起動が必要な場合は ExitWindowsEx を使用しますが、そのまま ExitWindowsEX を呼び出すだけでは NT 系の Windows の再起動を行うことはできません。 ExitWindowsEx を呼び出す前に SE_SHUTDOWN_NAME を引数に前述の権限取得関数を利用してシャットダウン権限を取得しましょう。 インストール・アンインストールの都合であれば ExitWindowsEx の第二引数は SHTDN_REASON_MAJOR_APPLICATION |SHTDN_REASON_MINOR_INSTALLATION を指定するとよいでしょう。 ちなみにリモート接続によるセッションだと権限取得に加え、ExitWindowsEx の第一引数で EWX_FORCE が指定されていないとダメみたいです。

Wraith the Trickster .inf ファイルを利用する場合、InstallHinfSection の mode 指定で 132 の代わりに 130 を指定してリブートさせるほうが手っ取り早いとは思いますけど。

初期化用レジストリ

Windows では各種初期化用に以下のようなレジストリが用意されています。必要に応じて利用しましょう。

レジストリキー値名説明
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run任意このキーに登録された値はそのユーザ(=current user)がログインする度に実行されれます。
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce任意このキーに登録された値はそのユーザ(=current user)が次回ログインした際に一度のみ実行されれます。(実行後、値は自動的に消去されます。)
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run任意このキーに登録された値はWindowsが起動する度に実行されれます。
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce任意このキーに登録された値はWindowsが次回起動した際に一度のみ実行されれます。(実行後、値は自動的に消去さます。)
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce\Setup任意このキーに登録された値はWindowsが次回起動した際にシステムが初期化を実行中である下の画面のようなメッセージとともに一度のみ実行されます。(実行後、値は自動的に消去されます。)

画面中の"サンプル アプリケーション"はレジストリ値の名前です。また、なぜかこの機能は直接レジストリに記述を行うだけでは効果がなく、.inf ファイルの AddReg によって書き込まなければ機能しないようです。
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\WinlogonUserinitWinlogonキーのUserinit値にカンマ区切りで登録されたコマンドはユーザ(全ユーザが対象)がログインする度に実行されます。

Wraith the Trickster HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run 以下は Windwos のバージョンによっては動作しなかった気がします。(多分、Windows 2000 以降であれば概ね大丈夫かと。) あと、特に Userinit 値の編集は他の初期化用コマンドを誤って消しちゃったりしないように注意してください。 それから余談ですが、RunOnce は system32 ディレクトリにある RunOnce.exe がキックするようです。

アンインストーラ

アンインストーラの登録はレジストリの HKEY_LOCAL_MACHINE\SoftWARE\Microsoft\Windows\CurrentVersion\Uninstall\[アプリケーション固有のGUID] キーに以下のようなデータ書き込むことで行います。
値名意味
DisplayIconREG_SZ[プログラムの追加と削除]で表示される時に使用されるアプリケーションのアイコン。(省略化。)
DisplayIconREG_SZ[プログラムの追加と削除]で表示される時に使用されるアプリケーションの名称。
ModifyPathREG_SZ[プログラムの追加と削除]で[変更]ボタンをクリックした時に実行されるコマンド。(省略化、省略すると[変更と削除]ボタンあるいは[削除]ボタンのみが表示される。)
NoModifyREG_DWROD[プログラムの追加と削除]でのボタンの表示オプション。
意味
0[変更と削除]ボタンが表示される。(デフォルト)
1[削除]ボタンが表示される。
UninstallStringREG_SZ[プログラムの追加と削除]で[変更と削除]ボタンあるいは[削除]ボタンをクリックした時に実行されるコマンド。

下のサンプルコードでは表示名とアンインストールのコマンドのみの登録を行います。


//  アンインストーラの登録
void regist_uninstaller(LPCTSTR uninstall_reg_path, LPCTSTR application_name, LPCTSTR uninstall_command)
{
    HKEY    RegKey;
    DWORD   dwDisp;
    HRESULT lResult;
    TCHAR   szSubKey[1024];
    lstrcpy(szSubKey, _T("SoftWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"));
    lstrcat(szSubKey, uninstall_reg_path);
		
    ERROR_SUCCESS != RegOpenKeyEx(HKEY_LOCAL_MACHINE, szSubKey, 0, KEY_SET_VALUE, &RegKey) &&
    ERROR_SUCCESS != RegCreateKeyEx(HKEY_LOCAL_MACHINE, szSubKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &RegKey, &dwDisp);
//  RegSetValueEx(RegKey, _T("DisplayIcon"), 0, REG_SZ, (LPBYTE)application_icon, (lstrlen(application_icon) + 1) * sizeof(TCHAR));
    RegSetValueEx(RegKey, _T("DisplayName"), 0, REG_SZ, (LPBYTE)application_name, (lstrlen(application_name) + 1) * sizeof(TCHAR));
//  RegSetValueEx(RegKey, _T("ModifyPath"), 0, REG_SZ, (LPBYTE)modify_command, (lstrlen(modify_command) + 1) * sizeof(TCHAR));
    RegSetValueEx(RegKey, _T("UninstallString"), 0, REG_SZ, (LPBYTE)uninstall_command, (lstrlen(uninstall_command) + 1) * sizeof(TCHAR));
    RegCloseKey(RegKey);
}

より高級なインストーラの自作

インストーラのインストーラ

より高級なインストーラを作成する場合はインストーラを二段構えにし、最初のインストーラで二つ目のインストーラをテンポラリフォルダに展開・実行し、二つ目のインストーラがユーザインターフェイスの提供と本体プログラムのインストールを行うようにするのがいいと思います。このようにすることで圧縮されていないコードを最小限に留めることができます。

インストール制限 or 動作制限

アクティベーションなどの技術によりインストールそのものや動作等を制限する技術を利用したい場合は「もっと他に時間をかけるべき場所があるだろう」という観点からインストーラ作成用の製品等を利用することを強くオススメします。

コードサインニング

セキュリティ的な観点から昨今のインストーラはコードサインニングされていることが望まれます。 お金も手間も余分が掛かりますが、(リリースするモノの)無償・有償を問わず企業としてリリースするモノであれば[ベリサイン コードサイニング証明書]などのサービスを利用するべきだと思います。 逆に個人的にリリースするフリーウェアでそこまでするのは少々仰々しいかと思いますし実際問題ベリサインも受け付けてくれませんので、 Personal E-mail Certificates のようなサービスを利用するもいいかと思います。 また、企業としてリリースするモノではあっても社内ユースオンリーなモノであるならば自社内のみで通用するルート証明書を作成・運用するのもよしとされます。

Wraith the Trickster cf. 日々是開発:SQS Development

具体的なコードサイングのやり方はベリサインのこちらのドキュメントでも参考にしてください。

でもやっぱりインストーラを自作なんてすんのマンドクセ('A`)と言う方は

このあたりから適当に自分の都合・好みに合ったものでも利用してください。