HomebrewのインストーラーをRubyからBashに書き直しました!

みなさんはHomebrewをお使いでしょうか。macOSをお使いの多くの開発者が使っていると思います。

HomebrewのインストーラーはRubyで書かれており、次のコマンドでインストールするようになっていました。

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

HomebrewがRubyに依存していることは良いのですが (formulaの書きやすさはRubyならでは)、インストーラーの話になると事情が変わってきます。HomebrewのインストールコマンドはmacOSの工場出荷状態でも動く必要があります。こういうものにRubyを使っているのはリスクがあります。

Homebrewの開発メンバーもインストーラーのRuby脱出問題は議論していて、Port installer to Bashというissueも立っていたのですが、数か月経っても誰も動き出す様子がありませんでした。 一月のある日、誰もBash化をやらないなら自分がやってしまおうという気持ちになったので書き直しました。

Rubyで書かれた小さなスクリプト (400行弱) とは言え、処理が1つでも抜けては困ります。 最初に git mv でファイルを移動し、 exit を少しずつずらしながら、順番にBashに書き換えていきました。 CIがすべての分岐を網羅しているわけではないので、細かいところは手でコマンドを打ったり変数の値を指定し実行してみて分岐に入るかどうかを確認したりしながら移行しました。

レビューの打ち返しをやってからしばらく反応がなかったのですが、先日ようやくマージされて、ホームページのインストールコマンドもBashに変わりました。 新しいコマンドは以下のようになります (引き続き以前のRubyのコマンドも使うことができますがWarningが表示されます)。

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

個人的な開発環境構築スクリプトBashスクリプトで書いており、RubyリスクはHomebrewをインストールするところだけだったので、これでようやくRuby依存をなくすことができました。 新しいmacOSRubyPythonが入っていなくても環境構築できそうです。 よかったですね。

Bashスクリプトを書く能力は現代となってはさほど重要ではありませんが、いざという時に役に立つこともあるので、数百行くらいなら書いちゃうぜという気持ちを持っているとお得です。 仮にBashが滅んだとしても、ログインシェル (zsh?) のスクリプトに書き直すのはRubyからBashに直すよりも簡単なはずなのですぐにできそうです。 Linuxbrewの取り込みも他の方の作業によって完了しています。 BashにしたことでLinuxインストーラーが統一されたのはめでたいですね。

おしまい!

ファイルをエディターで一括リネームするツールをGo言語で作った! ― 機能を増やさない信念と、OSSとの付き合い方

ファイルを一括でリネームしたいことはありませんか。私はあります。ということで作りました。

f:id:itchyny:20200109200059g:plain

インストールはHomebrew

brew install itchyny/tap/mmv

または以下のコマンドでできます。

go get github.com/itchyny/mmv/cmd/mmv

スクリーンショットではvimが起動していますが、 $EDITOR が設定されていればそれを使って編集することができます。

エディターでファイル名を編集して一括でリネームするというのは、新しい発想ではありません。 実際、多くのソフトウェア (特にファイラー) がこの機能を実装しています。

私はvimfilerの一括リネーム機能をよく使っていました。 特に不満はないのですが、ファイラーの付属機能である必要はないと考えて、またVimユーザーに限らず広く使ってもらえるように、独立したコマンドとして実装し直すことにしました。 一括リネームを実装するにはどういう技術的挑戦があるのか調べたかったという気持ちもありました。

実装

一括リネームと言ってもそんなシステムコールは当然ありませんから、順番にリネームしていくことになります。 簡単な一括リネームを実装すると次のようになります。

func Rename(files map[string]string) error {
    for src, dst := range files {
        if err := os.Rename(src, dst); err != nil {
            return err
        }
    }
    return nil
}

以上。終わりです。

巡回・スライシング

これで終わったらなんの挑戦もありませんが、厄介なケースがいくつかあります。 まずは巡回しているケースです。

a => b
b => c
c => a

そして、ずれていくケースです。

a => b
b => c
c => d

実際のユースケースとしてはあまりなさそうではあるものの、これらを全く考慮せずに実装してしまうと、ファイルを失う事故が起きてしまいます。 事故が起きないようにするには

  • 検知してエラーを吐く
  • リネームできるようにする

という2つの戦略が考えられます。

