rules_elm を作る
Elm 用の Bazel ルールがないので作ったという話です. 正確には EdSchouten/rules_elm がありますが,最新バージョンの 0.19.1 には対応してなかったので,対応したものを自作しました.
作ったもの
まず作ったのは:
- Elm コンパイラをインストールする(Toolchain)
elm make
をする Bazel ルール(elm_make
)- Windows でも動作する
要するに elm make
をできるようにしただけ.
作る
Elmコンパイラを取得する
これが結構めんどくさかった. というのも,基本的になんらかのバイナリをとってくる場合は repository_ctx.download
を使い,ダウンロード対象が zip
や tar.gz
でついでに展開する場合は repository_ctx.download_and_extract
を使う. しかし,Elm コンパイラは gz
だけでこれは repository_ctx.download_and_extract
で展開できない. 困った.
Bazel 仲間に知恵をもらった結果,次のように repository_ctx.download
でふつーに落としてきて gzip
で展開するようにした(無理やり):
def _elm_compiler_impl(ctx):
= ctx.attr.os
os = ctx.attr.version
version = "elm-{}".format(os)
file_name
ctx.download(= "https://github.com/elm/compiler/releases/download/{}/binary-for-{}-64-bit.gz".format(version, os),
url = ctx.attr.checksum,
sha256 = file_name + ".gz",
output
)"gzip"), "-d", file_name + ".gz"])
ctx.execute([ctx.which("chmod"), "+x", file_name])
ctx.execute([ctx.which( ...
elm make
をするルールを作る
こっちで大変だったのは,なんとか Windows でも動作するようにすることだった. というのも,できれば Elm プロジェクトがリポジトリのルートに無い場合でも動作するようにしたくて,この場合は生成物(--output
の引数)や elm バイナリを絶対パスにしたい. しかし,Windows の動作も考慮するとシェルスクリプトでは絶対パスへの変換をうまく動かすことが難しい.
ということでいろいろ試行錯誤した結果,最終的には Python を噛ませることでお茶を濁すことにした. 下記のような Python スクリプトをテンプレートで生成し:
#!/usr/bin/env python3
# elm_wrapper.py ELM_PROJECT_ROOT [ARGS_FOR_ELM...]
# 1引数目の ELM_PROJECT_ROOT だけ Elm プロジェクトへの相対パスで残りは elm コマンドへの引数
import os
import os.path
import subprocess
import sys
def run(cmd, *args, **kwargs):
try:
=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, *args, **kwargs)
subprocess.run(cmd, checkexcept subprocess.CalledProcessError as err:
buffer.write(err.stdout)
sys.stdout.buffer.write(err.stderr)
sys.stderr.raise
= os.path.abspath("path/to/elm") # ここはテンプレート
elm_runtime_path = sys.argv.pop(1)
elm_project_root for i, arg in enumerate(sys.argv):
if arg == "--output":
+1] = os.path.abspath(sys.argv[i+1])
sys.argv[i
# HOME: getAppUserDataDirectory:getEnv: does not exist (no environment variable)
# というエラーが出るので適当に定義しておく
"HOME", os.getcwd())
os.putenv(
os.chdir(elm_project_root)+ sys.argv[1:]) run([elm_runtime_path]
これを py_binary
で固めておいて次のように利用する:
def _elm_make_impl(ctx):
= ctx.toolchains["@rules_elm//elm:toolchain"].elm
elm_compiler = ctx.actions.declare_file(ctx.attr.output)
output_file
ctx.actions.run(= ctx.executable._elm_wrapper,
executable = [
arguments file.elm_json.dirname,
ctx."make", ctx.attr.main, "--output", output_file.path,
],= [elm_compiler, ctx.file.elm_json] + ctx.files.srcs,
inputs = [output_file],
outputs
)return [DefaultInfo(files = depset([output_file]))]
= rule(
elm_make
_elm_make_impl,= {
attrs "srcs": attr.label_list(allow_files = True),
"elm_json": attr.label(mandatory = True, allow_single_file = True),
"main": attr.string(default = "src/Main.elm"),
"output": attr.string(default = "index.html"),
"_elm_wrapper": attr.label(
= True,
executable = "host",
cfg = Label("@rules_elm//elm:elm_wrapper"), # py_binary で固めたやつ
default
),
},= [
toolchains "@rules_elm//elm:toolchain",
] )
この方法は tweag/rules_haskell
の cabal コマンド関連でも同様のことをしている(目的が同じかはわからないが参考にした).
使う
試しに使った:
mixlogue は Haskell + Elm の簡単なプログラム. この PR では Haskell のビルドも Bazel にしている.
課題
- 依存パッケージを Bazel で管理していないので毎回依存パッケージのインストールからする
- もっと Toolchain を活用する
(1)は単純な話. 普通 Bazel は依存パッケージを明示的に記述することで,無駄に依存パッケージを何回もインストールしようとするのを防ぐ方法をとる. しかし,elm_make
は雑に作ったので毎回インストールしちゃうっていう.
(2)は,Toolchain の action
なんかに elm
コマンドの振る舞いを突っ込んだ方がかっこいいかなーっていうだけ.
次回,頑張る.