NixのbuildをOpenTelemetryを使ってtraceする

概要

Nixのbuildは依存関係が複雑でどのビルドにどの程度の時間がかかっているかを把握するのは難しい。

そこで OpenTelemetry を使ってbuildやcacheの取得にかかった時間を可視化することを試みる。

NixはC++で書かれているので GitHub - open-telemetry/opentelemetry-cpp: The OpenTelemetry C++ Client を使ってNix自体にpatchを当てることでtraceの機能を実現することもできるが

  • Observabilityを上げる上でコード変更を伴わない方がversionの変更に対応しやすい
  • C++を書きたくない

といった理由から、今回はNixのsource codeを変更せずに可観測性を上げることを目標とする。ちなみに後者の理由が9割である。

実装方針(build)

Nixのoptionに preBuildHookpostBuildHook というものがある。これはbuildの前と後で任意のscriptを実行させることができる。

buildの開始と終了がわかればtrace情報が作成できるので、これらを使っていく。

ちなみに、検証した結果hookは以下のような仕様になっている

  • pre-build-hook$1derivationの名前$2derivationの名前.chroot が入った状態で呼ばれる。
  • post-build-hookDRV_PATH OUT_PATHS といった環境変数が入った状態で呼ばれる。
  • pre-build-hook は sandbox optionが有効になっていないと呼ばれない

また、shell scriptからtrace情報を送信するためのツールとして otel-cli というものがあり、今回はこれで送信する。

github.com

pre-build-hookとpost-build-hookの間で情報を受け渡す手段が用意されていないので、 pre-build-hookで $TMPDIR/$drv に開始時刻を書き込んで、 post-build-hookで otel-cliからtrace情報を送る方針にする。

自分の認識の限りではあるビルドの中で同じderivationがビルドされることはないのでうまくいくはず。

雑に試す場合は以下のようにする

pre-build-hook