頭の中でツールの設計をしているときにこれらのケースをどう扱うかしばらく考えていたのですが、検知できるならリネームする順番もわかるだろうと考えて、最初からリネームできるように実装しました。

それではどのようにリネームすればいいのでしょうか。 まず巡回するケースは、一時的なファイル名を用意すれば、次の順にリネームすることができます。

a => tmp
c => a
b => c
tmp => b

ずれていくケースは、ずれの最後から逆順にリネームすればokです。

c => d
b => c
a => b

このようにすれば、リストにあるファイルを失うことなく、リネームを行うことができます (前提として、同じファイル名へのリネームと、同じファイル名からのリネームは排除しているとします)。

これは明らかにグラフの問題になっています。 巡回を判定するには辿ったノードをマークしていけばいいわけですし、ずれの最後から辿るというのはグラフの葉ノード (leaf node) から矢印を逆順に辿ることを意味しています。 グラフの分岐、つまり同じファイル名(への|からの)リネームを排除しているので、そんなに難しい処理ではありません。

いずれにしても、実際のユースケースではあまり起きないケースだろうなということは分かっています。 しかしファイル名を編集してリネームするUIでユーザーが行を入れ替えたときは、おそらくファイルの入れ替えを期待するだろうと考えて実装してみました。 一括リネームツールを作ろうとしていたらいつの間にかグラフの問題を解いていたというのは面白いですね。

機能を増やさない信念

このツールは、一括リネームを行うただそれだけのツールです。 エディターを立ち上げて、ファイル名の変更を読み取って、リネームしていくだけです。 ただリネーム先のディレクトリがなければ作るという挙動があり、これは以下のように画像を整理するときに便利だと思います。 f:id:itchyny:20200109222031g:plain

やりたかったことは既に実装できています。 このツールに関しては、他の複雑なことは実装しないと思います。 例えばファイルを削除したり、リネームの記録をどこかに保存してundoしたりといったことです。 これらはあまりに複雑で、あまりメンテナンスしたくありません *1。 そしてこれらの機能はmassrenが既に実装してますので、そういうものが欲しい人はmassrenに誘導すればいいかなと思っています。

機能を実装しないというのは、重要な設計指針の一つです。

ソフトウェアは機能が多くなるほど、ユーザーが学ばなければいけないことは増えていきます。 一括リネームなんてそもそもめったにやらないオペレーションなのに、そのツールにいろいろな機能があったとして使いこなせるでしょうか?

何かを作りはじめるときに大事なことは、全部入りを目指すのか、シンプルな独立ツールを目指すのかを決めるということです。 全部入りが悪いと言いたいわけではありません。 一貫したインターフェイスで様々なものを操作できる基盤ツールは素敵です。 素敵ですが、そういうソフトウェアを作るときはより慎重に設計しましょう。 達成したい目標が高いほど、設計の失敗が命取りとなることがあります。

シンプルな独立ツールを目指すのであれば、まずはできる限り機能を削ってみてください。 設定もユーザーとのインタラクションもできるだけ排除しましょう *2。 本当にやりたいことしかできない状態になっても、90%、いえ95%くらいの人はそれで十分でしょう。 ほとんど使われないような機能を足すことは、多くの人にとって難しい印象をもたせてしまいます。 機能が多く難しいという印象をもったツールは使わなくなってしまいます *3

UNIXという考え方―その設計思想と哲学

UNIXという考え方―その設計思想と哲学

ソフトウェアをメンテナンスする上で、複雑にならないように保つというのはとてもむずかしいことです。 著名なエンジニアから機能要望やpull requestが来て悩むこともあれば、取り込まないと書いたコメントがdown voteされることもあります。 しかし、機能を取り込んだ後にそれをメンテナンスするのは (多くの場合) あなた自身です。 安易に取り入れた機能に対して芋づる式に要望が増えたり、他の機能と干渉してバグを生んだりして、手に負えなくなることもあるでしょう *4。 理にかなっている機能要望かどうかを設計指針と照らし合わせて判断する力、そして時には機能を引く判断、これがOSSを長くメンテナンスしていくための生存戦略につながるのだと考えています。

まとめ

ファイルを一括でリネームするツールを作りました。

  • ファイル名の一覧をエディターで編集し、その結果をもとにファイルを移動する
  • 移動先のディレクトリが存在しなければ作成してからファイルを移動する

