Haskell を Dev Container で動かしたい
1. Dev Container に Haskell プリセットがない
Dev Container で Haskell 環境を構築しようとすると,おそらくまず真っ先にプリセットから Haskell を探そうとするでしょう.そしてその試みは失敗に終わります.というのも,has
まで入力した時点で,候補が 0 件になってしまうのです.実はすこし以前にはあったようなのですが,現在では廃止されてしまったようです:
2. Haskell Feature だと拡張機能がうまく動作しない
Dev Container にプリセットがないとき,次に探すべきは Features です.そして幸運にも Features には Haskell がありました.これを利用すると,一応の Haskell 環境を Dev Container で構築することができます:
しかしこれで一件落着というわけにはいきませんでした.Haskell のコードを実行すること自体は出来るのですが,Visual Studio Code の Haskell 拡張機能を使おうとすると問題が生じたのです.具体的には,次のような 2 つのエラーメッセージが表示されました:
Project requires GHCup but it isn't installed
Cannot hlint the haskell file. The hlint program was not found. Use the 'haskell.hlint.executablePath' setting to configure the location of 'hlint'
そうかそうか,GHCup
と hlint
というやつを追加でインストールする必要があるんだね.ふむふむ.そして Haskell Feature にはオプションがあったことを思い出します.オプションは以下のとおりです:
Options Id | Description | Type | Default Value |
---|---|---|---|
ghcVersion | Select the GHC (Glasgow Haskell Compiler) version to install. | string | recommended |
cabalVersion | Select the Cabal (a system for building and packaging Haskell libraries and programs) version to install. | string | recommended |
globalPackages | Packages to install via cabal install , such as hlint for linting. Separate with spaces. This will add significant initial build time. |
string | - |
installHLS | Install HLS, the Haskell Language Server. | boolean | true |
downgradeGhcToSupportHls | This will downgrade GHC to the closest version supported by HLS. There is often a gap between a GHC release and tooling support. | boolean | true |
installStack | Install Stack, a build tool for Haskell. | boolean | true |
installStackGHCupHook | Enabling this means that stack won't install its own GHC versions, but uses GHCup's | boolean | true |
adjustBash | whether to adjust PATH in bashrc (prepend) | boolean | true |
ほうほう.GHCup
とか hlint
とかいう文字列があるしなんとかなりそう.とりあえず設定全部盛りにして確かめてみよう.しかし目論見は見事に外れました.しかしなぜだかよくわかりませんが,まぁとにかくこのオプションを使ってもエラーメッセージが解消されなかったのです😢
3. 公式イメージでも拡張機能がうまく動作しない
さて,なるべく Dockerfile は書きたくないのです.次の一手は,公式イメージを使うことです.調べてみると,Haskell も公式イメージが配布されていました.
だがしかし,悲しいかな.公式イメージにも GHCup
はインストールされていないのでした:
すべて読むのは大変だと思うので,かいつまんで要約すれば,インストールされているのは以下のものだけでした:
- Stack
- Cabal
- GHC
ちょっと調べただけなんですが,どうやら GHCup と Stack の機能が一部競合するらしく,最近のプロジェクトでは Stack が使われることが多いため,GHCup はお役御免となっているようです:
postCreateCommand
でインストールする
4. どうやら,多少のカスタマイズは避けられないようです.まずは devcontainer.json
の postCreateCommand
を使うことにしました.GHCup
と hlint
をインストールするコマンドは以下のとおりです:
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh && \
cabal update && \
cabal install hlint
しかし一筋縄ではいきません.curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
はオプションの選択などが対話的に実行されるため,そのままではコンテナの立ち上げが中断されてしまうのです.非対話的にインストールするには,環境変数を設定する必要がありました:
Dev Container の remoteEnv
で BOOTSTRAP_HASKELL_NONINTERACTIVE
を設定します.これでようやくコンテナ立ち上げ時に GHCup
と hlint
がインストールされるようになりました.
5. HLS もインストールする
しかしまだ問題がありました.上記の設定で Haskell のソースコードを開くと次のようなメッセージが表示されます:
Need to download hls-2.7.0.0, continue?
これはエラーメッセージではなく,単に質問するメッセージです.hls
というのは Haskell Language Server のことで,実のところ Haskell 拡張機能のコア機能はこいつらしいのです.じゃあ GHCup はなんだったのかというと,HLS をインストールするのに必要だからというだけだったようです.
さて,Yes
を選択すればインストールされて晴れて拡張機能が使えるようになります.しかし,せっかくコンテナを使っているのにコンテナでインストールが完結しないのはなんか違う気がします.そこで,HLS も postCreateCommand
でインストールすることにしました.インストールコマンドは以下のとおりです:
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh && \
ghcup install hls 2.7.0.0 && \
cabal update && \
cabal install hlint
しかしまだ不十分です.GHCup のパスを通す必要があるのでした.remoteEnv
で PATH
に GHCup を追加しましょう.これでインストールは成功するはずです.
6. GHC もインストールする
しかし,この状態でも Haskell のソースコードを開くと次のようなポップアップが表示されます:
Need to download ghc-9.8.2, continue?
つまり,対応しているバージョンの GHC を使えということですね.これも,ポップアップからインストールするのでなく,コンテナ側であらかじめインストールしておきたいところです.インストールコマンドはさらに次のようになります:
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh && \
ghcup install ghc 9.8.2 && \
ghcup install hls 2.7.0.0 && \
cabal update && \
cabal install hlint
7. 結局 Dockerfile に書きなおす
Dev Container の postCreateCommand
は改行を入れることができません.ひとつには JSON だから.そしてまた,Dev Container に複数のコマンドをリストで記述できるような仕組みがないからです.ここまでコマンドが長くなり環境変数もあるとなると,結局 Dockerfile を書いたほうがいいねということになってしまいました.完成した devcontainer.json
と Dockerfile
は次のとおりです:
{
"name": "Haskell",
"build": {
"dockerfile": "Dockerfile"
},
"customizations": {
"vscode": {
"extensions": [
"formulahendry.code-runner",
"mogeko.haskell-extension-pack"
]
}
}
}
FROM haskell:9.8.2
ENV PATH="${PATH}:/root/.ghcup/bin:/root/.cabal/bin:/root/.ghc/bin" \
BOOTSTRAP_HASKELL_NONINTERACTIVE=1
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh \
&& ghcup install ghc 9.8.2 \
&& ghcup install hls 2.9.0.0
RUN cabal update \
&& cabal install hlint
8. おわりに
ちょっと調べてみると,どうやら廃止される以前の Dev Container では,今回説明したような問題についてちゃんと解決されていたようなんですよね:
リポジトリの移行があったみたいで,廃止されたのはこのへんが原因なのかもしれません:
また Dev Container で簡単にセットアップできる日が来るとうれしいですね.
Discussion