bundlerを使っていて、'/' is not writable. Bundler will use '/tmp/bundrer/home/unknown/' as your home directory temporarily.` と出たときの対応
daemontools経由等で、デーモンのプロセスを立ち上げるときに、プロセス内でbundle exec hoge
などのコマンドを実行すると、
'/' is not writable. Bundler will use '/tmp/bundrer/home/unknown/' as your home directory temporarily.
のようなwarningメッセージがでます。
これは、HOME
が設定されていない環境におけるfallbackのための修正によって発生します。
1.14.5
以降のbundlerを使うと発生します。
https://github.com/bundler/bundler/blob/master/CHANGELOG.md#1145-2017-02-22
原因
下記のようなコードで再現できます(OSはMacです)。
% cat test.rb p ENV['HOME'] p Gem.user_home % HOME=/ bundle exec ruby test.rb `/` is not writable. Bundler will use `/var/folders/kw/_3kqzwx93nvbbl0h2wk6nkz8bj8f_z/T/bundler/home/hoge' as your home directory temporarily. "/" "/"
エラーがでるところはここです。
bundler/bundler.rb at 3203fdd2ad861af2aedfa233b754a02bfc1c4741 · bundler/bundler · GitHub
まずbundle
コマンドでbundler自体の設定を行うのですが、bundlerのconfigurationオプションの設定が書かれているファイルから設定を読み込みます。
bundler/settings.rb at 3203fdd2ad861af2aedfa233b754a02bfc1c4741 · bundler/bundler · GitHub
bundler/settings.rb at 3203fdd2ad861af2aedfa233b754a02bfc1c4741 · bundler/bundler · GitHub
このconfigurationオプションの設定とは、
ここにある設定群になります。
この内容はbundle config
で設定した内容が反映されています。
実際の内容はこんな感じです。
% cat .bundle/config --- BUNDLE_GEM__TEST: "rspec" BUNDLE_GEM__MIT: "false" BUNDLE_GEM__COC: "false" BUNDLE_BUILD__THERUBYRACER: "--with-v8-dir=/usr/local/opt/v8@3.15" BUNDLE_BUILD__LIBV8: "--with-system-v8"
bundlerのconfigurationオプションの設定があるファイルはglobalとlocalの2つがあります。
localは実行しているカレントディレクトリ/.bundle/config
にファイルが設置されています。
globalの場合が今回の問題で、設置されているパスは、環境変数BUNDLE_CONFIG
があれば、 $BUNDLE_CONFIG/.bundle/config
になり、なれば$HOME/.bundle/config
に設置されています。
ただし、HOME
もBUNDLE_CONFIG
が設定されていない場合や、HOME
に書込み権限がなければ実際に書込みが発生する際にエラーになっていました。(bundle install
やbundle update
、bundle config
を実行した時)
.bundle
配下はbundlerの設定やgemのchecksumなどのキャッシュファイルが格納するために必須です。
そこで、HOME
もBUNDLE_CONFIG
も設定されていない場合や、.bundle配下が書き込めない場合においてtempfileにディレクトリを作成してそこに格納するようにします。
tempfileに格納する際に、warningメッセージがでます。
このメッセージが出た場合
.bundle/config
で設定される内容はほとんどがビルドオプションになります。
bundle install
やbundle update
を行う場合にはbundlerのンパイルオプション等に影響があるので、要確認です。
ただし、bundle exec
経由でコマンドを実行する場合においては影響がないことがほとんどです。
(特にデーモン化したプロセスでbundler経由でコマンドを呼び出すと、HOMEが/
になっているのでこのwarningがでると思います。)
自分で設定した内容を確認して、問題なければwarningをスルーでも大丈夫です。
デーモンでもどうしてもこのwarningを止めたい
bundler binstub
を使ってプロジェクトにbundleされた実行可能ファイルを生成してその実行ファイル経由でコマンドを実行してください。
MAC(メッセージ認証コード)とは
MAC(Message Authentication Code メッセージ認証コード)について、整理してみました。
(MACアドレスのMACはMedia Access Controlの頭文字なので同じ文字面ですが全然意味が違いますね。)
MACは自分に届いたメッセージが送信者が意図した通り(改竄されてない、なりすましていない)であることを確認できます。
メッセージが改竄されていないことは、メッセージのintegrity(正真性、完全性)という性質です。
メッセージがなりすまされて送信されていない(正しい送信者からのものである)ということを、メッセージの認証という性質です。
MACは正真性を確認し、メッセージの認証を行います。
メッセージ認証コードとは
メッセージ認証コードには、
- 送信者と受信者が共有する鍵(共有鍵、MAC鍵、ハッシュ鍵ともよばれることもある)
- 任意のメッセージ
を元に固定ビット長の出力をします。
出力された値は MAC値 と呼ばれます。
一方向ハッシュ関数でメッセージのハッシュ値を取るのと似ていますが、この場合共有鍵はでてきません。
一方向ハッシュ関数を拡張して認証をできるようにしたのがMAC(メッセージ認証コード)になります。
MAC(メッセージ認証コード)を利用するための手順
MACを使った改竄検知、認証のフローの手順です。
- 受信者、送信者の二者で共有鍵を共有する(鍵配送問題が存在します)
- 送信者はメッセージと共有鍵の2つからMAC値を計算
- 送信者はメッセージとMAC値の両方を受信者に送る
- 受信者はメッセージを元にMAC値を計算する
- 受信者は送信されたMAC値を自分で計算したMAC値を比較する。
最後に計算したMAC値を比較し、MAC値が等しい場合は、メッセージが間違いなく送信者から送られてきたものします。
MAC(メッセージ認証コード)はハッシュ関数の拡張か生まれているので、一方向ハッシュ関数を用いて実現できます。
また、ブロック暗号でも実現可能です。
HMAC
MAC(メッセージ認証コード)を、一方向ハッシュ関数(SHA256等)を用いて実現する方法です。
- 共通鍵を一方向ハッシュ関数のブロック長より短い場合は、0パディングをする。
- 固定文字列ipad(バイト値 0x36 を 64回(ブロック長)繰り返した文字列)とパディングした鍵とのビット列のXORをとる
- 2で生成したバイト列をメッセージの先頭につける
- 3の結果を一方向ハッシュ関数を適用して、ハッシュ値を得る
- 固定文字列opad(バイト値 0x5C を 64 回(ブロック長)繰り返した文字列)と4で得られたビット列とのXORをとる
- 5で生成したビット列に4で生成されたハッシュをつける
- 6の結果を一方向ハッシュ関数に適用してハッシュ値を得る -> これがMAC値になる
再送攻撃(再生攻撃)への対応
すでに送られて、正しく認証されたMAC値とメッセージを取得しておき、それをなにも加工せずに使いまわして同じメッセージを送り続けることです。
防御策としては、
メッセージにシーケンス番号を含めておく
- 通信相手ごとに最後のシーケンス番号を記録しておかなければならない
メッセージにタイムスタンプを含めておきMAC値が一致しても古いメッセージが送られて来た場合にエラーにする(脆弱性残る)
- タイムスタンプの粒度(一秒とかだと一秒の間で攻撃される)の問題がある
- 送信者と受信者で時計を一致させるか、古いと判定するための時間に幅をもたせておくことをするが、ここも攻撃される可能性がある
使い捨てのランダムな値(ノンス)をメッセージに含めて送信する。
- 通信量が少し多くなるが、有効な手法
鍵の推測への対応
共有鍵は暗号学的に安全な疑似乱数生成器を使ってください。
できないこと
デジタル署名によって解決できます。
- 否認防止 送信者と受信者の区別が第三者には判断できない。
これもデジタル署名によって解決できます。
認証付き暗号(Authenticated Encryption with Associated Data)
Authenticated Encryption with Associated Data
を略してAEAD
とも呼ばれます。
MAC(メッセージ認証コード)と対称暗号を組み合わせて、機密性(暗号)、認証・正真性(MAC(メッセージ認証コード))を満たす仕組みです。
AEAD(認証付き暗号)では不適切に細工された暗号文を識別して(MACの認証と正真性)復号を拒否することができるため、選択暗号文攻撃に対して安全になります。
AEADは現在のところTLSで利用できる最良の暗号化利用モードです。
SSL/TLSの暗号化利用モードにおいて、AEAD(認証付き暗号)の一種GCM
が利用可能です。
Galois/Counter Mode - Wikipedia
ただし下記記事にあるように、素のままのOpenSSLには問題ないのですが、自前で実装されている場合は注意が必要です。
国際標準暗号GCMの新たな安全性評価結果(2012年08月17日):プレスリリース | NEC
参照
HMAC: Keyed-Hashing for Message Authentication
http://www.gtc.co.jp/glossary/cia.html
この記事は、クリエイティブ・コモンズ・表示・継承ライセンス3.0のもとで公表されたウィキペディアの項目「メッセージ認証符号」を素材として二次利用しています。
stackprofの原理
stackprofがどうやってプロファイルを行っているかを追ってみます。
プロファイルするコードは下記を使います。
stackprof/sample.rb at master · tmm1/stackprof · GitHub
自分用のメモなので、間違い等があるのはご容赦ください。。(随時ブラッシュアップしていければ)
プロファイルを取る仕組み
StackProd::Middleware経由でのプロファイリングの流れは下記の通り。
- リクエストを受ける
StackProf.start
が呼ばれプロファイリング開始- リクエストを処理する
StackProf.stop
を呼びプロファイルングを終了save_every
で指定した回数にリクエスト回数が達した場合、プロファイル結果をファイルに書込む
さらに今回はStackProf.start
からStackProf.stop
の流れを詳細におっていきます。
準備
gdbでおっていくので、マクロ等も展開できるようにデバッグ情報を付けてrubyをビルドします。
$ wget https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.1.tar.gz $ tar zxf ruby-2.4.1.tar.gz $ cd ruby-2.4.1 # -O0で最適化を無効にし、-g3でデバッグ情報を付ける ./configure optflags="-O0" debugflags="-g3" --prefix="${HOME}/.rbenv/versions/2.4.1-O0_g3" make make install
サンプリング
StackProf.start
が呼ばれると、stackprof_start
が呼ばれます。
https://github.com/tmm1/stackprof/blob/master/ext/stackprof/stackprof.c#L61
start時に、mode
をcpu
または wall
で設定すると、setitimer(2)
でinterval
で指定した時間(マイクロ秒)のインターバルタイマーを設定し、タイマーが満了したときに送られるシグナルに対してsigaction(2)
でサンプリングのシグナルハンドラーを設定します。
stackprofで指定するinterval
はサンプリングする間隔(これは時間)で、save_every
はサンプリングした結果をファイル等に保存する間隔(これはリクエスト回数)です。
save_every
はHTTPサーバー向けのオプション(StackProf:: Middlewareでのみ指定可能)で、save_every
で指定した数リクエストがきたら、サンプリング結果を保存します。
save_every
が大きすぎると、サンプリング結果が肥大化し、保存するときに時間がかかってしまいますが、小さいとこまめに保存されるためファイル数が膨大になってしまうので運用しながら要調整です。
mode
が:object
の場合は、TracePointを使って、オブジェクトの確保のたびにサンプリングを行います。
mode | 説明 |
---|---|
:wall | setitimer(2) でITIMER_REAL (実時間(時計時間 wall-clock-time)でタイマーが減少)を使います。タイマーが満了になるとSIGALRM シグナルが送られます。intervalはマイクロ秒になります。 |
:cpu | setitimer(2) でITIMER_PROF (CPU時間でタイマーが減少)を使います。タイマーが満了になるとSIGPROF シグナルが送られます。intervalはマイクロ秒になります。 |
:object | Rubyの新しいオブジェクト生成の毎にサンプリングします。RubyのVM時間とでも言えるかな。 |
シグナルハンドラーはRubyのC-APIのrb_postponed_job_register_one
を使ってサンプリングするjobを登録します。
サンプリングするときには、RubyのC-APIのrb_profile_frames
を使って、call stackにアクセスして、スタックフレームを取得します。
ruby/vm_backtrace.c at 249790802db62ff22c79830d4054c449fa3c243b · ruby/ruby · GitHub
サンプリング結果
各サンプルは複数個のスタックフレームで構成され、スタックフレームはMyClass#method
やblock in MySingleton.method
ように見えます。
サンプル内のこれらのフレームごとに、stackprof
は下記のメタデータを収集します。
(aggregate
オプションを有効にした場合のみです。デフォルトでは有効になっています。)
主に個々の部分でメタデータの収集を行っています。
https://github.com/tmm1/stackprof/blob/master/ext/stackprof/stackprof.c#L387
metadata | 説明 |
---|---|
samples | スタックフレームで一番最初に呼ばれている回数。つまり、サンプリングしたときにまさに呼び出されていた回数。まずこの数字を確認する。 |
total_samples | スタックフレームの中にふくまれてる回数。 |
lines | スタックフレームの呼び出されている行とその回数。 |
edges | このスタックフレームを呼び出したメソッドとその回数 |
結果
- print_method(/pow|newobj|math/)
stackprof print_method(/pow|newobj|math/)
- print_text
print_debug
サンプリングしていたときにスタックフレームは下記のようになっています。
- A#powの場合
pow
<- initialize
なので、 pow
のsamples
とtotal_samples
のカウントに1足され、initialize
のtotal_samplesに1足されます。
$ gdb -nw -silen -x ./.gdbinit --args `rbenv which ruby` sample.rb (gdb) break stackprof.c:387 (gdb) run (gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[0]) "pow" (gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[1]) "initialize" (gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[2])
- A#mathの場合
math
<-math
<-initialize
なので、match
のsamples
には1、total_samples
のカウントに2足され、initialize
のtotal_samplesに1足されます。
そのため、math
のtotal_samples
はsamples
より約2倍ほど多くなります。
math
は中にブロックがあり、それがスタックに積まれています。
(gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[0]) "math" (gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[1]) "math" (gdb) rb_p rb_profile_frame_method_name(_stackprof.frames_buffer[2]) "initialize"
:wall
と:cpu
の使い分け
ネットワークアクセスをする場合をサンプリングしてみます。
- :wall
- :cpu
ネットワークI/Oの待ち時間やディスクI/Oの待ち時間はCPU時間には反映されないので、:cpu
の場合は、ネットワークレイテンシが反映されないです。
これを踏まえると、まずは:wall
モードでサンプリングしボトルネックがどこにあるか明らかにしましょう。
I/O待ち多く(特にデータベース外部サービスの外部要因でどうにもならない場合)、ノイズにしかならないので、:cpu
モードでもサンプリングするのがよいでしょう。
参照
macro RTEST (Ruby 2.7.0 リファレンスマニュアル)
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/sigaction.2.html
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/setitimer.2.html
https://linuxjm.osdn.jp/html/LDP_man-pages/man3/sigdelset.3.html
real time/user CPU time/system CPU timeの違いをメモsiguniang.wordpress.com
rb_postponed_job_register_one
の説明
d.hatena.ne.jp
ruby trunk changes
rb_profile_frames
の説明
2013-10-07
Ruby 2.1: Profiling Ruby · computer talk by @tmm1
st_update
の説明
2011-12-28
BazelでC++をビルドする
以前Bazelの入門の記事をかきましたが、今回はC++をビルドする環境を作成してみます。 spring-mt.hatenablog.com
下記のチュートリアル通りにやっていきます。 Build Tutorial - C++ - Bazel
まずはexampleをcloneしてやっていくのですが、前回やった残りのプロジェクトもあるのでサクッと自分でやっていきます。
【1】単一パッケージ、単一ターゲットでビルド
workspaceのセットアップを行います。
workspaceにはソースファイルとビルド成果物が含まれます。
Bazelのowrkspaceとして、トップディレクトリにWORKSPACE
ファイルを置きます。
今回は依存関係がないので、空ファイルで置いておきます。
プロジェクトをビルドするために、全てのインプットと依存関係を同一workspaceに含める必要があります。
異なるワークスペースに存在するファイルは、リンクしていなければ互いに独立しています。
BUILD
ファイルはソースコードなどビルドに必要なファイルやビルド方法、出力されるファイルなどの設定を記述するファイルです。
BUILD
ファイルを含むワークスペース内のディレクトリはパッケージ
という単位になります。
BUILD
ファイルにはBazelのさまざまな種類の設定を記述しますが、一番重要な設定はビルドルールです。
ビルドルールは、実行可能なバイナリやライブラリなど、どのような成果物を作成するかを記述します。
BUILD
ファイル内のビルドルールのそれぞれの実体はターゲット
と呼ばれ、ソース・ファイルと依存関係の特定のセットを指します。ターゲット
は他のターゲット郡を指すこともできます。
最初に下記構成にしてみました。
. ├── WORKSPACE └── main ├── BUILD └── hello-world.cc
まずは単純なBUILD
ファイルを見ていきます。
cc_binary( name = "hello-world", srcs = ["hello-world.cc"], )
name
で指定したパラメータがターゲット
名になります。(name
の指定は必須です。)
hello-world
ターゲットはcc_binaryルールをインスタンス化します。
このルールでは依存関係のないhello-world.ccソースファイルから自己完結型の実行可能バイナリをビルドします。
ではビルドしてみます。
ビルドコマンドは
bazel build //main:hello-world
//main:
はworkspaseのルートディレクトリからのBUILD
ファイルが配置されてる相対パスを示しています。hello-world
はBUILD
ファイル内で指定したname
です。
% bazel build //main:hello-world ..................... INFO: Found 1 target... Target //main:hello-world up-to-date: bazel-bin/main/hello-world INFO: Elapsed time: 12.816s, Critical Path: 3.27s % bazel build //main:hello-world INFO: Found 1 target... Target //main:hello-world up-to-date: bazel-bin/main/hello-world INFO: Elapsed time: 0.165s, Critical Path: 0.00s
(二回目はキャッシュが使われて高速にビルドできています)
ビルドが完了すると、workspaceのルート直下のbazel-bin
に成果物ができています。
% bazel-bin/main/hello-world Hello world Fri Aug 4 01:11:51 2017
依存性の確認
BUILD
ファイルにはビルドの依存関係がすべて明示的に記述されています。
Bazelはこれらのステートメントを使用してプロジェクトの依存関係グラフを作成し、正確なインクリメンタルビルドを可能にします。
前述のプロジェクトの依存性のグラフは下記コマンドで生成できます。
% bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' --output graph digraph mygraph { node [shape=box]; "//main:hello-world" "//main:hello-world" -> "//main:hello-world.cc" "//main:hello-world.cc" }
グラフ構造はGraphViz で可視化できます。
【2】単一のパッケージ、複数のターゲット
大きなプロジェクトを複数のターゲットとパッケージに分割することで、変更した部分の再ビルドだけの高速なインクリメンタルビルドができ、分割されたプロジェクトを一度にビルドするとことでビルド時間の短縮ができます。
【1】プロジェクトを2つのターゲットに分けます。
diff --git a/CppExamples/main/BUILD b/CppExamples/main/BUILD index 20c6f47..43b22a4 100644 --- a/CppExamples/main/BUILD +++ b/CppExamples/main/BUILD @@ -1,4 +1,13 @@ +cc_library( + name = "hello-greet", + srcs = ["hello-greet.cc"], + hdrs = ["hello-greet.h"], +) + cc_binary( name = "hello-world", srcs = ["hello-world.cc"], + deps = [ + ":hello-greet", + ], )
hello-world
のリンクしたいライブラリのラベルをdeps
に指定します。
こうすると、hello-world
をビルドする前に、hello-greet
のビルドが実行されます。
% bazel build //main:hello-world INFO: Found 1 target... Target //main:hello-world up-to-date: bazel-bin/main/hello-world INFO: Elapsed time: 5.813s, Critical Path: 1.16s % bazel-bin/main/hello-world Hello world Sun Aug 6 15:23:47 2017
hello-greet.cc
が修正されたら、hello-greet.cc
のみコンパイルが行われます。
【3】複数のパッケージ、複数のターゲット
BUILD
ファイルを含む新しいディレクトリを作成します。
(BUILD
ファイルを含むワークスペース内のディレクトリはパッケージ
という単位)
cc_library( name = "hello-time", srcs = ["hello-time.cc"], hdrs = ["hello-time.h"], visibility = ["//main:__pkg__"], )
srcs
でソースファイルを、hdrs
でヘッダーファイルを指定します。
lib/BUILD
のなかで指定した// lib:hello-time
ターゲットをmain/ BUILD
から明示的に見えるようにするためにvisibility
で//main:__pkg__
を指定しています。
デフォルトでは、ターゲットは同じBUILD
ファイル内の他のターゲットに対してのみだけに公開されています。
Bazelは、ターゲットの可視性を利用して、ライブラリの実装の詳細がパブリックAPIに漏れることを防いでいます。
ビルドをしてみます。
% bazel build //main:hello-world INFO: Found 1 target... Target //main:hello-world up-to-date: bazel-bin/main/hello-world INFO: Elapsed time: 0.365s, Critical Path: 0.00s % bazel-bin/main/hello-world Hello world Sun Aug 6 22:01:01 2017
ラベルの使い方
BUILD
ファイルやコマンドライン上では、ラベルからターゲットを参照します。
シンタックスは下記の通り。
//path/to/package:target-name
ターゲットがruleのターゲットの場合、path/to/package
はBUILDファイルを含むディレクトリへのパスで、target-nameはBUILDファイルのターゲット(name
)の名前です。
ターゲットがファイルである場合、path/to/package
はパッケージのroot
からのパスであり、target-nameはターゲットファイル名です。
同じパッケージ内のターゲットを参照するときは、パッケージパスを省略して//:target-name
とだけで使えます。
同じBUILD
ファイル内のターゲットを参照する場合は、//
のworkspaceのルート識別子をスキップして、:target-name
だけを使用することができます。
Q4MのConditional Subscriptionのベンチマーク
Q4MのConditional Subscriptionを使う前にベンチマークを取ってみました。
マシン
Model Name: MacBook Pro Model Identifier: MacBookPro13,2 Processor Name: Intel Core i5 Processor Speed: 2.9 GHz Number of Processors: 1 Total Number of Cores: 2 L2 Cache (per Core): 256 KB L3 Cache: 4 MB Memory: 16 GB
テーブルスキーマ
CREATE TABLE `q4m_test` ( `id` varchar(255) COLLATE utf8mb4_bin NOT NULL, `payload` text COLLATE utf8mb4_bin NOT NULL, `enqueued_at` int(10) unsigned NOT NULL ) ENGINE=QUEUE;
enqueued_at
をConditional Subscriptionで使います。
enqueued_at
にはキューを入れた時間を入れています。
Conditional Subscriptionにはintegral values(整数値)なので、unixtimestamp形式で保存しています。
ベンチの条件
予め6000件のデータを作成しました。 payloadには1000バイトの文字列を入れています。
【1】6000件のデータのうち、最後の1000件のキューを取り出して処理するパフォーマンス
こんなスクリプトで計測しました。
require 'mysql2' require 'benchmark' client = Mysql2::Client.new(host: "localhost", username: "root", database: 'test') 1000.times do result = Benchmark.realtime do result = client.query("SELECT queue_wait('q4m_test:enqueued_at>=1501686146', 5)") client.query('select queue_end()') end puts "#{result}" end
結果
縦軸が秒になります。 横軸が時間軸になります。
特に劣化する様子はありません。
【2】5000件のデータを処理するパフォーマンス
上で5000件になったデータを全て順に処理していくようにして処理速度を計測してみました。
require 'mysql2' require 'benchmark' client = Mysql2::Client.new(host: "localhost", username: "root", database: 'test') 1000.times do result = Benchmark.realtime do result = client.query("SELECT queue_wait('q4m_test:enqueued_at<1501686146', 5)") client.query('select queue_end()') end puts "#{result}" end
結果
縦軸が秒になります。 横軸が時間軸になります。
1とくらべてもそこまで大きな変化はありませんでした。
キューを入れる処理
キューを入れるときも処理速度を計測してみました。
require 'mysql2' require 'benchmark' require 'securerandom' client = Mysql2::Client.new(host: "localhost", username: "root", database: 'test') payload = 'a' * 1000 stmt = client.prepare('INSERT INTO q4m_test VALUES (?, ?, ?)') 4000.times do result = Benchmark.realtime do stmt.execute(SecureRandom.uuid, payload, Time.now.to_i + 1000) end puts "#{result}" end
結果
○でくくったパフォーマンスが劣化した部分はおそらく、compactionが走った影響かと思われます。
参考にした記事
Bazel入門
Bazelとは
Googleが内部で利用していたビルドツールのオープンソース版として提供されているものです。
Bazel - a fast, scalable, multi-language and extensible build system" - Bazel
ビルドやテストの高速化を目指して作られています。
C++やJavaだけでなく、iOSやAndroidアプリのビルドにも対応しています。
今後のロードマップにはAndroid Studioとの統合も入っています。
Bazelを導入する理由
- Gradleよりも構造的な設計になっていて、各アクションが何をするかが正確に理解しやすい構成になっている(らしい)。
- すでに、ローカルでのキャッシュ、並列実行、依存性解析の最適化などが行われており高速に動作する。 さらに、分散キャッシュの仕組みも今絶賛入ろうとしていることろのなので、ビルドの高速化という意味では期待ができそう。
Bazelの公式ページも参照ください。
Bazelのinstall
Installing Bazel on macOS - Bazel
homebrewでinstallすると最後に補完スクリプトのパスが表示されるのでそれを利用してzshの補完の設定をします。
Bash completion has been installed to: /usr/local/etc/bash_completion.d zsh completions have been installed to: /usr/local/share/zsh/site-functions
fpath[1,0]=~/.zsh/completion/ mkdir -p ~/.zsh/completion/ cp scripts/zsh_completion/_bazel ~/.zsh/completion rm -f ~/.zcompdump; compinit # This way the completion script does not have to parse Bazel's options # repeatedly. The directory in cache-path must be created manually. zstyle ':completion:*' use-cache on zstyle ':completion:*' cache-path ~/.zsh/cache
Bazelでビルド
まずは簡単なチュートリアルを試してみます。
WORKSPACEファイル
Bazelは、ビルドしたいソースコードとビルド成果物を含むworkspaceというディレクトリでビルドを行います。
必要に応じて、1つのworkspaceを複数のプロジェクトで共有することができます。
workspaceはどこでもよいのですが、トップディレクトリにWORKSPACE
ファイルを必ず置く必要があります。
このWORKSPACE
ファイルに外部の依存性の定義を行います。
依存がなければ空ファイルでも大丈夫です。
touch WORKSPACE
BUILDファイル
Bazelは、プロジェクト内でのビルドターゲットを知るために、BUILD
ファイルを使います。
BUILD
ファイルはPythonに似たBazelのビルド用言語で記述します。
BUILD
ファイルにいろいろなルールを書いていきます。
各ルールは、入力、出力、および入力からの出力を計算する方法を指定します。
genrule
について
シェルコマンドを呼び出すgenrule
は、Makefileを使っている人たちにおなじみのルールです。
genrule
の詳細は下記ページにまとまっています。
今回は単純にecho
コマンドで標準出力に出した内容をリダイレクトしてファイルに書き込むルールを作ってみます。
genrule
では、 cmd
で指定したコマンドが実行されます。
genrule( name = "hello", outs = ["hello_world.txt"], cmd = "echo Hello World > $@", )
今回、cmd
の中で使われている$@
はBazelの中で使える変数で、outs
が一つの場合において、outs
の内容が展開されます。
利用可能な変数の一覧は Make variables にまとまっています。
ターゲットは、ルールの中で設定しているname
によって指定されるlabel
になります。
また、Bazelの成果物はソースツリーを汚染しないようにするために、ソースツリーとは別のbazel-genfiles
ディレクトリに格納されます。
1回目
% bazel build :hello .................... INFO: Found 1 target... Target //:hello up-to-date: bazel-genfiles/hello_world.txt INFO: Elapsed time: 7.322s, Critical Path: 0.01s
2回目
% bazel build :hello INFO: Found 1 target... Target //:hello up-to-date: bazel-genfiles/hello_world.txt INFO: Elapsed time: 0.155s, Critical Path: 0.00s % cat bazel-genfiles/hello_world.txt Hello World
BUILD
ファイルにouts
を複数して$@
を利用するとビルドエラーになります。
genrule( name = "hello", outs = ["hello_world.txt", "hello_world2.txt"], cmd = "echo Hello World > $@", )
% bazel build :hello ERROR: /Users/hoge/BazelTest/SampleEcho/BUILD:4:9: in cmd attribute of genrule rule //:hello: variable '$@' : more than one output file. ERROR: Analysis of target '//:hello' failed; build aborted. INFO: Elapsed time: 0.161s
ルールは、他のルールの出力を入力として利用できます。
生成したsrc
はlabelから参照できます。
$<
はsrc
のファイルの変数です。
genrule( name = "hello", outs = ["hello_world.txt"], cmd = "echo Hello World > $@", ) genrule( name = "double", srcs = [":hello"], outs = ["double_hello.txt"], cmd = "cat $< $< > $@", )
% bazel build :double INFO: Found 1 target... Target //:double up-to-date: bazel-genfiles/double_hello.txt INFO: Elapsed time: 0.396s, Critical Path: 0.04s % cat bazel-genfiles/double_hello.txt Hello World Hello World
genrule
ですが、通常利用するためのものではないです。
それぞれの言語に特化したルールがあるので、それを使ったほうがよいです。