GitHubに上がってたeasySetupMingwというプロジェクトの発想が面白かった。でもWindowsは優しくなかった

# はじめに
まず[このリポジトリ](https://github.com/h0tcat/easySetupMingw)を見つけたのは私ではなく[yumetodo](https://qiita.com/yumetodo)ちゃんです
え？投稿的にyumetodoはどう見ても男だって？
そんなあなたに[ゆめとどは美少女風説流布委員会](https://twitter.com/search?q=%EF%BC%83%E3%82%86%E3%82%81%E3%81%A8%E3%81%A9%E3%81%AF%E7%BE%8E%E5%B0%91%E5%A5%B3%E9%A2%A8%E8%AA%AC%E6%B5%81%E5%B8%83%E5%A7%94%E5%93%A1%E4%BC%9A&src=typed_query)

いやそんなことはどうでもよくてだな…
とにかく、yumetodoちゃんとSkypeで話してる時に彼女が見つけたのがきっかけでした

# どんなコード？
端的に言えば、chocolateyでMinGWをインストールするというものでした
とりあえずコードを見てみましょう

```cpp:main.cpp
#include <iostream>
#include <cstdlib>
#include <string>

int main(void){
    std::cout << "gccをインストールするために必要なプログラムをインストールします。" << std::endl;
    system("start powershell \"Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager/::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://chocolatey.org/install.ps1\'))\"");
    std::cout << "OK." << std::endl;
    std::cout << "gccをインストールします。" << std::endl;
    system("choco install mingw");

    std::cout << "インストールが完了しました!" << std::endl;
    std::cout << "一度プログラムを終了して、コマンドプロンプトを開き、gccコマンドが動作するかを確認してください。" << std::endl;
    std::cout << "enterキーを押すと終了します" << std::endl;
    std::string buffer;
    std::cin >>  buffer;
    exit(0);
    return 0;
}
```

# 問題（難易度★☆☆）
まあ発想は面白かったです。ただこのコードには問題があるんですね。
まあとりあえずすぐに分かるものから潰していきましょう

## 1. 到達できないコード
まず１つ目、到達できないコードです。
原因はmain関数の下から2行目の```exit(0);```です。
これがあることで一番下の```return 0;```に到達できないんですね。
まあこれは下から2行目を消すだけなのでささっと消しちゃいましょうね。

## 2. chocolateyのインストールを待機しないでchocoコマンドを実行してしまう
２つ目はchocolateyが入ってない環境で起きる問題です。それはmain関数の2行目です。
パッと見どこにも問題ないように見えますよね？
Windowsをよく知ってる人なら気付ける話なんですが、原因はコマンドの頭の```start```です。
うーん…そうですね…。試しにこの２つのバッチファイルをコピペして実行してみて下さい。

```bat:start-test1.bat
@echo off
start notepad.exe
```
```bat:start-test2.bat
@echo off
notepad.exe
```
実行したらコマンドプロンプトに注目してみて下さい。
明らかに挙動が違うのが分かると思います。
```start-test1.bat```ではメモ帳を開いてすぐにコマンドプロンプトが消えましたね。
でも反対に```start-test2.bat```はメモ帳を閉じるまでコマンドプロンプトが表示されていましたよね？
頭に```start```がついてるかついてないかでこんな差が出てしまうんです。
実際に今回のコードのまま実行してしまうと何が起きるかというと…もうお分かりですね？
そうです。chocolateyのインストールを待機しないでchocoコマンドを実行しようとするのです。
DockerでWindows Server Coreをインストールして実行した結果がこれです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94629/b512ba31-1723-2bf3-3a0d-340004eb1570.png)
chocoなんて無いよって言ってますね。そりゃchocolateyはインストールできてないですからね。
ここまで説明すればどうすればいいか分かりますよね？
そうです。```start```を消しちゃえばいいんです。というわけで消しちゃいましょう

## 3. std::cinは何も入力せずEnterを押しても入力を待機するので終了しない
下から４行目と３行目のことです。
せっかくですからメッセージもちょこちょこっと変えちゃいましょう。

# 問題解決後
こんな感じになりました

```cpp:main.cpp
#include <iostream>
#include <cstdlib>
#include <string>

int main(void){
    std::cout << "gccをインストールするために必要なプログラムをインストールします。" << std::endl;
    system("powershell \"Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://chocolatey.org/install.ps1\'))\"");
    std::cout << "OK." << std::endl;
    std::cout << "gccをインストールします。" << std::endl;
    system("choco install mingw");

    std::cout << "インストールが完了しました!" << std::endl;
    std::cout << "コマンドプロンプトを開き、gccコマンドが動作するかを確認してください。" << std::endl;
    system("pause");
    return 0;
}
```
１個目のsystemの```[System.Net.ServicePointManager]::SecurityProtocol```の閉じかっこが```/```になってましたけどまあそれは軽微も軽微なものですし、解説するほどのものでもないのでささっと直しちゃいました。
でもまだこれには問題があるんです。実はこれは私も実行してみるまでは気付かなかった問題なんですね。
いきなりですけど、私が実行するまで分からなかったバグを直しちゃいましょう

# 問題（難易度★★☆）
## Set-ExecutionPolicy
これは罠もいいところだよ…。だってPowerShellでこれに書いてあるコマンド叩いても何の問題も無いんだもん
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94629/e642cedf-b6d6-bedb-5ee5-8dc584f33a33.png)
そしてコマンドプロンプトで実行してもこの通り。何ならsystemの中に書いてあるコマンドをフルで叩いたって何の問題も無かったんだから…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94629/8e966d95-ca5a-3319-5086-3ddbb5c11bf9.png)
これ自体は```Bypass```の前に```-ExecutionPolicy```ってのを加えてやればいいだけの簡単なお仕事ではあるんだけど、今度は```-Scope```に難癖付けてくるもんだからなぁ…。どうしろってんねん…
そう思った時、私の頭の中に浮かんだもの

