4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

4
Last updated at Posted at 2021-04-22

はじめに

まずこのリポジトリを見つけたのは私ではなくyumetodoちゃんです
え?投稿的にyumetodoはどう見ても男だって?
そんなあなたにゆめとどは美少女風説流布委員会

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

どんなコード?

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

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. 到達できないコード

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

2. chocolateyのインストールを待機しないでchocoコマンドを実行してしまう

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

start-test1.bat
@echo off
start notepad.exe
start-test2.bat
@echo off
notepad.exe

実行したらコマンドプロンプトに注目してみて下さい。
明らかに挙動が違うのが分かると思います。
start-test1.batではメモ帳を開いてすぐにコマンドプロンプトが消えましたね。
でも反対にstart-test2.batはメモ帳を閉じるまでコマンドプロンプトが表示されていましたよね?
頭にstartがついてるかついてないかでこんな差が出てしまうんです。
実際に今回のコードのまま実行してしまうと何が起きるかというと…もうお分かりですね?
そうです。chocolateyのインストールを待機しないでchocoコマンドを実行しようとするのです。
DockerでWindows Server Coreをインストールして実行した結果がこれです。
image.png
chocoなんて無いよって言ってますね。そりゃchocolateyはインストールできてないですからね。
ここまで説明すればどうすればいいか分かりますよね?
そうです。startを消しちゃえばいいんです。というわけで消しちゃいましょう

3. std::cinは何も入力せずEnterを押しても入力を待機するので終了しない

下から4行目と3行目のことです。
せっかくですからメッセージもちょこちょこっと変えちゃいましょう。

問題解決後

こんな感じになりました

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;
}

1個目のsystemの[System.Net.ServicePointManager]::SecurityProtocolの閉じかっこが/になってましたけどまあそれは軽微も軽微なものですし、解説するほどのものでもないのでささっと直しちゃいました。
でもまだこれには問題があるんです。実はこれは私も実行してみるまでは気付かなかった問題なんですね。
いきなりですけど、私が実行するまで分からなかったバグを直しちゃいましょう

問題(難易度★★☆)

Set-ExecutionPolicy

これは罠もいいところだよ…。だってPowerShellでこれに書いてあるコマンド叩いても何の問題も無いんだもん
image.png
そしてコマンドプロンプトで実行してもこの通り。何ならsystemの中に書いてあるコマンドをフルで叩いたって何の問題も無かったんだから…
image.png
これ自体はBypassの前に-ExecutionPolicyってのを加えてやればいいだけの簡単なお仕事ではあるんだけど、今度は-Scopeに難癖付けてくるもんだからなぁ…。どうしろってんねん…
そう思った時、私の頭の中に浮かんだもの

「Windowsでsudoしたい」で書いたコマンドがあるじゃん!

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

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

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

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

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

こんな感じです

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;
}

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

問題(難易度★★★)

chocolateyのパスと環境変数の状態

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

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

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

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

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

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

問題解決後 Take 3

こんな感じになりました。

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で同じような処理を書く

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でも公開してます。

おまけ filesystemを使って書いてみた

私が最初から書いたやつをポイ捨てして行きます。

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です

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?