私にとっては既に十分であり、常用するツールになると思います。 そして今のこの気持ちを保ち続けることができたら、一年経ってもこのツールの機能をすべて思い出し、使いこなすことができるでしょう *5

最後になりましたが、様々なフィードバックをしていただいたvim-jpのみなさま、ありがとうございました。

*1:ファイルを削除するだけなのにそんなに複雑なのかと思われるかもしれません。ファイルの削除という危険な操作は、ユーザーによって様々な設定がされている場合があります。例えば単なる削除ではなくて~/.Trash/への移動にするとか、確認が欲しいとか、そういう話です。こういうものを実装し始めると、それは一括リネームツールの範疇を超えてしまいます。挙動をオンオフできるようにしたり、移動先を指定できるようにしたりといった要望が容易に想像つきます。

*2:逆に、すべてを設定できるようにするという発想もあります。茨の道ですが、そちらのほうが適切なケースもあるでしょう。しかし、設定同士が干渉したときの処理が複雑になり、メンテできなくなることが多い気がします。プログラミング言語ソースコードのフォーマッターは両極端で面白いです。

*3:標準のコマンドに関しては、機能が多くて難しくても使わざるを得ないのが現実でしょう。lsやpsのオプションを使いこなしている人はどれくらいいるのでしょうか。使いこなせなくても使うのは、どんな環境でも入っていて換えが効かないからでしょう。こういう分野で代替ツールがユーザー数を伸ばすのは至難の業です (batexaはすごい)。

*4:それはあなたの能力が低いからだと言われるかもしれませんが、その通りなのです。会社に勤めていると趣味OSSに割ける時間は限られていますし、睡眠は重要です (このブログを書くために多少睡眠を削っているわけですが…)。バグ報告や機能追加要望が多いと徐々にさばけなくなり、メンテ疲れしてしまいます。うまくいけば報酬を得てより多くの時間を割くとか、他の人にメンテナンスを委譲するとかできるかもしれませんが、多くの場合はうまく行かないので、自分が使える時間と実装能力、機能の需要やソフトウェアの品質などのバランスを取りながら、メンテナンスしていくことになります。自分に手に負えなくなりそうな機能であれば、要望を弾くのも大事です。時には他の代替ツールに引導を渡す判断が重要になる場面もあるかもしれません。

*5:3日前に誕生したばかりのツールです。ユーザーが増えて色々と言われるうちに気が変わって機能追加を始めるかもしれません。

2019年を振り返って

今年は仕事の部署異動があり気分一新したわけですが、思うようにパフォーマンスを出せず悩んでいたような気がします。前半もチームのために頑張っていた気がするんですがすべて忘れました。

今日は実家でgoreのGo modules対応をやってました。いい加減modules対応していないのやばいよなと思って一所懸命packagesパッケージのコードを読んでいます。まだ確認しないといけないパターンは沢山ありそうですが、年始にはマージする予定です (と書いて追い込んでおく)。goreは本当に便利なのでmodulesごときで死なせたくないですね…

今年はgojqを作れたのは大きいですね。これは本当にいいプロダクトなはずなんですが、宣伝がうまく行ってなくてイマイチですね。docker関連ツールの組み込みあたりを狙えたら本望なんですが、その前に英語で紹介記事を書かないとだめですね。しかしこれを作ったことで一年前よりも言語処理系に対するイメージがはっきりしてきたような気がします。jqのセマンティクスは結構特殊なんですけどね。

社内の諸事情から作ったgithub-migratorも便利なツールなのですが、ユースケースがニッチ過ぎてイマイチですね… 社内ドキュメントは書いてあるのでみんな移行してね。

今年は社用パソコンが変わってかなり快適になりました。自作cliツールのHomebrew tapリポジトリsetupリポジトリを整備できたのは大きいですね。環境セットアップが (理想的には) コマンド一つで立ち上がるようになりました。環境セットアップスクリプトCIを通っている安心感もありますね。

最近社内で圏論勉強会をやっていて、圏論に対する理解がかなり深まったのは大きな収穫です。随伴とか普遍的構成、極限あたりの考え方がだいぶ理解できて自信が付きました。普遍的構成の考え方ってプログラミングと相性がいいんですよね (一意の射が存在することが関数を定義できるのと同じなので)。ちゃんとプログラミングの世界に成果を持ち帰りたいところです。