「[Windowsでsudoしたい](https://qiita.com/AinoMegumi/items/fd56711fe1fd2a0e1bbf)」で書いたコマンドがあるじゃん！

早速書き換えて実行したら問題なくインストール完了！
勝った！

…っとと、説明しないとな
思い出したのがこれです

```shell:cmd.exe
powershell -NoProfile -ExecutionPolicy unrestricted -Command "Start-Process \"%1\" -Verb runas"
```

これを応用して、こんな形になりました

```cpp:main.cpp
    (前略)
    std::cout << "gccをインストールするために必要なプログラムをインストールします。" << std::endl;
    system("powershell -ExecutionPolicy Bypass -Command \"[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://chocolatey.org/install.ps1\'))\"");
    std::cout << "OK." << std::endl;
    (後略)
```

あ、```-NoProfile```は不要です。

# 問題解決後　Take 2
こんな感じです

```cpp:main.cpp
#include <iostream>
#include <cstdlib>
#include <string>

int main(void){
    std::cout << "gccをインストールするために必要なプログラムをインストールします。" << std::endl;
    system("powershell -ExecutionPolicy Bypass -Command \"[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://chocolatey.org/install.ps1\'))\"");
    std::cout << "OK." << std::endl;
    std::cout << "gccをインストールします。" << std::endl;
    system("choco install mingw");

    std::cout << "インストールが完了しました!" << std::endl;
    std::cout << "コマンドプロンプトを開き、gccコマンドが動作するかを確認してください。" << std::endl;
    system("pause");
    return 0;
}
```

よーし、全部直った！
…と思ったそこのあなた、まだ１個残ってますよ！
これ以上何が残ってるんだって？これはWindowsの環境変数やカレントディレクトリの闇を知る者なら即座に分かる問題です。

# 問題（難易度★★★）
## chocolateyのパスと環境変数の状態
ようこそWindowsの闇の世界へ。私はこの闇の世界の住人だ。この闇に飲まれれば二度と光の世界へ帰ることはできないであろう。フハハハハハ！
何言ってんだこいつって思ったそこのあなたはさぞかし恵まれた環境でプログラミングしてるんでしょうね。
てか自分でも言ってて恥ずかしいなこれ。
でもWindowsのプログラミングをしてると嫌ってほどカレントディレクトリ問題と環境変数問題って付きまとうんですよねぇ…。
さて、そろそろ真面目に解説しましょう。
１回目のsystemコマンドでchocolateyのインストールは完了しました。
普通ならここでもうPATH通ってるだろって思うでしょ？でも残念でした。PATHは通ってないんです。
理由は非常にばからしい話ですが

chocolateyをインストールしたのはあくまでpowershell.exeという別のプロセスであり、PATHが通ってるのはそのpowershell.exeの中での話！

よーく思い出してみて下さい。system関数に書いたコマンドを.

頭にpowershellって付いてませんでした？
そう。あれはpowershell.exeという子プロセスを生成し、chocolateyをインストールするためのコマンドを実行させただけ
親プロセスの環境変数は変わってないのでPATHは通ってないんです！
この解決策は次の３つがあります。

- プロセスのカレントディレクトリを変更する
- PowerShellに実行させるコマンドに```choco install mingw```を加えてしまう
- choco.exeのフルパスを書く

ただ、２つ目のはMinGWインストール前にメッセージを表示させたいという作者さんの意図に反しちゃうので無かったことにしましょう。まあもちろんあのメッセージをPowerShellで表示させるって手もあるけど、コマンドライン引数が長くなるから私もやりたいと思えない。だから無かったことにしましょう。
さて、話を戻しましょう。
１個目のはstd::filesystemでやるも良し、Win32 APIを使うも良し。まあ好きなようにやってくれ。
３個目のはもうほんとにやることシンプル。```choco install mingw```を```C:\ProgramData\chocolatey\bin\choco.exe install mingw```に書き換えてやるだけだ。
今回は３個目の方でやります。

# 問題解決後 Take 3
こんな感じになりました。

```cpp:main.cpp
#include <iostream>
#include <cstdlib>
#include <string>

int main(void){
    std::cout << "gccをインストールするために必要なプログラムをインストールします。" << std::endl;
    system("powershell -ExecutionPolicy Bypass -Command \"[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://chocolatey.org/install.ps1\'))\"");
    std::cout << "OK." << std::endl;
    std::cout << "gccをインストールします。" << std::endl;
    system("C:\\ProgramData\\chocolatey\\bin\\choco.exe install mingw");

    std::cout << "インストールが完了しました!" << std::endl;
    std::cout << "一度プログラムを終了して、コマンドプロンプトを開き、gccコマンドが動作するかを確認してください。" << std::endl;
    std::cout << "コマンドプロンプトを開き、gccコマンドが動作するかを確認してください。" << std::endl;
    system("pause");
    return 0;
}
```

マジで疲れました
てかこの程度ならPowerShell Scriptでもできるんですよ…

# PowerShell Scriptで同じような処理を書く
```powershell:easySetupMingw.ps1
if (-not (Test-Path Variable:choco)) { 
    $choco = Convert-Path "$((Get-Command choco).path)";
    if (-not (Test-Path Variable:choco)) { $choco = "C:\ProgramData\chocolatey\bin\choco.exe"; }
}
if (-not [System.IO.File]::Exists($choco)) {
    [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072;
    iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'));
}
Start-Process $choco -Wait -ArgumentList install,mingw
```
あえてメッセージは表示させてません。
私のはchoco.exeがあるかどうかのチェックまで入れてありますので、chocolateyがインストールされてる環境ではchocolateyのインストール部分をスキップするようになっています。
ちなみに[私のGitHub](https://github.com/Kamioda/easySetupMingw_Powershell)でも公開してます。

# おまけ　filesystemを使って書いてみた
私が最初から書いたやつをポイ捨てして行きます。

```cpp:main.cpp
#include <iostream>
#include <cstdlib>
#include <filesystem>

int main() {
    try {
        const std::filesystem::path choco("C:\\ProgramData\\chocolatey\\bin\\choco.exe");
        if (!std::filesystem::exists(choco)) {
            std::cout << "GCCをインストールするために必要なプログラムをインストールします。" << std::endl;
            std::system("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -ExecutionPolicy Bypass -Command \"[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(\'https://chocolatey.org/install.ps1\'))\"");
            std::cout << "OK" << std::endl;
            if (!std::filesystem::exists(choco)) throw std::runtime_error("Failed to install chocolatey");
        }
        std::cout << "GCCをインストールします。" << std::endl;
        std::system((choco.string() + " install mingw").c_str());
        std::cout << "インストールが完了しました!" << std::endl;
        std::cout << "コマンドプロンプトを開き、gccコマンドが動作するかを確認してください。" << std::endl;
        std::system("pause");
    }
    catch (const std::exception& er) {
        std::cerr << er.what() << std::endl;
    }
    return 0;
}
```
いちいち解説はしません。だってポイ捨てだし、そもそもPowerShell Scriptで十分だからね。

# 最後に
Windowsってやっぱり闇が深いね！

# 2021/4/23　追記
さっき気付いたけど今回紹介したやつ、LICENSE無いやん！
私がここにポイ捨てしたfilesystem使って書いたコードはGitHubとかには上げてないけどMIT LICENSEです