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

### msiはお好きですか？

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

別にそこまで嫌っている訳ではありませんが、「*.exe形式のインストーラと.msi形式のインストーラのどちらがいいか*」と訊かれたら、**速攻で**前者を選びます。
<br>
**だって、msiのインストール面倒なんだもん！**
<br>
いや、全部のmsiがそうだと言う訳ではなく、**管理者権限が必要な**msiだと面倒だと言う話です。
Windowsを長く使っていた人なら、msiを普通にインストールしようとして、エラーを吐かれた経験が1度はあるかと思います。

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

一方でmsiの場合は、

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

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

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

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

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

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

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

---

### プログラム

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

```cs
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のどこかに置いて完成。
<br>
あとは、コンテキストメニューをどうするかです。

---

### レジストリ

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

特定の拡張子にのみ対応するコンテキストメニューを編集したい場合は、`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](https://qiita-image-store.s3.amazonaws.com/0/177229/91019435-5f29-38f8-a85d-0ff9d29ee38d.png)

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

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

---

### インストーラ

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

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

```pascal
#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](https://qiita-image-store.s3.amazonaws.com/0/177229/cab2889c-efbd-2672-4240-25fa145442e8.png)

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

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

---

この**EasyMsi**は無料で配布しますので、[こちら](https://dl.dropboxusercontent.com/s/i1fydr32antjl1m/EasyMsi_R1_Setup.exe)からどうぞ。
Windows Vista以降、.NET Framework 4.6以降、そして0.7MBのディスク容量が必要です。

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