C#
msi
レジストリ
innosetup

【配布】msiファイルのインストールの面倒さをなんとかする

msiはお好きですか?

私は、.msi形式のインストーラが苦手です。

別にそこまで嫌っている訳ではありませんが、「.exe形式のインストーラと.msi形式のインストーラのどちらがいいか」と訊かれたら、速攻で前者を選びます。


だって、msiのインストール面倒なんだもん!


いや、全部のmsiがそうだと言う訳ではなく、管理者権限が必要なmsiだと面倒だと言う話です。
Windowsを長く使っていた人なら、msiを普通にインストールしようとして、エラーを吐かれた経験が1度はあるかと思います。

そう、.exe形式なら、右クリックして「管理者として実行」を選べば良いだけなのに…!

一方でmsiの場合は、

  1. 管理者権限でコマンドプロンプトを立ち上げ
  2. その間にインストーラをShift+右クリックしてパスを取得し、
  3. コマンドプロンプトに貼り付けてEnter

…という手順を踏まなければなりません。

これは、msiexec.exeを呼び出すと同時にインストーラのパスを渡す、かつmsiexec.exeを管理者として実行する方法がほかにない為です。
管理者権限を持つコマンドプロンプトから呼び出してやれば、自ずとmsiexec.exeにも管理者権限が付与されるというワケ。


…いやいや、面倒すぎ

確かにそこまで手間ではないかもですけど、なんか釈然としない解決方法に思えます。

あと、Windows 8や10なら 比較的簡単に管理者権限でコマンドプロンプトを実行できますが、Windows 7だとこれがまた少し手間なんですよね。

msi形式のインストーラを何回もテストしなければならない開発者の方もいるはずですし、私としてもなんとかしたい…!
Microsoftも対処してくれない…!

そこで、.msiファイルのコンテキストメニューに「管理者としてインストール」を表示する方法を模索してみます。


プログラム

まずは、msiexec.exeを呼び出す為のプログラムを書きます。C#で。
プログラム名は…EasyMsiでいいや。

using System.Diagnostics;