drv=$1
name=${drv#/nix/store/}
$path/bin/date +%s.%N > /tmp/$name"

post-build-hook

drv=$DRV_PATH
name=${drv#/nix/store/}
${pkgs.otel-cli}/bin/otel-cli span \
--start $($path/bin/cat /tmp/$name) \
--end $($path/bin/date +%s.%N) \
--config $OTEL_CLI_CONFIG \
-n $name \
-s nix-build \
--fail

dateコマンドやotel-cliへのコマンドは当然絶対PATHで用意すること

helloコマンドをビルドした感じは以下の様な感じ

実装方針(cache)

上記のtraceはbuildの時間しか計測されず、cacheを取得している時間は含まれていない

また、cacheの取得はbuildではないので hookを使うことができない

しかしcacheをhttpで取得している場合はproxyを使うことでtrace情報を取得できる可能性がある

今回はEnvoy proxyを使ってcacheのtrace情報を取得する。

envoyのtraceに関しては公式のドキュメントがあるので、以下を読む。

www.envoyproxy.io

ドキュメントによると、traceの設定をした上で traceparent headerをつければ送ってくれるようだ

ただ、cacheの取得のリクエストに対して traceparent headerにtrace idを乗せた上で送る方法を考える必要がある。

nix側のコードを見た感じではそのような設定はないので、今回は traceparent headerを載せるenvoy proxyとtraceの送信を行うproxyの2段構成でやる。

つまり 、nix -> (traceparent headerをつけるproxy:8081) -> (cache.nixos.orgにrequestを通しながらtraceの送信を行うproxy:8080) といった感じだ。

trace id を envoyのconfigに対して直接書き込でおけば比較的簡単に1段目のproxyは実装することができるが、今回はビルドごとにidを発行したい。

なので、envoyのconfigを動的に変更する xDS configuration API を使う。

www.envoyproxy.io

xDSはistioとかを使っていると比較的に簡単に使うことができるのだが流石に大袈裟な仕組みになってしまうので、今回はファイルシステムの変更を検知して設定を書き換えてくれるDynamic from filesystemを使う

Configuration: Dynamic from filesystem — envoy 1.27.0-dev-f12afb documentation

ファイルシステムの変更検知にどのsyscallを使っているのか知らないが mvコマンド相当を使わないといけないらしい。

雑にやる場合、以下の様になる。

export ENVOY_LDS_FILE_TMP="$TMPDIR/tmp.json"
jq --arg traceparent "$TRACEPARENT" \
  '.resources[0].filter_chains[0].filters[0].typed_config.route_config.request_headers_to_add[0]={"header": {"key": "traceparent","value": $traceparent}}' \
  ${./lds.json} > "$ENVOY_LDS_FILE_TMP"
rm "$ENVOY_LDS_FILE"
mv "$ENVOY_LDS_FILE_TMP" "$ENVOY_LDS_FILE"
sleep 1
nix build "$1" \
--option substituters "http://localhost:8081?priority=20" \
--no-link \
-L

sleep 1を挟んでいるのはファイルの変更検知後にenvoyの設定変更を待つためである

実行した感じは以下の様になる

tracing-nix-build

今回試しやすい様にある程度再現しやすい形でCodeをpushしておいた

github.com

ただ、現段階では動かすのにテクがいるのでそれを書いておく。

まず、上記のスクリーンショットにも使っているがjaegerを動かす必要があるが、jaeger自体がかなり複雑なので提供されているdocker imageを使うのが良いだろう。

上記のrepositoryで言うと example ディレクトリの中で docker-compose up で立ち上げてもらう。

opentelemetry collectorが 4317で立ち上がるので、example/envoy.yamlexapmle/config.json の中にある 192.168.64.1を そのアドレス(127.0.0.1など)に書き換えてもらう。

これは私の環境ではdocker自体はmacos上で動かしておりUTMを使ってnixos上でenvoyと今回のtracing-nix-buildを実行しているので、上記の様な設定になっている。

また、/tmp/envoy/lds.yaml に空ファイルを用意しておかないとenvoyが立ち上がらないので用意しておく必要がある。この値は ENVOY_LDS_FILE で変更することができる。

まとめると

nix develop
cd example
export OTEL_CLI_CONFIG_FILE=$PWD/config.json
export ENVOY_LDS_FILE=/tmp/envoy/lds.yaml
docker-compose up -d
vi envoy.yaml 
vi config.json
touch $ENVOY_LDS_FILE
envoy -c envoy.yaml &

tracing-nix-build ".#hello"

の様になる

Nixの日本語コミュがあるので、動かない人は以下で聞いてください

github.com

今後の方針

上記の実装方法ではenvoyの設定を書き換えながらやるので複数のビルドを同時に扱うことができない。

なので、netrc-filelocalhost:8081に対する認証情報をtrace idを埋め込むようにして、1段目のproxyで認証情報からtraceparent headerを組み立てると理屈上はenvoyの設定を動的に変更せずにtrace idをビルドごとに送ることができる可能性がある

また、ZeroCodeChangesではできないことも多いので、詳細なメタデータを送るために C++ でtracingの実装を追加するのも面白いかもしれない。

nixosのtoplevelをビルドした例。途中でcacheのfetchに失敗している(httpの分割requestが失敗している雰囲気)

cacheの取得を進めた上で max-jobsとcoreを8指定でした例。並行で実行されている様子がわかる

第5回しろさとTT反省会

前回は以下

hiroqn.hatenablog.com

前回からのざっくり変更点

  • zwiftのTT tune up プランを終えた
  • Rotorクランクになった
    • フロントシングルになった
    • パワーメータ導入
    • クランク長が170→165
  • ボトムブラケット新しくなった

レーニン

TT tune upを終えた

whatsonzwift.com

このプランは8週間のインターバル系?的なトレーニングで、週を追うごとに微妙に負荷が上がっており折り返しの5週目あたりからはグッとしんどくなる

特に最後の方のPower Intervalsマジでキツかった

このプラン自体は必須のメニューだけで週5日のトレーニングが必要なので、時間の捻出と肉体の回復の両方の意味で自分にはまぁまぁ大変だった

FTPが合っていれば回復が追いくぐらいの設定におそらくなっておりなんとか継続できている。これが例えば自分で適当に組んだトレーニングの場合は同程度の間隔でトレーニングするのは無理だろう

また、上のプランをやっていない期間は大体、週4〜5程度のお好みトレーニングもしくはレースをした。

お好みトレーニングは気分に左右されるので適切な負荷をかけにくいし、レースはオールアウトしがちなので継続性に難があると感じた

FTP変化

今回の大会付近のFTPは以下

RAMP TEST→238w

zwiftpower 推定値→208w

どんどんRAMP TESTと20分TTの値が乖離していく

目標設定値

今回の目標は本来パワーメータで試走した結果のW数から出す予定だった。

が、しかしパワーメータをインプレできたのは大会がある週になってしまい、一切まともに試走できていない状態で大会を迎えることになってしまう。

こんなtweetをしている場合ではない

最終的に180Wぐらいで漕いだ場合、流石に34km/hは出るだろうしポジション維持できれば35km/hぐらい出るのでは?という雑思考で目標値を34~35に決定する

本番

前回は前泊だったが今回は当日の朝出発にして、受付1時間前には着く予定だった

しかし、事故渋滞でエントリー締切の時間に会場の門をくぐることになってしまう(一敗)

駐車場でチャリの準備をしていたが、焦りのあまり計測チップを結束バンドでフォークとスポークごと結んでしまいチャリを担いで走る人間が発生してしまう(二敗)

出走前に最終のカロリーを腹に入れる予定だったのだが、そんな余裕もなく出走し途中でお腹が減る(三敗)

タイム1:32:45で32.992km/hだった

と言うわけで全然目標未達でダメだった。前回より2分程度は早くなったが自分の中でベストを尽くせなかったので、かなり悔やまれる。

ちなみに、他にもトラブルはあった

走っている途中の8周目あたりで気づいたのだがライトがハンドルについていた(四敗)

帰ってから気づいたが、前日に微妙にいじったせいでクランクの取り付けに失敗しておりチェーンがついてない状態で一切回らない状態だった(五敗)

データ

パワー

パワー

ケイデンス

ケイデンス

心拍

データから見る反省

心拍数的には最初の2周が飛ばし過ぎていた感じがする。

お腹が減ったのもあるが6〜7周目あたりで足の疲労を招いてしまい、心肺は余裕だがペダルがもう回らない状態になっていた

あと、パワーを可視化してみて改めて思ったのが出力が安定していない

もうちょっと実走を増やしてパワーを安定させる感覚を掴まなければいけないなってなった。zwiftではそれができないので

余談:パワーメータの取り付けがなぜギリギリになってしまったか

実は1月あたりから余裕を持って準備を開始したのだがトラブル続きでこのような事態になった

パワーメータ取り付けに当たって、自分の股下の長さ的に165cmのクランクの方が良さそうと言う考えになりクランク長の変更が容易なRotorにする意思決定をした

また、自分の使い方的にもフロントシングルでいいと思ったのでナローワイドのチェーンリングとRotorの INSPIDER DMを海外通販で発注した

しかし、注文したチェーンリングは通販の画像では4armsだったのに到着したものは5armsと言うトラブルが発生

仕方なく4armsのチェーンリングを注文するも、どうやら途中で紛失になった様子

そして届かぬまま2週間前になり、今回はパワメつけて走る目標を何とか達成させるためにSIGEYIというRotor互換で5arms対応のパワメを購入する

ギリギリで届いたので装着し当日へ

Rotor INSPIDER DMが余ってしまったな...

ちなみにチェーンリングはクレームを入れたところ返金されたので使わなさそうならパワメは売るか

SQLiteでLoadable Extensionsを静的リンクする

about

この記事は Run-Time Loadable Extensions で書かれている Statically Linking A Run-Time Loadable Extension をやる。

SQLiteのversionは3.40以降

Loadable Extension とは

Loadable ExtensionはSQLiteに関数とか何やかんやを足すことができる機能である。

基本的な使用方法としてはsqlite cliから .load ライブラリへのpath を使うとloadすることができる。

今回、Loadable Extension を作ることはせずに既存のextensionを使っていく。

そもそもの発端としては sqlite内でbase64のencode/decodeしたかったので以下を使う。

なんでsqlite内でbase64のencode/decodeをしたかったのか、という点は直接聞いてもらえれば。

github.com

ビルド編

実は最近はLoadable Extensionの仕様が変わっており、上記のcodeは現代的な書き方ではないので変更していく

変更し終わったコードは以下。

github.com

nix build .#sqlite3-base64 でビルドできるはず。

macの場合、 export DYLD_LIBRARY_PATH=$(nix build .#sqlite3-base64 --print-out-paths --no-link)/lib のようにしてあげれば

sqlite cli 内で.load libbasesixtyfour で loadできるようになる。

勿論、 .load 絶対PATH でも良い

以下、変更点解説など

変更点: #include <sqlite3ext.h>

拡張用のheaderになっている

変更点: sqlite3_basesixtyfour_init

ちょっと未確認 だが、.load libxxxx.so で呼び出した際に、 sqlite3_xxxx_init が呼び出されるようだが、xxxxに数字が含まれるとうまくいかなかったので名前を変えている。 libファイルの名前の制約のように感じる・

変更点: SQLITE_EXTENSION_INIT*

静的リンクする場合に備えたmacroで SQLITE_CORE が定義されているかどうかで挙動が変わるようになっている

静的リンクに向けて

実はsqlite 3.40までは既存のコードに変更を加えずに素直に静的ビルドするのが難しい

ここで良い話と悪い話がある。

SQLite: Check-in [07543d23]

良い話は上記のcommitでビルド時にフラグを追加するだけでビルドできるようになる。

悪い話は上記のcommitでbase64の機能が追加されたので、実は上のextensionをloadしなくても良くなった。

つまり sqlite3でbase64を使いたいだけの人間は そのうちリリースされる予定のsqlite 3.41を使えば良い

今回はモチベーションを失いつつも静的リンクに向けてやっていく

静的リンクビルド解説

元々どういった手法でビルドすれば良いかを明確に解説したドキュメントはないのだが、

SQLite Forum: Decimal extension in amalgamation?

SQLite Forum: Latest trunk/extensions & UDF's on tap

を見ると、とにかく shell.c に拡張用のcを結合しつつ SQLITE_EXTRA_INIT に init用の関数を指定しろ、と言うふうに読める。

ちなみに SQLiteは amalgamationという全てのソースコードを1ファイルにまとめる操作を Tcl という言語を使いつつやっているのだが

「とんでもないコードが出てますよ今」

という印象を受ける

そんな操作をしているので cat ext.c >> shell.c すれば良いという雰囲気になっている。

しかし、直近は SQLITE_EXTRA_INIT 無くなったようで、上記commitを見ると, SQLITE_SHELL_EXTFUNCSSQLITE_SHELL_EXTSRC や以下のmacroが追加されている。

#define SHELL_SUB_MACRO(base, variant) base ## _ ## variant
#define SHELL_SUBMACRO(base, variant) SHELL_SUB_MACRO(base, variant)
    /* Let custom-included extensions get their ..._init() called.
     * The WHATEVER_INIT( db, pzErrorMsg, pApi ) macro should cause
     * the extension's sqlite3_*_init( db, pzErrorMsg, pApi )
     * inititialization routine to be called.
     */
    {
      int irc = SHELL_SUBMACRO(SQLITE_SHELL_EXTFUNCS, INIT)(p->db);
    /* Let custom-included extensions expose their functionality.
     * The WHATEVER_EXPOSE( db, pzErrorMsg ) macro should cause
     * the SQL functions, virtual tables, collating sequences or
     * VFS's implemented by the extension to be registered.
     */
      if( irc==SQLITE_OK
          || irc==SQLITE_OK_LOAD_PERMANENTLY ){
        SHELL_SUBMACRO(SQLITE_SHELL_EXTFUNCS, EXPOSE)(p->db, 0);
      }
#undef SHELL_SUB_MACRO
#undef SHELL_SUBMACRO
    }
#endif

これは目を細めて凝視すると、

#define BASESF_INIT(db) sqlite3_basesixtyfour_init(db, 0, 0)
#define BASESF_EXPOSE(db, pzErr)

という initを呼ぶ macroを定義しつつ SQLITE_SHELL_EXTFUNCS=BASESFSQLITE_SHELL_EXTSRC=拡張関数.cへのpath のフラグを渡せばい良い、ということがおぼろげながら浮かんでくる。

あとはやるだけなので以下のnixファイルからビルドする。

sqlite.overrideAttrs (final: prev: {
  src = pkgs.fetchurl {
    url = "https://sqlite.org/snapshot/sqlite-snapshot-202302052029.tar.gz";
    sha256 = "sha256-MNjujQGJ/QlKs/3gUGna2G6Fmc/gAZ4qxmXflWH5DH0=";
  };
  buildInputs = prev.buildInputs ++ [ pkgs.libb64 ];
  NIX_CFLAGS_COMPILE = prev.NIX_CFLAGS_COMPILE + " -DSQLITE_CORE=1 -lb64 -isystem ${pkgs.libb64}/include/b64 -DSQLITE_SHELL_EXTSRC=${sqlite3-base64}/sqlite3_base64.c -DSQLITE_SHELL_EXTFUNCS=BASESF";
}))

実行結果

❯ sqlite3 
SQLite version 3.41.0 2023-02-05 20:29:10
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> SELECT BASE64(X'010203');
AQID
sqlite> SELECT HEX(BLOBFROMBASE64('AQID'));
010203
sqlite> 

よかったですね


SAIYO JOHO

github.com

第4回しろさとTT反省会

これは何?

チャリを買ったので3ヶ月練習して、しろさとTTというチャリを漕ぐ会に参加してきて思ったことを書く

参加は50kmの部門。

shirosato-tt.com

参加するまで

なんか世間ではzwiftという家でチャリを漕ぐやつが流行ってるらしいじゃね〜か、というインサイトを今年の夏頃に得て

チャリを...漕ぐカァ...という気持ちが高まり諸々を買い、せっかくなのでトレーニングして上のレースに出る意思決定をした。

zwiftを買ってよかったのはFTPというチャリ界の階級みたいなものに基づいて科学的な根拠のあるトレーニングができるので、根性論的なやる意味をいちいち考えさせられるような運動をさせられないのが良い

というか学生時代の部活の体力トレーニングって人間ごとにメニューを組む分けではなかったので、無意味で自己満足を育ててやっている感を出してるだけだなって思ってしまった

ちなみにFTPというのは1時間漕ぎ続けることができる限界のパワー(単位はワット)になる

練習など

練習メニューを組む上でとりあえずFTPを測らないといけないので、なんか測り方が色々あってその中でシュッと測れる RAMP testで測った

RAMP testはアスリート以外は基本的に高く出ることが多いらしい

どのみち練習メニューをこなした際にターゲットの心拍数ゾーンが設定されており、それより高く出ていればFTPの値が間違っていると考えることができるので下げたらいいと思う。

ちなみにやってみた結果は以下のようになった

8月:152w 9月:184w 11月:214w

練習メニューは目的に合わせてzwiftの初心者向けのトレーニングメニューであるFTP Builderというの一旦2ヶ月やった

途中、コロナっぽい風邪に罹って1週間休んだが概ねメニューは完遂した。

本番前

申し込みの時点では51kmあたり大体30km/hの1時間40分ぐらいを目標にしていたが

事前にzwiftや路上で漕いでみた感じでは 普通の姿勢で31km/h エアロポジションで32km/h ぐらいはいけるかなという感じだったので、 時速33km/hの1時間32分あたりを目標にした。

元々、クレアチンを取っていたが一旦飲まない期間を置いてから本番1週間前にクレアチンローディングをした。

ケトジェニックで体を慣らせば有酸素能力が一時的に上がるのでは?と思い本番2週間前から1週間ぐらいにやってみたが、科学的根拠に足る文献が無かったから今のなしで、ってなった

ただ糖質オフすると明らかに練習の負荷が高まってその後糖質をとるとめちゃ楽にこげる!ってなるから気分上の効果はあるかもしれない

本番&振り返り

チャリの会は初だったので若干不安だったがなんとかなった。

結果は1h34m30sだった

練習で50kmの距離をエアロポジション取り続けることは無かったので不安だったがずっと取り続けれたのはよかった。

心拍数のavgは166で1時間半のTTの場合はおそらくL4領域の心拍数に収まっていたらいいと思うので、そこらへんのコントロールはうまく行った説はある。

後半の足の前の筋肉がかなり"キてしまった"ので、尻で漕いでた感じがある。足がきてしまったのも一定のパワーで漕げていないことからスピードを維持しようとして不安定な出力をしてしまった感じがある。 あと、1時間半通しのトレーニングをあまりしていなかった。

zwiftで今漕いでいるパワー(ワット)が出るが、持っているチャリにはつけていないので、練習通りのパワーコントロールができていないという課題がある。

ケイデンスは安定はしてた気もするような、してないような?

気づき

とりあえずトレーニングをしてFTPを上げてくしかない

エアロポジションの研究はしたいが、ハンドルまわりを変えないといけないし計測しにくいので難しいなって思った

パワーメータは流石に欲しいかもしれない。エンジニアとして「推測するな計測せよ」というのは大事にしないといけないので、計測できる環境をまず作るか

来年の3月に第5回があるのでそれも出る。

最近はNix on raspberry pi zero をやった

イントロ

raspberry pi zeroをちょっと前に2枚買ったのが特に用途がない

強いて用があるとすれば、この前買ったserial通信経由でデータが取れるco2計測器がLinuxの方が読みやすそうだなってぐらい。 これはこのco2計測器が悪いわけではなく、macにマウントされるserial device?がちょっといまいちな挙動をしているからだ。

Amazon.co.jp: 国内生産 日本製 二波長CO2センサー搭載マイコン EPEA-CO2-NDIR-08 PC連携可 二波長NDIR方式 Dual Beam CO2モニター 二酸化炭素濃度計 : 産業・研究開発用品

というわけで 何かと開発する上で便利なので、Raspberry Pi OSにnixをいれていく。

余談だが、rasipbian OSはRaspberry Pi OSに名前が変わっていた。

install

Nixをいれるのは適当にやってもらっていいが、以下の行に注意する必要がある

nix/install.in at master · NixOS/nix · GitHub

"$(uname -s).$(uname -m)" の実行結果が Linux.armv6l_linux になることを期待しているが、 少なくとも自分の環境では Linux.armv6l になる

これは Linux.armv6l_linux が返ってくる環境が世の中にあるのかわからないのでバグなのかわからないが、armv6lのRaspberry Pi OSにNixを入れようとしている人間がzeroに近いことを演繹させてくれる有力な証拠である。

ちなみに NixOS on raspberry pi zeroをやっている人間はGitHubを見た感じでは何人かいることが観測できたので、そのうちやると思う。

build on pi

普通の環境では(ここでいう普通の環境とはflake-utilsのdefaultSystemsとか)「はい!あとはプログラムを書いて終わり!」となるが、ここはarmv6l、一切のcacheが降ってこない砂漠地帯、nix-shellが要求するbashInteractiveですらgccのビルドから始まってしまう。

というわけで gccとstdenvのビルドをやっていく。

まぁ時間さえあれば終わるでしょうと思っていてビルドを走らせていたわけだが、途中で止まってしまう。

どうやらメモリが足りなくなってOOMで落ちたっぽいエラーを観測した。足りないのは時間ではなくメモリ。

正直 パイゼロは500Mしかメモリがない、がしかし「20年前のPCでも500Mあったし流石にgccはビルドできるのは?」という気持ちも捨てがたい。そして「SD上にswapを増やしても大変な遅さになってしまうのでは?」と思いパイゼロ上でビルドするのはやめた。

なのでここからはmacでビルドしてラズパイに送る戦略をとる。

build on mac

ところでDockerDesktopという便利なものがある。何が便利かというと、マルチCPUアーキテクチャサポートがある

つまり armv6lのlinuxが豊富なメモリと屈強なCPUを積んだmacで動いてくれるわけだ。

今回は arm32v6/alpine Docker Hub にNixをインストールする戦略をとる。

余談だが NixのcrossCompile機能を使ってやるという手法もあるのだが、derivationが異なってしまう恐れを直感的に感じ取り今回はやめた。 具体的には crossCompileでビルドしたgccを使って ラズパイ上で他のものをビルドすること際に気を使う必要がある可能性を感じた、これは試していないからわからない

話を戻し、まずは bashをビルドしていく。ここで必要なのは時間と忍耐力で、具体的には一晩の時間とmacのファンの音を気にしない心だ。

ここで 一旦 ラズパイの方に できた bashを送る。 nix copy -s --to $ビルドされたbashのpath

送る際に because it lacks a valid signature と言われると思うので、nix.confに require-sigs = false を足して nix-daemonを再起動させる。

おそらく動くと思うし、bashをbuildしようとしてもbuildされずに先程送ったpathが返ってくると思う。もしビルドが始まった場合同一のderivationをビルドしようとしていない可能性が高いと思う。

あとはstdenvをビルドしていく。

あとはビルドするだけと思いきや、ここでstdenvの依存の中に含まれるpcreのtest phaseでコケてしまう。

「そんなわけがないだろう」という気持ちになるが、手元にはすでにコンパイル済みのgccがいるのでラズパイに送りさえすればビルドできるので試してみたところ pcreはビルドできてしまった。

ビルドできてしまったこと自体も「そんなわけがないだろう」という気持ちになったが、pcreがビルドされていれば後はmacのdocker側に持って来ればいいだけなので nix copy --from で持ってくる。もはやキャッチボールだ。

stdenvとbashInteractiveをラズパイ側に送りつけ、 nix-shell できることを確認する。

よかったですね

何故こういったことをやっていたのか忘れてしまったが、おおよそ、私の目的はビルドでプログラムの実行は副作用でしかないので、よかったですね。

pcre debug編

なぜpcreのテストが失敗したのか調査していくと pcregrepの -L オプションで失敗しているようだった。

pcreのソースコードを読んでいくとreaddirの挙動がおかしいように感じたので、疑似的なプログラムを書いて試してみると確かに動かない。

比較するために

  • mac
  • docker(armv6l)
  • docker(x86_64)

の三つの環境で疑似プロを動かしたところ armv6lでだけ動かない。

最初はoverlayfsあたりが悪さをしているのでは?と考えたがどうやら違うらしい。

そもそもマルチCPUアーキテクチャqemuを使っているので、そこら辺のワードと共に探っていくと以下のIssueに行き着いた。

gitlab.com

これは...ダメですね...

テーマは「情報収集について」

RSS

自分の場合、日常的なInputとして使っているのはRSS(Feedly)で大体以下のRSSかな

ほんとは他にもフォローしているブログがあったのだが、更新されなくなってしまった

まぁはてブの人気エントリは1ヶ月で2000ぐらいあったりするので大体それ見とけばいいとなっている 基本的にタイトルは全て追っていて、気になったり知らないことに関して書かれた記事は読むという感じ

Twitter

Twitterもソースとして当然使っているが、とはいえメジャーな技術の記事は上に含まれているような気もする。

なのでニッチな技術に関してはその第一人者をfollowしておくとかで有用、例えばNixなら Domen Kožar (@domenkozar) | Twitter とか

たまに海外記事のまとめをReplyで翻訳し始める人間もいるので、そう言った場合に有用。

AWS

他にはAWS ニュースレターとかもAWSのアップデートを追う上ではまぁまぁ有効、最近は新サービス開始時にTokyo regionもおま国されないのが嬉しい

nixpkgs

github.com

nixpkgsのcommit logとか適当に見ているだけでかなりの情報がある、これは流石に全部見れんが

GitHub

遭遇したIssueはとりあえずSubscribeしとくと便利、あまりfollow機能は使っていない

Web

あと、なんか普通に仕事してたりTwitter見ていたら、知らないことって結構出てくるので納得いくまで調べることが多い

shell.nixとdirenvでプロジェクトごとに補完をいい感じにする

概要

HERPでは shell.nix をgit repositoryのrootに置いて依存関係を解決しているが、PATH がprojectごとに設定されるだけなので 例えば kubectl の補完が有効になるわけではない。

その課題を解決するために shell.nixzshの補完もうまく済ませるワザップを今回は書く。

zsh completion について

zsh: 20 Completion System

zsh の completionの仕組みをざっくり書くと、 FPATHをいい感じにして compinit すれば FPATHから解決されるcompletion用のscriptが実行され補完される

手法

FPATH用のderivation

まず、completionを集めたderivationを作る。

buildEnv というderivationの特定のディレクトリの集合体を作る関数があるのでそれを使う

FPATH = "${nixpkgs.buildEnv {
     name = "zsh-comp";
     paths = buildInputs;
     pathsToLink = "/share/zsh";
   }}/share/zsh/site-functions";

