🐘

Haskell を Dev Container で動かしたい

2024/03/31に公開

1. Dev Container に Haskell プリセットがない

Dev Container で Haskell 環境を構築しようとすると,おそらくまず真っ先にプリセットから Haskell を探そうとするでしょう.そしてその試みは失敗に終わります.というのも,has まで入力した時点で,候補が 0 件になってしまうのです.実はすこし以前にはあったようなのですが,現在では廃止されてしまったようです:

https://github.com/microsoft/vscode-dev-containers/tree/main/containers/haskell

2. Haskell Feature だと拡張機能がうまく動作しない

Dev Container にプリセットがないとき,次に探すべきは Features です.そして幸運にも Features には Haskell がありました.これを利用すると,一応の Haskell 環境を Dev Container で構築することができます:

https://github.com/devcontainers-contrib/features/tree/main/src/haskell

しかしこれで一件落着というわけにはいきませんでした.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'

https://marketplace.visualstudio.com/items?itemName=haskell.haskell

そうかそうか,GHCuphlint というやつを追加でインストールする必要があるんだね.ふむふむ.そして 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

https://github.com/haskell/vscode-haskell#downloaded-binaries

ほうほう.GHCup とか hlint とかいう文字列があるしなんとかなりそう.とりあえず設定全部盛りにして確かめてみよう.しかし目論見は見事に外れました.しかしなぜだかよくわかりませんが,まぁとにかくこのオプションを使ってもエラーメッセージが解消されなかったのです😢

3. 公式イメージでも拡張機能がうまく動作しない

さて,なるべく Dockerfile は書きたくないのです.次の一手は,公式イメージを使うことです.調べてみると,Haskell も公式イメージが配布されていました.

https://hub.docker.com/_/haskell/

だがしかし,悲しいかな.公式イメージにも GHCup はインストールされていないのでした:

https://github.com/haskell/docker-haskell/blob/a20f832dd35d2f7ceff5337b41a7e37244e1c9e1/9.8/buster/Dockerfile

すべて読むのは大変だと思うので,かいつまんで要約すれば,インストールされているのは以下のものだけでした:

  • Stack
  • Cabal
  • GHC

ちょっと調べただけなんですが,どうやら GHCup と Stack の機能が一部競合するらしく,最近のプロジェクトでは Stack が使われることが多いため,GHCup はお役御免となっているようです:

https://zenn.dev/mod_poppo/articles/haskell-setup-2023

4. postCreateCommand でインストールする

どうやら,多少のカスタマイズは避けられないようです.まずは devcontainer.jsonpostCreateCommand を使うことにしました.GHCuphlint をインストールするコマンドは以下のとおりです:

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 はオプションの選択などが対話的に実行されるため,そのままではコンテナの立ち上げが中断されてしまうのです.非対話的にインストールするには,環境変数を設定する必要がありました:

https://www.haskell.org/ghcup/guide/#customisation-of-the-installation-scripts

https://github.com/haskell/ghcup-hs/blob/63e714d1b1a0b1c059ba097c6e0b03cf6b6bd707/scripts/bootstrap/bootstrap-haskell#L7-L24

Dev Container の remoteEnvBOOTSTRAP_HASKELL_NONINTERACTIVE を設定します.これでようやくコンテナ立ち上げ時に GHCuphlint がインストールされるようになりました.

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 のパスを通す必要があるのでした.remoteEnvPATH に 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.jsonDockerfile は次のとおりです:

{
  "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 では,今回説明したような問題についてちゃんと解決されていたようなんですよね:

https://github.com/haskell/docker-haskell/issues/76

https://github.com/microsoft/vscode-dev-containers/pull/1478

リポジトリの移行があったみたいで,廃止されたのはこのへんが原因なのかもしれません:

https://github.com/microsoft/vscode-dev-containers/issues/1762

また Dev Container で簡単にセットアップできる日が来るとうれしいですね.

GitHubで編集を提案

Discussion