今回作るコマンドラインツールの題材
※読み飛ばしてOK
私は同人誌PDFの作成にRe:Viewを使用しています。Re:Viewでは参考文献の定義方法を次のように定めています。
参考文献は同一ディレクトリ内の bib.re ファイルに定義します。
//bibpaper[cite][キャプション]{…コメント…}
コメントは省略できます。
//bibpaper[cite][キャプション]
なので私はbib.reにこんな感じで書いています。
= 参考文献
//bibpaper[github.com/apple/swift]["apple/swift: The Swift Programming Language", @<href>{https://github.com/apple/swift}]
これが1本や2本だけならいいですが、参考文献が数十本になってくるとだるすぎます。URLのリストだけ渡したらいい感じにbib.reを作って欲しい。
今回Bazelで作るもの
前述の目的を達成するため、BazelとSwiftを利用して以下のものを作成します。
- URL一覧のテキストファイルとテンプレートファイルからbib.reを出力してくれるコマンドラインツール
-
./BibGenerator <input> <output> -t <template>
な感じ - 既存のSwiftライブラリ(swift-argument-parser、Kanna、Stencil)を使って楽に作りたい
- でもSwift Package Managerには頼らない(Package.swiftは書かない使わない)
-
- 既定のテキストファイルとテンプレートファイルを使って上記コマンドラインツールを実行するやつ
-
./run_generator
で↑のコマンドをさくっと実行してほしい - これができたらbib.re生成〜PDF生成の流れができそう
-
Bazelでは、
- rule(ソースファイルに対するコンパイル実行やファイル出力といった処理 = actionを組み合わせたもの)を定義して
- ruleと入力ファイルからtargetを作成(インスタンス化)して
- targetをビルドして欲しいものを出力する
という順番を踏みます。今回はSwiftを使うこともあり以下のruleが必要です。
- Swiftファイルを入力としてSwiftコンパイラを実行しSwiftライブラリを出力するrule
- 今回はbazelbuild/rules_swiftの
swift_library
ruleを使います
- 今回はbazelbuild/rules_swiftの
- Swiftファイルを入力としてSwiftコンパイラを実行し実行可能バイナリを出力するrule
- 今回はbazelbuild/rules_swiftの
swift_binary
ruleを使います
- 今回はbazelbuild/rules_swiftの
- テキストファイルとテンプレートファイルを入力として実行可能バイナリを実行しreファイルを出力するrule
そしてこれらのruleを元に
- reファイルを出力する実行可能バイナリ
BibGenerator
をビルドするためのBibGenerator
target -
BibGenerator URL一覧.txt bib.re -t template.stencil
を実行するrun_generator
target
を作成することを考えます。
ディレクトリ構成
Bazelは構成に特に決まりがないので、SPMに合わせることにします。
BibGenerator
├── WORKSPACE
├── BUILD
├── rules.bzl
├── Sources
│ └── BibGenerator
│ └── main.swift
└── externals
├── swift-argument-parser.BUILD
├── Kanna.BUILD
├── PathKit.BUILD
└── Stencil.BUILD
ポイント
- WORKSPACEファイルをプロジェクトルート直下に置きます。これでBibGeneratorディレクトリがworkspaceと見なされます。
- BUILD(targetを書いておくファイル)を用意しておきます。
- rules.bzl(ruleを定義しておくファイル)を用意しておきます。
- 各リポジトリの名前のついたBUILD(依存ライブラリ用のtargetを書いておくファイル)を用意しておきます。各リポジトリのPackage.swiftの代わりになります。
Bazel用のファイルを用意する
Swiftソースファイル以外に次のファイルが必要となりました。
- WORKSPACE
- BUILD
- rules.bzl
- swift-argument-parser.BUILD
- Kanna.BUILD
- PathKit.BUILD
- Stencil.BUILD
依存ライブラリを使えるようにする
今回使いたいSwiftライブラリやbazelbuild/rules_swiftは全てGitHub上で公開されています。なので
- GitHubから各ライブラリのソースファイルをダウンロードする
- bazelbuild/rules_swiftをダウンロードする
-
swift_library
ruleを使い、リポジトリ名.BUILDに各ライブラリ用のtargetを書いておく
という作業が必要です。1と2のことはWORKSPACEファイルに、3のことは各リポジトリ名.BUILDに記述します。
ソースファイルとruleのダウンロード
インターネット上のソースファイルやruleをダウンロードするには http_archive
ruleを使って必要事項をWORKSPACEに書きます。 http_archive
ruleを使えるようにするには load
文でインポートしなければなりません。
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
これで http_archive
ruleを使えるようになりました。続けて必要なものをダウンロードするtargetを書きます。
https://github.com/bazelbuild/rules_swift にはSwift関係の便利なruleがたくさん定義されています。WORKSPACEへ書くことがREADME.mdにまとまっているのでコピペしておきます。
http_archive(
name = "build_bazel_rules_swift",
sha256 = "d2f38c33dc82cf3160c59342203d31a030e53ebe8f4c7365add7a549223f9c62",
url = "https://github.com/bazelbuild/rules_swift/releases/download/0.15.0/rules_swift.0.15.0.tar.gz",
)
load(
"@build_bazel_rules_swift//swift:repositories.bzl",
"swift_rules_dependencies",
)
swift_rules_dependencies()
load(
"@build_bazel_rules_swift//swift:extras.bzl",
"swift_rules_extra_dependencies",
)
swift_rules_extra_dependencies()
依存ライブラリをダウンロードするtargetも書いておきます。PathKitはStencilの依存ライブラリです。残念ながらStencilは(というか大抵のものは)Bazel対応をしていないため依存先のぶんも用意してやらないとビルドできません。
http_archive(
name = "swift-argument-parser",
url = "https://github.com/apple/swift-argument-parser/archive/0.3.1.tar.gz",
sha256 = "563592e29092d8e551d54c9833623d9f78c9ef95e9107caea6ab1645aaade5aa",
build_file = "//:externals/swift-argument-parser.BUILD",
strip_prefix = "swift-argument-parser-0.3.1"
)
http_archive(
name = "Kanna",
url = "https://github.com/tid-kijyun/Kanna/archive/5.2.3.tar.gz",
sha256 = "9aad278e9ec7069a4c06d638c8b21580587e93a67e93f488dabf0a51cd275265",
build_file = "//:externals/Kanna.BUILD",
strip_prefix = "Kanna-5.2.3"
)
http_archive(
name = "PathKit",
url = "https://github.com/kylef/PathKit/archive/1.0.0.tar.gz",
sha256 = "6d45fb8153b047d21568b607ba7da851a52f59817f35441a4656490b37680c64",
build_file = "//:externals/PathKit.BUILD",
strip_prefix = "PathKit-1.0.0"
)
http_archive(
name = "Stencil",
url = "https://github.com/stencilproject/Stencil/archive/0.14.0.tar.gz",
sha256 = "1f20c356f9dd454517e1362df7ec5355aee9fa3c59b8d48cadc62019f905d8ec",
build_file = "//:externals/Stencil.BUILD",
strip_prefix = "Stencil-0.14.0"
)
http_archive
ruleの各引数1の説明
-
name
:適当に分かりやすい名前をつけておきます(ここではリポジトリ名をつけています) -
url
:圧縮ファイルのURL。zipでも可。 -
sha256
:圧縮ファイルのsha256。Bazel対応済みの親切なものならREADME.mdやRelease tagのページのあたりに書いてくれています。 -
build_file
:対応するBUILDのパス。labelという形式で書くことになっています。今回の場合は//:
に続けてファイルまでのパスを書いておけば大丈夫(本当は@
から書くべき)。 -
strip_prefix
:GitHubから落としたソースファイルは「リポジトリ名-x.y.z」というディレクトリに梱包されているので、これがプロジェクトルートであることを示しておきます。
依存ライブラリ用にBUILDを用意する
今回使用するSwiftライブラリはどれもBazel非対応なので、使う側がtargetを書いて http_archive
ruleに渡しておく必要があります。
swift_library
ruleも load
した上で使いましょう。
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "ArgumentParser",
module_name = "ArgumentParser",
srcs = glob(["Sources/ArgumentParser/**/*.swift"]),
visibility = ["//visibility:public"]
)
swift_library
の引数説明
-
name
:いい感じにつけておきます。ここではモジュール名をつけています。 -
module_name
:デフォルトだと長い名前がついてややこしくなる場合があるので明記しておくとよし(今回はなくても同じだけど) -
srcs
:Swiftソースファイルを指定します。全部手動で列挙してもいいですが、glob
関数と*
の合わせ技でまるっと指定することもできます。今どきのSwiftライブラリはSPM向けにSources以下にまとめてあるのでSources/**/*.swift
などと書いておけば足ります。 -
visibility
:アクセス修飾子みたいなやつ。"//visibility:public"
と書くと誰でも参照できるようになります。
他の依存ライブラリも同じように書いておきます。
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "Kanna",
module_name = "Kanna",
srcs = glob(["Sources/Kanna/*.swift"]),
visibility = ["//visibility:public"]
)
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "PathKit",
module_name = "PathKit",
srcs = glob(["Sources/*.swift"]),
visibility = ["//visibility:public"]
)
StencilにはPathKitに依存しているということも書いておく必要があります。
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "Stencil",
module_name = "Stencil",
srcs = glob(["Sources/*.swift"]),
deps = [
"@PathKit//:PathKit"
],
visibility = ["//visibility:public"]
)
-
deps
:依存先targetを書いておきます。Stencilの場合はPathKitに依存しているのでPathKitを指定しておきます。これもlabel形式で書く必要があり、今回の場合は@WORKSPACE内に書いたname//:各リポジトリ名.BUILD内に書いたname
などとしておけばよいです。
ライブラリによってはさらに記述しなければいけないこともありますが、今回使うものは以上の項目だけで足ります。
試しに依存ライブラリをビルドしてみる
bazel build @swift-argument-parser//:ArgumentParser
bazel build @Kanna//:Kanna
bazel build @PathKit//:PathKit
bazel build @Stencil//:Stencil
bazel-bin/externalsを見てみるとビルドされたものがあるはずです。
bazel-bin/external
└── swift-argument-parser
├── ArgumentParser-Swift.h
├── ArgumentParser.swiftdoc
├── ArgumentParser.swiftmodule
└── libArgumentParser.a
BUILDを用意する、ruleの定義もする
バイナリを作るところまで
各ライブラリを用意したので、次はいよいよ自分の BibGenerator
をビルドできるようにします。 swift_binary
も load
してから使います。
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_binary")
swift_binary(
name = "BibGenerator",
srcs = glob([
"Sources/BibGenerator/*.swift"
]),
deps = [
"@swift-argument-parser//:ArgumentParser",
"@Kanna//:Kanna",
"@Stencil//:Stencil"
]
)
あとはいい感じにSwiftを書いて BibGenerator
の完成です。
bazel build //:BibGenerator
./bazel-bin/BibGenerator urls.txt bib.re -t bib.stencil
Bazelらしくバイナリを実行してみる
BibGenerator
を既定のファイルと共に実行するtargetも作ってみます。しかし BibGenerator
を実行するruleはこの世に存在していないため自分で作らなければいけません。そのようなruleをrules.bzlに定義した上でBUILDにtargetを作成します。
こんなtargetが作れるruleが欲しい!↓↓↓
load("//:rules.bzl", "run_generator")
run_generator(
name = "generate",
template = "//:bib.stencil",
list_of_urls = "//:URLs.txt",
output_file_name = "bib.re",
)
ruleの定義は rule
関数を使用してbzlファイル内に書きます。今回はrules.bzlに run_generator
というruleを定義しています。ruleの実装は引数が ctx
ただ1つだけの関数(ここでは _impl
関数)に書いておきます。一方引数は rule
関数の引数 attrs
に辞書型で書いておきます。
def _impl(ctx):
# 省略。_implの中身についてはまた後で!
run_generator = rule(
implementation = _impl,
attrs = {
"template": attr.label(
mandatory = True,
allow_single_file = True,
),
"list_of_urls": attr.label(
mandatory = True,
allow_single_file = True,
),
"output_file_name": attr.string(
mandatory = True
),
"_generator": attr.label(
default = Label("//:BibGenerator"),
executable = True,
cfg = "exec",
),
}
)
rule
関数の引数について:
-
implementation
:そのruleの処理を実装した関数を渡します。引数ctx
だけを持つ関数でないといけません。 -
attrs
:そのruleの引数を書いておきます。attr.label
なら(ここではファイル識別子として)labelを受け取る引数、attr.string
なら文字列を受け取る引数になります。- 引数
name
はBazelが勝手に用意するので私たちは用意しなくてOK。 - 名前の先頭に
_
がついているとprivateな引数となり外から与えることができなくなります。privateな引数を用意したのは、//:BibGenerator
に依存したい、でもtargetを作成する側に指定されたくはないからです。 -
cfg
にはexec
/host
/target
のいずれかを設定します。今回のBibGenerator
を実行するのは開発機なのでexec
にしておきます。
- 引数
ruleの実装について:
もう1度 run_generator
ruleを見てみましょう。引数が4つあります(+ privateな _generator
)。
run_generator(
name = "generate",
template = ":bib.stencil",
list_of_urls = ":URLs.txt",
output_file_name = "bib.re",
)
しかし run_generator
ruleの実装 _impl
には引数 ctx
しかありません。ではどうやって渡された値を取得するかというと、 ctx.ナントカ.引数名
というかたちで受け取ります。今回なら
引数名 | 取得方法 |
---|---|
name |
ctx.attr.name |
template |
ctx.file.template |
list_of_urls |
ctx.file.list_of_urls |
output_file_name |
ctx.attr.output_file_name |
_generator |
ctx.executable._generator |
となります。( attr.label
で allow_single_file = True
を指定した引数からは File
が得られます)
実行可能ファイルの実行には ctx.actions.run
を呼びます。引数 executable
と arguments
を持っており、それぞれ実行可能ファイル(ここでは ctx.executable._generator
)と引数(ここでは <input> <output> -t <template>
)を渡せばよさそうです。しかしBazelでは書くべきことが他にもあります。
先に完成形はこちら:
def _impl(ctx):
# ファイルを作成することを宣言
output = ctx.actions.declare_file(ctx.attr.output_file_name)
# <input> <output> -t <template>
args = [ctx.file.list_of_urls.path, output.path, "-t", ctx.file.template.path]
ctx.actions.run(
inputs = [ctx.file.list_of_urls, ctx.file.template],
outputs = [output],
arguments = args,
executable = ctx.executable._generator
)
return [DefaultInfo(files = depset([output]))]
Bazelには以下のような特徴があります。
- actionは入力ファイルと出力ファイルの関係性を示すもの
- /private/var/tmp下(※macOSの場合)に入力ファイルのsymlinkを用意し、そこでビルドを行う。出力ファイルの実体もそこに作られる
- これのおかげでソースファイルをおいたディレクトリが散らかされない
なので、actionに実行可能バイナリや引数を渡すだけ、ファイルに出力したらそれっきり……ではいけません。何を入力に受け取って何を出力するのかを明確にする必要があります。引数 inputs
に必要な入力ファイルを渡さないと/private/var/tmp下にsymlinkが作られず上手くいきません。また、引数 outputs
には何かしら指定しなければエラーになり、指定したファイルが作成されていなければやはりエラーになります。
以上のことを踏まえて def _impl
を実装していきます。
ctx.actions.declare_file
はそのruleがファイルを作成するということを宣言するものです。ただしこれだけでは作成されず、他のaction(ここでは ctx.actions.run
)で出力されなければなりません。ここで宣言したファイルを使って args
を組み立てたり、 ctx.actions.run
の出力に指定したり、最後に返却してこのruleの出力であることを示したりします。
ctx.actions.run
の引数:
-
inputs
:入力ファイルを渡します。書かないでいると、/private/var/tmpに入力ファイルのsymlinkが作成されないためBibGenerator
がファイルを発見できなくなります。 -
outputs
:必須。 -
arguments
:いつもの感じで渡します。ファイルのパスはpath
で取得できます -
executable
:実行可能ファイルを渡します。
最後にこのruleの出力を返却します。 DefaultInfo
は出力ファイルに関する情報を示すためのものです。Bazelは不必要な処理をしないので、怠ると「(nothing to build)」と言われて何も実行されない&出力されないまま終わります。
ファイルのパス取得&出力の注意点
run_generator
targetでは引数 ctx
を介してファイルのパスを BibGenerator
に渡し実行しました。先に述べたように、Bazelは/private/var/tmp下でビルドを行い出力を行います。workspaceディレクトリが~/Documents下などにあったとしても、ビルド時のカレントディレクトリは
// "/private/var/tmp/_bazel_ユーザ名/略/execroot/workspace名"
print(FileManager.default.currentDirectoryPath)
となります。
inputやtemplateは相対パスで渡されてきます。
// "URLs.txt"
print(input)
// "bib.stencil"
print(template)
もしmain.swiftと同じ場所に置いて :Sources/BibGenerator/URLs.txt
と指定していれば Sources/BibGenerator/URLs.txt
が来ます。
ctx.actions.declare_file
で宣言したoutputも相対パスで渡されてきます。Bazelの出力先はOSやビルド設定によって変わるので例えば
// "bazel-out/darwin-fastbuild/bin/bib.re"
print(output)
となります。つまりoutputの絶対パスは
/private/var/tmp/_bazel_ユーザ名/略/execroot/workspace名/bazel-out/darwin-fastbuild/bin/bib.re
です。ここにきちんとbib.reが生成されていないと(間違った場所にbib.reを生成してしまうと)
ERROR: 略: output 'bib.re' was not created
ERROR: 略: not all outputs were created or valid
とエラーになります。
まとめ?
Swift Package Managerを使った方がもちろん楽です。
もう少し複雑な例、例えば
-
BibGenerator
をビルド -
BibGenerator
を実行してbib.reを出力 - 他のreやyml合わせて
review-pdfmaker
でPDFを出力
の一連の流れを実行してくれるtargetを作ればかなり恩恵を得られそう……ですが、
PDFの出力先およびplantuml.jarの位置はプロジェクトフォルダ(config.ymlがあるところ)直下前提
( https://twitter.com/kmuto/status/1168782986176692224 )
というRe:Viewの仕様と、所定の位置に出力ファイルが置かれていないとエラーになるBazelの仕様が合わない問題が発生します。(一応この問題はどうにかなるけど)
-
Bazelではruleの引数のことを特別にattributeと呼びますが、本記事ではわかりやすさを優先して引数と呼ぶことにします。 ↩