旅行は夏に長崎に行きました。色づく世界の明日からやsolaの聖地を訪れることは良かったのですが、坂道の多い街なので歩きまわって疲れましたね。色づく世界の明日からのカットを回るにはもうちょっと下調べが必要でしたね。とある公園から山を降りるときにミスって墓地に迷い込んで大変でした。稲佐山からの夜景はめちゃくちゃきれいでした。

今年の良かったアニメは『ベルゼブブ嬢のお気に召すまま。』『やがて君になる』『かぐや様は告らせたい~天才たちの恋愛頭脳戦~』『女子高生の無駄づかい』『まちカドまぞく』『鬼滅の刃』あたりでしょうか。かぐや様の古賀葵さんめっちゃくちゃ良かったですね。ラジオ『令和最初の告RADIO』も良かった。

来年もいい一年になりますように、きっとなりますように。

錆兎「努力は、どれだけしても足りないんだよ。知ってるだろ、それはお前も」

鬼滅の刃 第四話

GitHub Enterprise から GitHub への移行ツールをGoで作りました!

弊社ではGitHub Enterprise (以下GHE) からGitHubへの移行が進んでいます。今年頭のプラン改変GitHub ConnectActionsAppsの充実などGitHubの機能強化が後押しとなりました。GHEのメンテナンスコストも徐々に重荷になってきていました。

リポジトリを移行するにあたって問題となるのが、これまでの歴史をどこまで新リポジトリに移行するかということです。もちろんgitのログはそのまま移行できますが、以下のようなものも移行したいと言われると色々と考えることが出てきます。

  • issueやpull requestのコメントやレビュー、ラベル
    • コードコメントからの参照もあるし、リポジトリ間も相互にリンクしている
      • 番号を維持したい
  • projectやmilestone
    • スプリントのフローが依存している
      • 今のカンバンをそのまま移行したい

これらをすべて移行するツールをGoで作りました。

f:id:itchyny:20191224220759j:plain

  • issueを番号を維持したまま移行します
    • 番号を維持することで、コメントのリンクはリポジトリのURLを変更するだけでOKです
    • ラベルもそのまま移行します
  • pull requestはissueに変換して移行します
    • レビューコメントもissueへのコメントに変換します
    • 番号が維持されるので、merged commitからのリンクも維持されます
  • プロジェクトやマイルストーンも、ほぼそのまま移行します
    • こちらも番号が維持されます
    • プロジェクトのautomationsはAPIで指定できないので移行しない
  • webhookの設定も移行します
    • webhookの設定は、手でやるとイベントを選ぶ必要があり面倒です

類似ツールとの比較

もともとfastlane社のissue移行ツールがありました。 issueのコメントを、それを書いた人が分かる形でリポジトリを移行する素晴らしいツールです。 issueやpull requestの移行にはIssue import APIを使っています。 tableタグでアイコンを出すなど、見た目の上でもかなり参考にしています。