FPATHの設定

上記で作った FPATH$HOME/.zshrc で追加された FPATH に足せばいいのだが

安易な方法として shell.nix にFPATHを足してしまうと、そのまま上書きされて既存の補完の全てを失ってしまう

なので、 project_FPATH のような環境変数に一時的に保存する。

このまま nix-shell --run zsh のように実行する場合は, ZDOTDIR をshell.nix中で上書きするなどのコードをを足せば問題ないのだが、 いちいち実行するのが面倒なので direnvを使っている場合は工夫が必要になる。

use nix
path_add FPATH $project_FPATH

以下の理由でこのコードは動きそうに見えて動かない(人によっては動く)

direnvは .envrc の評価をbash上で行っており、評価した結果の差分を既存のshellに足すような実装になっている。(コレは echo $0 してあげるとわかる)

実はzshFPATH は exportされていない変数なので (コレは echo ${(t)FPATH} でわかる、コレをlocal変数と言っていいのかはわからない) bash上で評価した際に存在していない変数として扱われFPATHが上書きされる

なので direnvの評価が始まる前、つまり ~/.zshrc などで export FPATH (typeset でも可) しておく必要がある

export FPATH してない人間向け対応

世の中には export FPATH していない人間もいるので、そう言った人が projectに入って direnv allow した瞬間に壊れて cd するのも困るのは不親切という考え方もあるので対応をする。(考え方によっては学習機会を奪っている。人間は困らないと進化しないので)

