Swift Package Managerの代わりにBazelを使ってコマンドラインツールを作る

# 今回作るコマンドラインツールの題材

※読み飛ばしてOK

私は同人誌PDFの作成にRe:Viewを使用しています。Re:Viewでは参考文献の定義方法を次のように定めています。

https://github.com/kmuto/review/blob/master/doc/format.ja.md

> 参考文献は同一ディレクトリ内の bib.re ファイルに定義します。
>
> ```
> //bibpaper[cite][キャプション]{…コメント…}
> ```
> コメントは省略できます。
>
> ```
> //bibpaper[cite][キャプション]
> ```

なので私はbib.reにこんな感じで書いています。

```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では、

1. rule（ソースファイルに対するコンパイル実行やファイル出力といった処理 = actionを組み合わせたもの）を定義して
2. ruleと入力ファイルからtargetを作成（インスタンス化）して
3. targetをビルドして欲しいものを出力する

という順番を踏みます。今回はSwiftを使うこともあり以下のruleが必要です。

* Swiftファイルを入力としてSwiftコンパイラを実行しSwiftライブラリを出力するrule
  * 今回は[bazelbuild/rules_swift](https://github.com/bazelbuild/rules_swift)の `swift_library` ruleを使います
* Swiftファイルを入力としてSwiftコンパイラを実行し実行可能バイナリを出力するrule
  * 今回は[bazelbuild/rules_swift](https://github.com/bazelbuild/rules_swift)の `swift_binary` ruleを使います
* テキストファイルとテンプレートファイルを入力として実行可能バイナリを実行し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上で公開されています。なので

1. GitHubから各ライブラリのソースファイルをダウンロードする
1. bazelbuild/rules_swiftをダウンロードする
1. `swift_library` ruleを使い、リポジトリ名.BUILDに各ライブラリ用のtargetを書いておく

という作業が必要です。1と2のことはWORKSPACEファイルに、3のことは各リポジトリ名.BUILDに記述します。

### ソースファイルとruleのダウンロード

インターネット上のソースファイルやruleをダウンロードするには `http_archive` ruleを使って必要事項をWORKSPACEに書きます。 `http_archive` ruleを使えるようにするには `load` 文でインポートしなければなりません。

```bazel:WORKSPACE
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にまとまっているのでコピペしておきます。

```bazel:WORKSPACE
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対応をしていないため依存先のぶんも用意してやらないとビルドできません。

```bazel:WORKSPACE
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]の説明

[^1]: Bazelではruleの引数のことを特別にattributeと呼びますが、本記事ではわかりやすさを優先して引数と呼ぶことにします。

* `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` した上で使いましょう。

```swift-argument-parser.BUILD
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"` と書くと誰でも参照できるようになります。

他の依存ライブラリも同じように書いておきます。

```Kanna.BUILD
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"]
)
```

```bazel:PathKit.BUILD
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に依存しているということも書いておく必要があります。

```bazel:Stencil.BUILD
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` などとしておけばよいです。

ライブラリによってはさらに記述しなければいけないこともありますが、今回使うものは以上の項目だけで足ります。

### 試しに依存ライブラリをビルドしてみる

```sh
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` してから使います。

```bazel:BUILD
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` の完成です。

```sh
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が欲しい！↓↓↓

```bzl:BUILD
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` に辞書型で書いておきます。

```rules.bzl
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`）。

```bazel:BUILD
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では書くべきことが他にもあります。

先に完成形はこちら：

```rules.bzl
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下などにあったとしても、ビルド時のカレントディレクトリは

```main.swift
// "/private/var/tmp/_bazel_ユーザ名/略/execroot/workspace名"
print(FileManager.default.currentDirectoryPath)
```

となります。

inputやtemplateは相対パスで渡されてきます。

```main.swift
// "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やビルド設定によって変わるので例えば

```main.swift
// "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を使った方がもちろん楽です。

もう少し複雑な例、例えば

1. `BibGenerator` をビルド
2. `BibGenerator` を実行してbib.reを出力
3. 他のreやyml合わせて `review-pdfmaker` でPDFを出力

の一連の流れを実行してくれるtargetを作ればかなり恩恵を得られそう……ですが、

> PDFの出力先およびplantuml.jarの位置はプロジェクトフォルダ(config.ymlがあるところ)直下前提

（ https://twitter.com/kmuto/status/1168782986176692224 ）

というRe:Viewの仕様と、所定の位置に出力ファイルが置かれていないとエラーになるBazelの仕様が合わない問題が発生します。（一応この問題はどうにかなるけど）