namespace easymsi
{
    class Program
    {
        static void Main(string[] args)
        {
            var proc = new ProcessStartInfo();
            proc.Arguments = @"/i """ + args[0] + @"""";    // プログラムに渡す引数
            proc.FileName = "msiexec";  // 呼び出すプログラム名。msiexecはPATHが通っているのでフルパス要らず
            proc.Verb = "RunAs";        // 管理者として実行する為のおまじない

            Process.Start(proc);
        }
    }
}

インストーラのパスをMain関数の引数として受け取り、そのままmsiexec.exeに横流しするプログラムです。
超短い上に、超シンプル。

コンソールアプリケーションで書きましたが、コマンドプロンプトがいちいち表示されると鬱陶しいので、後からWindowsアプリケーションに変更します。これでコマンドプロンプトが表示されなくなります。これまた釈然としない方法ですが、ほかに有効策は見当たりませんでした。
ビルドしてできた「easymsi.exe」をPCのどこかに置いて完成。


あとは、コンテキストメニューをどうするかです。


レジストリ

コンテキストメニューの操作といえば、やっぱりレジストリですよね。

特定の拡張子にのみ対応するコンテキストメニューを編集したい場合は、HKEY_CLASSES_ROOTの中から該当のキーを探し出します。
msiの場合は、Msi.Packageキーがこれに該当します。

Msi.Package\shellに、「runasadmin」キーを作成します。キーの値は「管理者としてインストール(&A)」にします。アルファベットの前に&を付加すると、そのアルファベットがショートカットキーとして認識されます。&は表示されません。

次に、runasadminキーの中に「Icon」文字列値を作成します。この値に画像のパスを入れてやれば、コンテキストメニューにアイコンが表示されるようになります。
今回は管理者権限の盾アイコンが欲しいので、Windows内蔵のimageres.dllを利用します。ここにはWindowsで使用されるアイコンがどっさり入っていて、盾アイコンは73番目に格納されています。という訳で、文字列値の値は「C:\Windows\System32\imageres.dll,73」にします。
imageres.dllは64bit版WindowsだとSysWOW64に入っていますが、自動でリダイレクトされるのでこのままでも問題ないはずです。

最後に、runasadminキーの傘下に「Command」キーを作成します。キーの値は「"~\easymsi.exe" "%1"」にします。
~\easymsi.exeの部分にはeasymsi.exeのパスを入れます。
%1の部分には、右クリックしたファイルのパスが入るようになっているので、そのままeasymsi.exeに引数として送られます。

scr.png

最終的にこんな感じになるはずです。


この方法で行くと、easymsi.exeの場所は固定しておかなければなりません。また、そのパスをレジストリに登録しておく必要があります。

となると、インストーラも用意した方が手っ取り早いですね。
easymsi.exeはProgram Filesにでも置いておいて、そのパスをレジストリに書き込む処理をしてやれば良いのです。


インストーラ

という訳で、インストーラも作ってしまいましょう。
今回は、インストーラ作成ツールとしてInno Setupを採用しました。

下記がそのスクリプトです。

#define MyAppName "EasyMsi"
#define MyAppVersion "test"
#define MyAppPublisher "test"
#define MyAppURL "test"
#define MyAppExeName "easymsi.exe"

[Setup]
AppId={-}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={pf}\{#MyAppName}
OutputBaseFilename={#MyAppName}_{#MyAppVersion}_Setup
Compression=lzma
SolidCompression=yes

[Languages]
Name: "English"; MessagesFile: "compiler:Default.isl"
Name: "Japanese"; MessagesFile: "compiler:Languages\Japanese.isl"

[Files]
Source: "C:\EasyMsi\easymsi.exe"; DestDir: "{app}"; Flags: ignoreversion

[Registry]
Root: "HKCR"; Subkey: "Msi.Package\shell\runasadmin"; ValueType: string; ValueData: "Install as &administrator"; Flags: uninsdeletekey; Languages: English
Root: "HKCR"; Subkey: "Msi.Package\shell\runasadmin"; ValueType: string; ValueData: "管理者としてインストール(&A)"; Flags: uninsdeletekey; Languages: Japanese
Root: "HKCR"; Subkey: "Msi.Package\shell\runasadmin"; ValueType: string; ValueName: "Icon"; ValueData: "C:\Windows\System32\imageres.dll,73"; Flags: uninsdeletekey
Root: "HKCR"; Subkey: "Msi.Package\shell\runasadmin\Command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey

[Code]
function IsDotNetDetected(version: string; service: cardinal): boolean;
var
    key, versionKey: string;
    install, release, serviceCount, versionRelease: cardinal;
    success: boolean;
begin
    versionKey := version;
    versionRelease := 0;

    if version = 'v1.1' then begin
        versionKey := 'v1.1.4322';
    end else if version = 'v2.0' then begin
        versionKey := 'v2.0.50727';
    end

    else if Pos('v4.', version) = 1 then begin
        versionKey := 'v4\Full';
        case version of
          'v4.5':   versionRelease := 378389;
          'v4.5.1': versionRelease := 378675;
          'v4.5.2': versionRelease := 379893;
          'v4.6':   versionRelease := 393295;
          'v4.6.1': versionRelease := 394254;
          'v4.6.2': versionRelease := 394802;
          'v4.7':   versionRelease := 460798;
          'v4.7.1': versionRelease := 461308;
          'v4.7.2': versionRelease := 461808;
        end;
    end;

    key := 'SOFTWARE\Microsoft\NET Framework Setup\NDP\' + versionKey;

    if Pos('v3.0', version) = 1 then begin
        success := RegQueryDWordValue(HKLM, key + '\Setup', 'InstallSuccess', install);
    end else begin
        success := RegQueryDWordValue(HKLM, key, 'Install', install);
    end;

    if Pos('v4', version) = 1 then begin
        success := success and RegQueryDWordValue(HKLM, key, 'Servicing', serviceCount);
    end else begin
        success := success and RegQueryDWordValue(HKLM, key, 'SP', serviceCount);
    end;

    if versionRelease > 0 then begin
        success := success and RegQueryDWordValue(HKLM, key, 'Release', release);
        success := success and (release >= versionRelease);
    end;

    result := success and (install = 1) and (serviceCount >= service);
end;

function InitializeSetup(): Boolean;
begin
    if not IsDotNetDetected('v4.6', 0) then begin
        if ActiveLanguage = 'English' then begin
            MsgBox('EasyMsi requires Microsoft .NET Framework 4.6 or later.'#13
                'Please apply Windows Update or install from Microsoft''s homepage.', mbInformation, MB_OK);
        end
        if ActiveLanguage = 'Japanese' then begin
            MsgBox('EasyMsiのインストールには、Microsoft .NET Framework 4.6以上が必要です。'#13
                'Windows Updateの更新プログラムを適用するか、Microsoftのホームページからインストールしてください。', mbInformation, MB_OK);
        end
        result := false;
    end else
        result := true;
end;

どうせなら英語にも対応してみたかったので、日本語でインストールするときは「管理者としてインストール(A)」を、英語でインストールするときは「Install as administrator」を表示するようにしました。
命令文の最後にLanguage: xxxオプションを付加すると、言語によって処理を切り替えられます。これを利用して、書き込むレジストリの値を変更します。アンインストール時にレジストリが削除されるよう、Flags: uninsdeletekeyも忘れずに。

[Code]セクションでは、.NET Framework 4.6以降が入っていないときに警告を出すコードを書いてあります。これも見よう見まねですが、おそらく問題ないはず。あまり試してはいませんが…


完成!

できた!

先ほど手動で編集したレジストリを元に戻して、恐る恐るEasyMsiをインストールしてみると…

scr.png

おお!ちゃんと表示されています。これからはmsiとも仲良くやっていけそうな気がする。

プログラム云々よりも、Inno Setupの扱いに手間取りました。Pascalとか書いた事ないし…


このEasyMsiは無料で配布しますので、こちらからどうぞ。
Windows Vista以降、.NET Framework 4.6以降、そして0.7MBのディスク容量が必要です。

一応ウイルスチェックはしてありますが、気になる方は自分でもウイルスチェックをお願いします。