issue番号を維持するというのはaereal/migrate-gh-repoにアイディアをもらいました。 issue間の相互リンク (#128のようなもの) やmerged commitのメッセージなど、issue (pull request) へのリンクが壊れると大変な箇所は意外とたくさんあります。 projectやmilestoneを移行するのにもissue番号が維持されているのは必要なことなのです (このことを気がつかせてくれました)。

issue・pull request番号を維持しつつコメントもレビューコメントもすべて移行する、それがgithub-migratorです。

リポジトリの歴史をどこまで捨ててよいかというのは様々な意見があると思います。 gitのログさえ残っていればよいという人もいれば、すべてのコメントやレビューに価値があり残すべきと考える人もいるでしょう。 GHEは当分運用されると分かっていても、いつか来る撤退の日に向けて、移行時にできるだけ情報を吸っておきたいというのが私の気持ちです。

実装

言語選択は、自分が書けて好きな (書いていて苦にならない) 言語と、社内で書ける人が多く手元で動かせる言語で共通集合を取ってGo言語一択でした。

GitHubAPIを叩く部分は自前でクライアントの実装を行っています。 golang/go-githubはIssue import APIに対応していない (undocumentedなAPIなので仕方ない) のと、構造体のメンバーがポインターだらけで使いにくいです。 APIクライアントは自前で作ったほうがAPIへの理解が深まるし、クライアントのドキュメントとにらめっこしなくてもよいし、リトライとかキャッシュとか変なところではまらなくてよいと思っています。

github-migratorは、issue一覧やコメント一覧、プロジェクトカード一覧など、様々なものの一覧APIを叩いています。 GitHubAPIは基本的にどんなリソースも100件ずつしか取れません。 ページングはレスポンスのLinkヘッダーを見て行います (参考)。

APIクライアントはページングのAPIをどのように扱ったらいいのでしょうか。 ユーザーとしては、ページングの切れ目を意識したくはありません (少なくとも今回のユースケースでは)。 そこで、イテレータを返してページングがユーザーに見えないようにしてみました。

// Issue represents an issue.
type Issue struct {
    ID int `json:"id"`
    // ...
}

// Issues represents a collection of issues.
type Issues <-chan interface{}

// Next emits the next Issue.
func (is Issues) Next() (*Issue, error) {
    for x := range is {
        switch x := x.(type) {
        case error:
            return nil, x
        case *Issue:
            return x, nil
        }
        break
    }
    return nil, io.EOF
}

// ListIssues lists the issues.
func (c *client) ListIssues(repo string, params *ListIssuesParams) Issues {
    // ...
}

使う側は、次のような感じです。

   issues := cli.ListIssues("sample/repo", &ListIssuesParams{})
    for {
        issue, err := issues.Next()
        if err != nil {
            if err == io.EOF {
                return nil
            }
            return err
        }
        fmt.Printf("%#v\n", issue)
    }

次のページとかページあたりの数とかを気にせず、一重のループで全件辿れるのは素敵です。 このやり方は基本的に全件たどりたいという今回のようなパターンには合うと思います (カーソルベースのページングには合わない)。

github-migratorはissueの番号を維持するように実装されています。 実はこれはそんなに簡単ではありません。 まずimportを順番に、エラーを確認しながら行う必要があります。 並列に作成して片方が失敗してしまうと、別のissueがその番号をとってしまいます。 そして番号は飛ぶことがあります。 issueやprojectが削除されたらその番号は欠番になりますし、issueはすでに別のリポジトリに移転されているかもしれません。 欠番は新しいリポジトリでも欠番でなくてはなりません (実はAPIでissueを削除することはできないため、github-migratorでは空のissueを作って削除は諦めています・projectは削除しています)。

リポジトリに何千件とissueがあると、そこそこ時間がかかってしまいます。 github-migratorは、いつ中断されても、同じコマンドで再開できるように設計されています。

GitHub間のリポジトリの移行ならば問題はないのですが、GHEからの移行で一番大きな問題になるのはユーザーの対応です。 IDが異なるユーザーがいるかも知れませんし、GHEに存在するユーザーがGitHubに存在しないかもしれません。 同じIDのユーザーが存在するのだけど実は別人の可能性もあります。 コメントを書いた人が同一IDの別人にリンクされたり、あるいはIDが異なる人のアイコンが出なかったりすると少し不便です。 こういう対応は人間にしかわかりませんから、ユーザーIDの対応を設定してもらうことにしました。 受け取った設定を元に、issueのauthorやメンションなどを置換するようにしています。

テスト

github-migratorは、APIを叩いて情報を収集し、APIを叩いて投稿するツールです。 移行元の情報と投稿する情報の対があれば、それが一つのテストケースになります。

Go言語ではテストケースを一覧で定義してループで回すTable driven testsスタイルが推奨されますが、構造体が複雑になるとこれさえ書くのが億劫になってきます。 そういうときはYAMLファイルにテストケースを書いてしまうのがおすすめです。 複雑な構造体を書く必要はありませんし、JSONとの変換のテストにもなっています (実はgojqでも同じようにテストケースを書いています)。

GitHub APIクライアントのモックはFunctional options patternで行っています。 必要なAPIのみをモックしたり、最初はサーバーが落ちていて2回叩いたらOKを返すなど (これは今回はやってませんが)、APIクライアントのモックには適した方法だと思っています。 APIクライアントを使うツールをテストするときは、頑張ってローカルにサーバーを立ててテストするよりも、モックしてしまうほうがよいでしょう。 テストをたくさん書いて通しても、本番サーバーに向けて落ちるものは落ちます (メソッドが間違ってたりヘッダーが足りなかったり)。 本番サーバー相当のバリデーションをテストに書くのはあまりにもナンセンスです (docker imageが提供されていないか探すほうがよい)。 作っているものがAPIクライアントlibraryそのものであり、その品質を高めたいという場合はサーバーを立ててもいいと思いますが、そうでない場合は頑張りすぎないほうが良いと思います。

まとめ

GitHubリポジトリ移行ツールをGoで作りました。 issueのコメントやレビュー、プロジェクトやマイルストーンなどをほぼすべてそのまま移行するツールです。

GitHub APIクライアントは自前で書く選択を取りました。 おかげでGitHubAPI v3にはそこそこ詳しくなったと思います。 自前で書くのはあまりおすすめできませんが、今回はundocumentedなAPIを叩く必要があり、また二週間ほどで作り上げる必要があったので (問題があったときにissueを立てて待ったりするのに律速されたくなかった)、すべて自分で書いてしまいました。 事前に必要なAPIが分かっていて、そんなに複雑なプロトコルでなければ、自前で書くのもアリだなと思いました。

私の所属しているチームのほぼすべてのリポジトリを今回作ったツールでGitHubに移行しました。 issueの移行もそうですが、projectsの移行をきちんと実装していたおかげで、スプリントの進行を妨げることなく (ポチポチとカンバンを作り直すことなく)、GitHubに移行することができました。 GitHub ActionsDependabotRenovateなどのモダンな開発ツールを勢いよく取り入れて、より効率よく開発を回し、コードの健全性を維持していきたいと思います。

開発環境構築スクリプトのCIをGitHub Actionsで回す

小ネタですが、開発環境の構築はスクリプト化して、CIを回そうという話です。

開発環境を構築することは年にそう何回もあるわけではないですが、スクリプトを一発叩いて必要なツールが揃うようにしておくと便利です。私は素朴にシェルスクリプトで書いています。好きな言語で書けばいいと思いますが、macOSは将来的にRubyやPythonといったスクリプト言語を排除しようとしていて、不安ですね。Ansibleみたいなのを使ってもいいと思います。私はちょっと苦手で…

あくまで私用のスクリプトなので使わないでください。

このスクリプトを叩いてしまえば、iTerm2やVim、tmux、自分のdotfilesの配置と言語処理系のインストール、Google ChromeやSlackのインストールを行ってくれます。モダンなプロジェクトならdockerさえあればいいんでしょうが、なかなかそういうわけにはいかないですよね。

この環境構築スクリプトを作り始めてから、普段使っている自作cliツールのHomebrewルールをきちんと書くようになりました。当初は環境セットアップ時にもビルドすればいいのではと思っていたのですが、コンパイルに時間のかかる言語だとセットアップにも時間がかかってしまうので、やはりGitHub Releasesからビルド済みのバイナリを落としてくるだけのほうが構築時は楽ですね。

あと、dotfilesでbootstrap的なことをやるのはあまりおすすめしません。もともとdotfilesを配置してzshを叩けば色々とプラグインをとってくるみたいな処理を.zshrcに書いていたのですが、環境構築のスクリプトを作ったらそちら側にまとめられますし、一度セットアップしたらプラグインディレクトリのチェックなどは不要になるので、rcファイルは大幅に削減できました。

環境構築スクリプトを育てていると、どうしても今のPCの環境には適用できるけれど実は新規PCには適用できなくなっているということは起きてしまいます。まっさらな状態からセットアップすることはめったにありませんからね。具体的にはディレクトリのないところにsymlinkを貼るとか、セットアップの前の方で入れているツールを後の方で使っているのだけどPATHが通っていないとか、そういうケースです。

そこでCIを回しましょうという話です。最近、GitHub Actionsのmacos-latestでCIを回すようになりました。実際にCIを回したら、必要なディレクトリを作るのを忘れていたり、諸々通らないことが発覚しました。

CIを回すと、環境構築にかかるおよその時間がわかります。もちろんスペックの違いや、GitHubとのレイテンシの違い (Actionsはcloneが異常に速い) などもありますが、概ね20分でセットアップが完了するようです。一つコマンド叩けば概ね普段の環境が立ち上がるのはやはり良いですね。

おしまい