Bazel で生成したファイルを Haskell から参照する
前に Elm のコードを Bazel でビルドするためのルールを作りました. この生成物を Bazel でビルドしてる Haskell アプリケーションから参照する方法のメモ書きです.
Bazel生成物を参照する方法
思い付いた方法は2つ:
- Bazel で Docker イメージを作るときに含めて動的に参照する
- Bazel で Haskell をビルドするときに埋め込む(Template Haskell)
前者は Docker で固めるだけなので,Haskell 側で特別なことをする必要がなく簡単. 後者は rules_haskell と Haskell 側で試行錯誤する必要があるが,Haskell アプリケーション単体で完結するので便利だ.
Docker イメージに含める
Bazel を利用して Docker イメージを作る場合は次のように書く:
pkg_tar(= "bin",
name = [":app"],
srcs = "0755",
mode = "/usr/local/bin",
package_dir
)
container_image(= "image",
name = "@haskell_base//image",
base = [":bin"],
tars = ["/usr/local/bin/app"],
entrypoint )
:app
は,例えば rules_haskell の haskell_binary
などで生成した実行ファイル(の Bazel 生成物)だ. container_image
にファイルを直接渡す場合は files
属性を利用する:
container_image(= "image",
name = "@haskell_base//image",
base = [":bin"],
tars = [":mainjs"], # ココ
files = ["/usr/local/bin/app"],
entrypoint )
:mainjs
は,例えば rules_elm の elm_make
などで生成したファイルだ. この場合,ルートディレクトリ直下に生成物が置かれる. 任意のパスにしたい場合は directory
属性を使えば良い:
container_image(= "image",
name = "@haskell_base//image",
base = [":bin"],
tars = [":mainjs"],
files = "/work/static", # ココ
directory = "/work",
workdir = ["/usr/local/bin/app"],
entrypoint )
しかし,この場合 :bin
のパスも変わってしまう(今回の場合は /work/static/usr/local/bin/app
になってしまう). そこで,:mainjs
も :bin
のように pkg_tar
を介すようにすれば良い:
pkg_tar(= "static",
name = [":mainjs"],
srcs = "0444",
mode = "/work/static"
package_dir
)
container_image(= "image",
name = "@haskell_base//image",
base = [":bin", ":static"],
tars = "/work",
workdir = ["/usr/local/bin/app"],
entrypoint )
これで実行ファイルは /usr/local/bin
にあって,Haskell アプリケーション側で読み込む静的ファイルは /work/static
にあるようにできた. ただし,こっちの方法の問題として Docker イメージまで作らないと手元で動作確認ができない点がある. それでは不便な場合は,次の実行ファイル自体に静的ファイルを埋め込んでしまう方法をとると良い.
Haskell に埋め込む
Haskell でコードを埋め込むには Template Haskell を利用する. ちなみに,実際の作業PRはこちら.
今回は file-embed パッケージを利用する:
import Data.FileEmbed (embedDir)
import Servant
type API = "static" :> Raw
server :: ServerT API (RIO Env)
= serveDirectoryEmbedded $(embedDir "./static") server
rules_haskell で Template Haskell などのために Haskell のソースコード以外を渡す場合には extra_srcs
属性を利用する(参照):
haskell_library(= "homelyapp-library",
name = "src",
src_strip_prefix = glob(["src/**/*.hs"]),
srcs = [
deps "@stackage//:base",
...
],= [":mainjs"], # ココ
extra_srcs = GHC_FLAGS,
compiler_flags )
extra_srcs
に渡しているのが Bazel の生成物でない場合はこれでうまくいくが,Bazel の生成物を渡した場合はこれだけではうまくいかない. というのも,Bazel の生成物のパスが "./static"
ではないからだ. :mainjs
自体は ./static/main.js
という設定で生成したが,Bazel サンドボックスにおいてはカレントパスが変わってしまう(Bazelサンドボックスと各種ディレクトリ構造について).
ではどうすれば良いか. 一応,パスの類推は可能だが,Haskellコード側に Bazel 専用のパスをハードコードするのはいやだ. で,いろいろと Issue を漁っていたらドンピシャなものを見つけた. Issue のコメント曰く,次のようにしてあげれば良い:
haskell_library(= "homelyapp-library",
name = "src",
src_strip_prefix = glob(["src/**/*.hs"]),
srcs = [
deps "@stackage//:base",
...
],= [":mainjs"],
extra_srcs = [
compiler_flags "-DMAINJS_FILE=\"$(execpath :mainjs)\"", # ココ
+ GHC_FLAGS,
] )
-D
オプションはCプリプロセッサによる変数を外部から与えるやつで,MAINJS_FILE
変数に execpath :mainjs
の結果を与えている. execpath
は Bazel 専用の特殊な変数展開らしく,サンドボックスのルートから与えた Bazel 生成物への相対パスを返す. つまり,これを embedDir
に渡してやれば良い:
{-# LANGUAGE CPP #-}
server :: ServerT API (RIO Env)
= serveDirectoryEmbedded $(embedDir (takeDirectory MAINJS_FILE)) server
おまけ:Hazell の修正
compiler_flags
をいじった結果,Hazell が動作しなくなったので直した. 問題箇所は2つ:
- リストの結合(
+
)は未対応 - 文字列の
"
のエスケープが未対応
両方対応した:
リスト結合はだいぶ雑に実装しており,もし別の式をパースしたくなった場合はまるっと書き直す必要がある(演算子の優先順位などを考慮していないため).
おしまい
ちなみに,僕は後者の埋め込みを利用することにしました. こっちだと,bazel run
のワンコマンドでアプリケーションをローカルで起動できるからです.