CMakeの代替 (となってほしい)、Mesonチュートリアル

この記事はeeic （東京大学工学部電気電子・電子情報工学科） Advent Calendar 2018の17日目の記事として書かれました。昨日はyuyabathさんの[土屋言語への誘い](https://yuya.site/2018/12/15/invitation-for-tsuchiyalang/)、明日はhatiparallelさんの[eeicタベホ同好会の歩み](https://hatiparallel.hatenablog.com/entry/2018/12/17/234308)です。

[去年の超クソ雑configureとMeson比較](https://qiita.com/turenar/items/922a845cfc9350b0eece)に引き続き、Mesonについて書きます。昨日までmesonタグ2記事しかなくて悲しいﾅｧ

# 似たような記事

* [GStreamer Advent Calendar 2016](https://qiita.com/advent-calendar/2016/gstreamer)の[Hello Library World: The Meson Build System](http://hellolibraryworld.blogspot.com/2016/12/the-meson-build-system.html)
* [Tutorial](https://mesonbuild.com/Tutorial.html) (公式; 英語)
* [Getting started with Meson build system and C++](https://medium.com/@germandiagogomez/getting-started-with-meson-build-system-and-c-83270f444bee) (英語) どうしてCMakeではなくMesonを (書いた人が) 選んだのか、という理由もあります

メインの作者さんのCppCon 2018での[発表動画も英語ですがYouTubeに上がっています](https://www.youtube.com/watch?v=SCZLnopmYBM)。


# そもそもMesonってなにさ

> Meson is an open source build system meant to be both extremely fast, and, even more importantly, as user friendly as possible.
> -- The Meson Build System https://mesonbuild.com/

(意訳) Mesonは、驚異的な速さと、もっと重要な要素である、よりユーザーフレンドリーであることを意図したオープンソースビルドシステムです。

ビルドシステムって何じゃいなという人もいるかと思いますが、ビルドに関連するツールのことです。どういうことかわからないですね。ビルドは、コンパイルして実行ファイルを作る一連の流れ周辺のことを指す言葉です。Mesonはその中でもコンパイルの前準備を扱います。C/C++の分野でいうと、Mesonと似たようなものには、CMake、autotools (+automake)、GYPなどがあります。

このへんは見たほうがはやいかなあと思うので、次に行きましょう。

# Mesonのインストール

Mesonは前準備を扱うので、実際のコンパイルなどは[Ninja](https://ninja-build.org/)、Visual Studio、Xcodeなどを使います。コンパイル補助ツールとして有名 (？) なmakeは使えません。

MesonはPythonで書かれており、pipでインストールできます。大抵の人のPCはpythonとpipがインストールされているはずなので、

```console
$ pip install meson ninja --user
```

でインストールできます。

``--user``をつけたときのインストールされる場所は、```python -c 'import site; print(site.USER_BASE);'```で確認できます。

あとは、```python -c 'import site; print(site.USER_BASE);'```で出力されたディレクトリ直下のbinにPATHを通せば終わりです。

```bash
$ PATH="${PATH}:$(python -c 'import site; print(site.USER_BASE);')/bin"  # PATHを通します (bash向け)
$ meson --version
0.50.1  # インストールされたバージョンが出力されます。0.45以上がおすすめです
```

普通の*nix環境ならシステムのパッケージマネージャー (brew、apt、yum[^1]など) を使うことも出来ます。ただ、バージョンが古く使い物にならない場合があるので注意しましょう。

[^1]: epelレポジトリの追加が必要

Windowsなら[GitHubのreleasesページ](https://github.com/mesonbuild/meson/releases)から取得できるmsiインストーラーでもインストール出来ます。

# 使ってみる

というわけで、実際にmesonを使ってみましょう。

「プログラミングに関連することの最初は、Hello worldを出力することから始めるべきだ」のようなことは源氏物語にも書いてある[^2]ので、「Hello worldを出力するプログラム」をビルドしてみましょう。

[^2]: 嘘です

```c++:hello.c
#include <stdio.h>

int main() {
    printf("hello world\n");
}
```

```meson:meson.build
# hello worldから始めろとは源氏物語には書いてありませんが、
# meson.buildはprojectから始めるというのは世界の理です。
project('helloworld', 'c')

# hello.cをコンパイルしてhelloという名の実行ファイルを作成します
executable('hello', 'hello.c')
```

meson.buildにはシンタックスハイライトが効かないのは僕のせいではないので許してください。[GitHubならハイライトされます](https://github.com/turenar/meson-handson/blob/hello_world/meson.build)

上のファイルを作成したら、mesonを使ってビルドしてみましょう。

Visual Studio上でビルドしたい人は、Visual Studio 20XX Command Promptを開き[^10]、ソースコードのフォルダーに``cd``コマンドで移動した上で、

[^10]: 開く必要ないかも

```
meson build --backend vs
```

とすれば``build``フォルダーにVisual Studio用のslnファイルが作成されるはずです。作成されたら、Visual Studioで開いていつものようにビルドしてください。以下同様なので、Windowsの説明は省きます。

Xcode？``--backend xcode``でも渡せば動くんじゃね、知らんけど。

GCC/Clang+Ninjaを使う人は

```console
$ meson build/
$ cd build/
$ ninja
```

でビルドまで走ります[^11]。実行は``build/``の中で

[^11]: ``build/``の部分は、お好きなディレクトリ名で構いません。

```console
$ ./hello
hello world
```

です。なんか出力のしまりが悪いですね。``Hello world!``を出力するようにソースコードを書き換えてみますか。

```c++:hello.c
#include <stdio.h>

int main() {
    printf("Hello world!\n");
}
```

再ビルドのときは``meson``コマンドを実行する必要はありません。

```console
$ ninja
$ ./hello
Hello world!
```

# (中級編) もうちょっと頑張る

あーなんか突然数値の足し算を計算するだけのライブラリと、それを使うプログラム書きたくなってきたなー！！書きたくて書きたくて腕がうずいちゃうなー！！！もうこれは書くしかないなー！！！！！！

というわけで書きましょう。[ソースコードはcalcブランチにあります](https://github.com/turenar/meson-handson/tree/calc)

```c++:include/libperfectcalc/plus.hpp
#pragma once

int plus(int lhs, int rhs);
```

```c++:src/libperfectcalc/plus.cpp
#include "libperfectcalc/plus.h"

int plus(int lhs, int rhs) {
    return lhs + rhs;
}
```

```c++:src/main.cpp
#include <iostream>
#include "libperfectcalc/plus.h"

int main() {
    int lhs, rhs;
    std::cin >> lhs >> rhs;
    std::cout << plus(lhs, rhs) << std::endl;
}
```

```meson:meson.build
# c++のソースコードの場合はcppを2つ目の引数に渡します
project(
  'calculator', 'cpp',
  default_options: ['warning_level=3'])
# ↑名前付きの引数を渡すときは、名前も書く必要があります。
# どういう引数が渡せるかは https://mesonbuild.com/Reference-manual.html#project を見ましょう。
# ちなみに、default_optionsとして渡せるオプションは、ビルドディレクトリの中でmeson configureで一覧が見えます。


# includeディレクトリをincludeできるように、include_directoriesオブジェクトを作成します。(ただの呪文です)
includes = include_directories('include')


# subdir()で別のディレクトリのmeson.buildを読み込むことが出来ます。
subdir('src')
```

```meson:src/meson.build
subdir('libperfectcalc')

program(
    'calculator', ['main.cpp'],  # 勘の良い方は気づくかと思いますが、ソースファイルは複数指定できます (['a.cpp', 'b.cpp'])
    include_directories: includes,  # インクルードディレクトリの指定
    link_with: perfectcalc_lib)  # リンクするライブラリの指定
```

```meson:src/libperfectcalc/meson.build
# library() でライブラリを定義します。
# ただの library() の場合はデフォルトでは shared library (libperfectcalc.soなど) が作られます。
# meson build/ -Dbuildtype=static や meson configure -Dbuildtype=static で 
#   static library (libperfectcalc.a) が作られるようにも出来ます。

perfectcalc_lib = library(
    'perfectcalc', ['plus.cpp'],
    include_directories: includes)
```

``meson.build``の分割はぶっちゃけ気分です。全部一つのファイルにまとめてもいいです。

このまま``build/``の中で再ビルドしてもいいですが、``default_options``の効果が発揮されるのは最初の``meson``を呼び出すときなので、一度``build/``を消しちゃいましょう……[^4]

[^4]: こうやってなにも気にせずにビルドディレクトリを消せるのは、out-of-treeと呼ばれる、ソースコードとは別のディレクトリをビルドディレクトリにする形 (をmesonが強制する) だからですね

```console
$ rm -rf build/
$ meson build/
$ ninja -v  # ninjaに-vを渡すと、実行したコマンドをひとつずつ表示してくれます。
[1/5] ccache c++ -Isrc/libperfectcalc/src@libperfectcalc@@perfectcalc@sha -Isrc/libperfectcalc -I../src/libperfectcalc -I../include -fdiagnostics-color=always -pipe -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -Wnon-virtual-dtor -Wextra -Wpedantic -g -fPIC  -MD -MQ 'src/libperfectcalc/src@libperfectcalc@@perfectcalc@sha/plus.cpp.o' -MF 'src/libperfectcalc/src@libperfectcalc@@perfectcalc@sha/plus.cpp.o.d' -o 'src/libperfectcalc/src@libperfectcalc@@perfectcalc@sha/plus.cpp.o' -c ../src/libperfectcalc/plus.cpp
[2/5] ccache c++ -Isrc/src@@calculator@exe -Isrc -I../src -I../include -fdiagnostics-color=always -pipe -D_FILE_OFFSET_BITS=64 -Wall -Winvalid-pch -Wnon-virtual-dtor -Wextra -Wpedantic -g  -MD -MQ 'src/src@@calculator@exe/main.cpp.o' -MF 'src/src@@calculator@exe/main.cpp.o.d' -o 'src/src@@calculator@exe/main.cpp.o' -c ../src/main.cpp
[3/5] ccache c++  -o src/libperfectcalc/libperfectcalc.so 'src/libperfectcalc/src@libperfectcalc@@perfectcalc@sha/plus.cpp.o' -Wl,--no-undefined -Wl,--as-needed -shared -fPIC -Wl,--start-group -Wl,-soname,libperfectcalc.so -Wl,--end-group  
[4/5] /usr/lib64/python-exec/python3.6/meson --internal symbolextractor src/libperfectcalc/libperfectcalc.so 'src/libperfectcalc/src@libperfectcalc@@perfectcalc@sha/libperfectcalc.so.symbols' 
[5/5] ccache c++  -o src/calculator 'src/src@@calculator@exe/main.cpp.o' -Wl,--no-undefined -Wl,--as-needed -Wl,--start-group src/libperfectcalc/libperfectcalc.so -Wl,--end-group '-Wl,-rpath,$ORIGIN/libperfectcalc' -Wl,-rpath-link,/home/eclair/workspace/cpp/meson-handson/build/src/libperfectcalc  
```

さっき``default_options``に``warning_level=3``を指定したので、コンパイラーコマンドに警告を出力するオプション (-Wall等) が付いていることがわかります (コマンド行が長くてわかりにくいですが……)[^5]。また、試した環境にはccacheがインストールされているので、ccacheが (勝手に) 使われていたこともわかります。

[^5]: Windowsではコマンドの引数に渡せる長さがあまり長くないので、コマンド引数の大部分は別のファイルに保存されて代わりに``@file``引数が渡されてコンパイラーが呼ばれます。なので、どういうコマンド引数が渡されているのかが出力だけではわからなくなります。

重要なのは、``build/src/libperfectcalc``に``libperfectcalc.so``が出来ているのと、``build/src``に``calculator``が出来ていることです[^13]。実行してみましょう。

[^13]: デフォルトの``buildtype``は``shared``なので``libperfectcalc.a``ではありません。

```console
$ src/calculator
1 3
4
```

``1 3``を入力して最後にEnterすると4が出力されるはずです。完璧[^9]に足し算が行える計算機が完成しましたね。

[^9]: 足される数、結果ともにint型の範囲で収まる限り

ちゃんと``libperfectcalc.so``を使うようにもなっています。

```console
$ ldd src/calculator
...
	libperfectcalc.so => /home/eclair/workspace/cpp/meson-handson/build/src/libperfectcalc/libperfectcalc.so (0x00007f6c3f04b000)
...
```

# (上級編) インストールされていたりされていなかったりするライブラリをつかう

最後に、Luaを使ってちょこっと遊んでみましょうか。[ソースコードはlua_depブランチに置いてあります](https://github.com/turenar/meson-handson/tree/lua_dep)

```c++:src/main.cpp
#include <lua.hpp>

int main() {
    lua_State* state;
#if LUA_VERSION_NUM >= 501
    state = luaL_newstate();
#else
    state = lua_open();
#endif
    luaL_openlibs(state);
    // まあつまり、Hello world!をLua経由で出力させるだけのプログラムです
    luaL_dostring(state, "print \"Hello world!\"");
    lua_close(state);
}
```

説明の都合上``default_options``に``warning_level=3``を渡しません。

```meson:meson.build
project('luatoasobu', 'cpp')

# luaのようにpkg-configなどが使えるライブラリは、dependency()で
# インクルードディレクトリやリンクするライブラリなどを適切に設定できるようになります。
lua_dep = dependency('lua')

# pkg-configが使えない場合は、find_library()やinclude_directories()などを使う必要があるかもしれません

subdir('src')
```

```meson:src/meson.build
executable(
    'main', ['main.cpp'],
    dependencies: [lua_dep])
```

というわけでビルドして実行してみましょう！build/以下は削除してもしなくてもどっちでも良いですが、``default_options``を書き換えたので削除しておくことにします。

```console
$ rm -rf build/
$ meson build/
The Meson build system
Version: 0.45.1
Source dir: /home/illyasviel/tmp/meson-handson
Build dir: /home/illyasviel/tmp/meson-handson/build
Build type: native build
Project name: luatoasobu
Native C++ compiler: ccache c++ (gcc 7.3.0 "c++ (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0")
Build machine cpu family: x86_64
Build machine cpu: x86_64
Found pkg-config: /usr/bin/pkg-config (0.29.1)

meson.build:7:0: ERROR: Native dependency 'lua' not found

A full log can be found at /home/illyasviel/tmp/meson-handson/build/meson-logs/meson-log.txt
```

(さっきと違うPCなのでいろいろ違うのは許してください)

luaは見つかんないよ！と怒られてしまいました。それもそのはず。このPCはluaがインストールされていないのです！(luaがインストールされている場合は普通にビルドされます)

普通にluaをインストールしても良いのですが、ここではMesonのWrap dependency systemというものを使ってみましょう。

そうと決まれば、まずアクセスするべきは[WrapDB](https://wrapdb.mesonbuild.com/)です。長い一覧の中にluaがあることがわかりますね。

luaの行をクリックすると[別のページに飛びます](https://wrapdb.mesonbuild.com/lua)。どれでもいいのでDownloadリンクをクリックすると、なんかよくわからないファイルが表示されるかダウンロードされると思います。

```lua-5.3.0-r6.wrap
[wrap-file]
directory = lua-5.3.0

source_url = http://www.lua.org/ftp/lua-5.3.0.tar.gz
source_filename = lua-5.3.0.tar.gz
source_hash = ae4a5eb2d660515eb191bfe3e061f2b8ffe94dce73d32cfd0de090ddcc0ddb01


patch_url = https://wrapdb.mesonbuild.com/v1/projects/lua/5.3.0/6/get_zip
patch_filename = lua-5.3.0-6-wrap.zip
patch_hash = 2b287e1f31466a7a3cc4ca319c436b812ce8c55b5bd6f7ff591f43052e914862
```

これをプロジェクトの``subprojects/lua.wrap``に保存しましょう。

あるいは、コマンドで叩くのがお好きな方は、

```console
$ meson wrap search lua  # このwrapはサブコマンドです。build/はサブコマンドではないので、ややこしいですね。
lua  ← luaはさっきのWrapDBに登録されているよということです
$ mkdir subprojects  # これはどうやら必須のようです
$ meson wrap install lua
Installed lua branch 5.3.0 revision 6
```

でも同じことが出来ます。

このwrapファイルは何者？という疑問は置いておいて、さっさとビルドが通るようにしちゃいましょう。[例によってソースコードはlua_wrapブランチに置きました](https://github.com/turenar/meson-handson/tree/lua_wrap)。

```meson.build
# 変更箇所だけ示します
# ↓fallback: を渡すようにした
lua_dep = dependency('lua', fallback: ['lua', 'lua_dep'])
```

ただ残念ながら、wrapファイルを置くことでLuaのインストールは不要になりますが、環境によってはreadlineの開発者用ファイルをインストールする必要があります[^10]。

[^10]: Ubuntuならlibreadline-devです

```console
$ meson build/
The Meson build system
Version: 0.45.1
Source dir: /home/illyasviel/tmp/meson-handson
Build dir: /home/illyasviel/tmp/meson-handson/build
Build type: native build
Project name: luatoasobu
Native C++ compiler: ccache c++ (gcc 7.3.0 "c++ (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0")
Build machine cpu family: x86_64
Build machine cpu: x86_64
Found pkg-config: /usr/bin/pkg-config (0.29.1)
Downloading lua from http://www.lua.org/ftp/lua-5.3.0.tar.gz
Download size: 278045
Downloading: ..........
Downloading patch from https://wrapdb.mesonbuild.com/v1/projects/lua/5.3.0/6/get_zip
Download size: 1673
Downloading: ..........

Executing subproject lua.

Project name: lua
Native C compiler: ccache cc (gcc 7.3.0 "cc (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0")
Library dl found: YES
Library m found: YES
Library readline found: YES
Build targets in project: 2

Subproject lua finished.
Dependency lua from subproject subprojects/lua found: YES
Build targets in project: 3
Found ninja-1.8.2 at /usr/bin/ninja
```

実行してみましょう。

```console
$ ninja
$ src/main
Hello world!
```

うまくいきましたね。

このようにMesonのWrap dependency systemを使うと、システムにインストールされていない依存先を準備してビルドできるようにしてくれます。インストールされている場合はインストールされているものを使ってくれます。便利ですね。

もちろん、WrapDBに登録されていないライブラリでも、自分でwrapファイルを書けば同じようにやってくれます。[Wrap dependency system manual](https://mesonbuild.com/Wrap-dependency-system-manual.html)を参考にどうぞ。もし自分でwrapファイルを書く場合は、subprojectの中で死んだときに理由が出力されない困るバグが修正されているので、Meson 0.45.0以上を使いましょう。

# まとめ

Mesonは楽だし書きやすくていいぞ！

# Q&A

## どうして上級編では``warning_level=3``にしなかったのですか

試してもらうとわかるのですが、luaのビルドでコンパイラが警告を吐くのが鬱陶しいからです。

lua側のビルド中はwarning_level=1にする方法とか、逆にroot projectのみwarning_level=3にする方法があれば良いんですけど、ぱっと見なさそうなんですよね……ここが残念。

ちなみに、-isystemオプションをサポートしているコンパイラーなら、ライブラリのヘッダファイル内の警告であれば抑制することが出来ます。

```meson
include_directories('path/to/include', is_system: true)
```

~~ただ、WrapDBに登録されているwrapは、include_directoriesにis_systemが渡されていないことが多いので(お察しください) 
そういう意味で、自分でwrapファイルを書いたほうがなにかと都合が良いかも。~~

v0.52.0より、``dependency()``に``include_type: 'system'``を渡せるようになりました。あくまでヘッダinclude時警告の制御なので、wrapファイル内でコンパイルを行う場合はあまり意味がないですが……