上記で書いたように 評価がbash上で行っているので export -p した中に FPATH が含まれていなければ export FPATH されていないことがわかる

よって以下のようになる

use nix

if export -p|grep "declare -x FPATH" ; then
  path_add FPATH $project_FPATH
fi

compinitの設定

あとは compinit するだけなのだが毎回 compinit していてはダルい(ダルい)

なので direnv がhookで評価した後にFPATHが書き変わっていれば compinit するようなhookを作ってあげれば良い

export COMPINIT_DIFF=""
_chpwd_compinit() {
  if [ -n "$IN_NIX_SHELL" -a "$COMPINIT_DIFF" != "$DIRENV_DIFF" ]; then
    compinit -u
    COMPINIT_DIFF="$DIRENV_DIFF"
    echo "compinited !"
  fi
}
if [[ -z ''${precmd_functions[(r)_chpwd_compinit]} ]]; then
  precmd_functions=( ''${precmd_functions[@]} _chpwd_compinit )
fi
if [[ -z ''${chpwd_functions[(r)_chpwd_compinit]} ]]; then
  chpwd_functions=( ''${chpwd_functions[@]} _chpwd_compinit )
fi

FPATH の diffを取ると言ったが色々考えた結果,nix-shell中でdirenv が実行されたときにcompinitされればいいので上記のようにした。(compinit以外のこともしたくなるかもしれない)

蛇足として一応書いておくが autoload compinit は他でするように

終わりに

nix-shelldirenv の組み合わせは環境を揃える手法として有名だが、実は補完まで揃えることができる。 各個人のzshrcに追記する必要はあるがオススメできる手法である。

余談

herp.careers

HERPのSREチームはnixを使って各種ツールの環境を揃えている。