LoginSignup
5
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-04

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

※読み飛ばしてOK

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

参考文献は同一ディレクトリ内の 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
  • Swiftファイルを入力としてSwiftコンパイラを実行し実行可能バイナリを出力する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から各ライブラリのソースファイルをダウンロードする
  2. bazelbuild/rules_swiftをダウンロードする
  3. swift_library ruleを使い、リポジトリ名.BUILDに各ライブラリ用のtargetを書いておく

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

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

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

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

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

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の説明

  • 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"]
)
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に依存しているということも書いておく必要があります。

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 などとしておけばよいです。

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

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

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

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 の完成です。

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が欲しい!↓↓↓

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)。

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.labelallow_single_file = Trueを指定した引数からは File が得られます)

実行可能ファイルの実行には ctx.actions.run を呼びます。引数 executablearguments を持っており、それぞれ実行可能ファイル(ここでは 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の仕様が合わない問題が発生します。(一応この問題はどうにかなるけど)


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

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1