Bazelのマクロでコード生成を自動化（+ツールの管理を任せる）

この記事は[OpenSaaS Studio Advent Calendar 2019](https://adventar.org/calendars/3928)、12日目の記事です。[前回](https://qiita.com/mmmknt/items/cb23a597dfb5a4ca6353)は依存ライブラリの管理について触れましたが、今回は手動で実施していたビルド処理をBazelの管理スコープに入れよう、というお話。

# はじめに
Bazelでは公式や有志によって[様々なルール](https://docs.bazel.build/versions/1.2.0/rules.html)が提供されており、それらを利用することでプログラミング言語やDockerイメージのビルドなど色々な処理を定義することができます。また、既存のルールでは要件を満たせない場合に新しくルールやマクロを定義することで処理の自動化やビルドに必要なツールの管理をBazelに移譲できます。
この記事では、私が実際に作ったマクロを元に新しい処理を追加する方法とその際に気をつけたポイントを紹介します。

# マクロ化する処理
**PEGで定義された規則**から**Golang用パーサのソースコード**を生成する処理。[PEG](https://ja.wikipedia.org/wiki/Parsing_Expression_Grammar)を使うと`sample.peg`のように独自の記法で構文を定義できます。詳細は今回の記事に関係ないので省略。
ソースコードの生成には[pointlander/peg](https://github.com/pointlander/peg)というツールを使っていて、構文を変更すると（ツールが入っていなければ）ツールのインストール、コードの生成、コミットしてリポジトリにpush、という作業が必要でした。

```PEG:sample.peg
package example

type Opt Peg {
    Params map[string][]string
}

Expression  <- ( KEY PATH )

KEY     <- < KEY_SYNTAX > &DOMAIN                       { p.addParam("prefix", text) }
PATH    <- DOMAIN < PATH_SYNTAX > ( QUESTION / EOF )    { p.addParam("path", text) }


##########################
#### Syntax
##########################
KEY_SYNTAX          <- [a-zA-Z0-9_*{}(),:;%#=\-+]+   # key can not contain Slash
PATH_SYNTAX         <- [a-zA-Z0-9_*{}(),.:;%#=\-+/]+  # key can contain Slash


DOMAIN              <- ('.localhost' PORT '/' )
PORT                <- ':' [0-9]+

##########################
#### Elements
##########################
QUESTION            <- '?'
EOF                 <- !.
```

# 必要なライブラリを定義
それではマクロを作っていきましょう。
これまでと同じくコード生成にはpointlander/pegを使います。ツールのダウンロードもBazelにやってもらいたいので、まずはツール（と依存ライブラリ）を定義します。

```Bazel:WORKSPACE
### Peg to go
# keep
go_repository(
    name = "com_github_pointlander_peg",
    importpath = "github.com/pointlander/peg",
    commit = "169894c6af14e914b8ee20035b2fe22f222d77af",
)

# keep
go_repository(
    name = "com_github_pointlander_jetset",
    importpath = "github.com/pointlander/jetset",
    commit = "eee7eff80bd4f18ff56a0eef8d5b005c8d592e7a",
)

# keep
go_repository(
    name = "com_github_pointlander_compress",
    importpath = "github.com/pointlander/compress",
    commit = "ff44bd196cc34cff192fd5c586f74b9ca7fc9a11",
)
```

ここで定義したライブラリは`go.mod`では定義されていないので、Gazelleで削除されないようにkeepコメントを付けています。([前回の記事](https://qiita.com/mmmknt/items/cb23a597dfb5a4ca6353#%E4%B8%8D%E8%A6%81%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E5%89%8A%E9%99%A4)を参照)

# マクロを作成
## spec
* PEGファイルと同じパッケージにGolangのソースコードを生成
* 生成するファイル名はPEGファイルの拡張子を`go`にしたもの（例えば、`sample.peg`->`sample.go`）
* 入力は1ファイルのみ

## 実装
Bazelには指定した入力を元にbashコマンドを実行してファイルを生成する[genrule](https://docs.bazel.build/versions/master/be/general.html#genrule)というルールがあるのでこれを使います。

```Bazel:peg_go.bzl
def peg_go(name, src):
    gosrc = src + ".go"
    native.genrule(
        name = name,
        srcs = [src],
        outs = [gosrc],
        cmd = "cp $(<) $(@D) && $(location @com_github_pointlander_peg//:peg) $(@D)/%s && rm $(@D)/%s" % (src, src),
        tools = ["@com_github_pointlander_peg//:peg"],
        visibility = ["//visibility:public"],
    )
```

`peg_go`の`src`に指定したPEGファイルを元にGolangのソースコードが生成され、生成されたコードは`name`で指定したラベルでアクセスします。
雰囲気でなんとなく分かると思うので`genrule`の詳細は[公式のドキュメント](https://docs.bazel.build/versions/master/be/general.html#genrule)にお任せしますが、分かりづらいところがあるので二点解説します。

* `cmd`で使用している`$(<)`や`$(@D)`といった記述は、`srcs`や`outs`で指定した内容に基づいた記法で、入力（のうち1ファイルだけの場合）や出力先ディレクトリを意味しています。詳細を知りたい場合は上で挙げているこのルールの公式ドキュメントや[変数に関するドキュメント](https://docs.bazel.build/versions/master/be/make-variables.html#predefined_variables.genrule.cmd)を参照してください。
* bashコマンドの実行後、`outs`で指定したファイルが存在しないとビルドエラーになります。`pointlander/peg`で生成したコードは入力ファイルと同じディレクトリに出力されるので、ツールを実行する前にPEGファイルを出力先ディレクトリに移動（`cp $(<) $(@D)`）し、実行した後にそのファイルを削除（`rm $(@D)/%s"`）しています。（出力先に関するノウハウをどこかで見た気がしますが、探しきれませんでした。。。）

### 入力/出力ディレクトリの補足
Bazelは独自のサンドボックス環境を使ってビルドをする仕組みになっており、ビルド成果物を入力ファイルと異なるディレクトリに出力する必要があります。[公式](https://docs.bazel.build/versions/master/output_directories.html)に詳細のドキュメントがあるので興味があれば確認してみてください。

# マクロの使用
定義したマクロを呼び出してソースコードを生成しましょう。

```Bazel:Build.bazel
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//:peg_go.bzl", "peg_go")

peg_go(
    name = "peg_go",
    src = "example.peg",
)

go_library(
    name = "go_default_library",
    srcs = [
        "example.go",
        ":peg_go",  # keep
    ],
    importpath = "github.com/mmmknt/rules_peg/tests/example",
    visibility = ["//visibility:public"],
)
```

`peg_go`の`name`で指定したラベルを`go_library`の`srcs`で指定しています。また、ここで生成されるコードはリポジトリに含めていないため、[Gazelle](https://github.com/bazelbuild/bazel-gazelle)でBazelのビルドファイルを生成する際に削除されないようkeepコメントを付けています。

# リポジトリの中に生成したコードへのシンボリックリンクを作る
急にイケてない作業が出てきましたが、Bazelの仕様に対応するためのワークアラウンドです。
[入力/出力ディレクトリの補足](https://qiita.com/mmmknt/items/573f5948b99ebf553e8f#%E5%85%A5%E5%8A%9B%E5%87%BA%E5%8A%9B%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E3%81%AE%E8%A3%9C%E8%B6%B3)で取り上げましたが、Bazelの成果物はリポジトリ配下には生成されません。（出力ディレクトリへのシンボリックリンクができるので、そこから辿ることはできる）
このままではIDEが認識してくれないorパッケージを間違って認識してしまうため、リポジトリの中の想定しているディレクトリの中にシンボリックリンクを追加できるようにします。

```bash:hack/expose-generated-go.sh
#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

OS="$(go env GOHOSTOS)"
ARCH="$(go env GOARCH)"
ROOT=$(dirname ${BASH_SOURCE})/..

expose_package () {
	local out_path=$1
	local package=$2
	local old_links=$(eval echo \$$3)
	local generated_files=$(eval echo \$$4)

	# Compute the relative_path from this package to the bazel-bin
	local count_paths="$(echo -n "${package}" | tr '/' '\n' | wc -l)"
	local relative_path="../../"

	# Delete all old links
	for f in ${old_links}; do
		if [[ -f "${f}" ]]; then
			echo "Deleting old link: ${f}"
			rm ${f}
		fi
	done

	# Link to the generated files
	local found=0
	for f in ${generated_files}; do
		if [[ -f "${f}" ]]; then
			found=1
			local base=${f##*/}
			echo "Adding a new link: ${package}/${base}"
			ln -nsf "${relative_path}${f}" "${package}/"
		fi
	done
	if [[ "${found}" == "0" ]]; then
		echo "Error: No generated file was found inside ${out_path} for the package ${package}"
		exit 1
	fi
}

##################
# For peg go files
##################

# Build peg go files
for label in $(bazel query 'attr(generator_function, peg_go, //...)'); do
	bazel build "${label}"
done

# Link to the generated files and add them to excluding list in the root BUILD file
for package in $(bazel query 'attr(generator_function, peg_go, //...)' --output package); do
	# Compute the path where Bazel puts the files
	out_path="bazel-bin/${package}"

	old_links=${package}/*.peg.go
	generated_files=${out_path}/*.peg.go
	expose_package ${out_path} ${package} old_links generated_files
done
```

Golangのコード生成した後に[Bazelのquery](https://docs.bazel.build/versions/master/query-how-to.html)で出力先を取得、その結果を使ってリポジトリの中にシンボリックリンクを作っています。
作ったシンボリックリンクは消したいですよね、ということでこんなスクリプトもあります。

```bash:hack/unlink-go.sh
#!/bin/sh

for f in $(find . -name \*\.peg\.go -not -path "./vendor/*"); do
	echo ${f}
	unlink ${f}
done;
```

# まとめ

* マクロ化することでBazelのビルドプロセスに組み込める
* Bazelに組み込むことで、ツールの管理をBazelに任せられる
* 生成したコードをリポジトリに追加する手間が増えた

Bazelのスコープに組み込むことで楽になるところもありつつ、Bazelのコンセプトを考えるとどうしてもワークアラウンドが必要になるところがある、という結果になりました。
Bazelが特定の言語に寄っていなかったり、ベストプラクティスが定まっていなかったり、ということもありどうしても新しいワークアラウンドを自分たちで考える必要があると思うので、チームの方向性やビルドプロセス全体のバランスを考えつついい感じに導入していきたいですね！（cloneしたらすぐ開発できるように生成したコードもコミットしとく、とか）

# おまけ
* [Bazelisk](https://github.com/bazelbuild/bazelisk)を使えばBazel自体のバージョンも管理できます。
* 今回の内容はリポジトリにまとめてあるので、もう少し整理したらリンク貼ります。