Linux版OneShotの終盤でファイルが開けない(ネタバレ注意)

 この記事は以下の記事の続きです。

octahedron.hatenablog.jp

 上の記事の内容も利用しているので、適宜参照ください。

 ストーリー終盤のギミックについての不具合対処記事なのでネタバレを含みます。ご注意を!!

続きを読む

OneShot (ゲーム)をUbuntu 18.04で動かす

あらまし

 先日、チャンスは一度きりなゲームOneShotGNU/Linux版がリリースされました。ふだんUbuntuのLTS版を利用しているので、いままでWindows 10に切り替えなければ遊べなかったOneShotをいつもの環境で起動できるのはとてもうれしいです。

steamcommunity.com

 しかし要求してくるライブラリのバージョンがとても新しく、LTS版では起動できませんでした。この記事ではそれをなんとかしてみます。

発生現象

 Steam GNU/Linux版でOneShotをダウンロード後に起動しようとすると起動中っぽい動きを一瞬したあとそのままなにも起こりません。これはプログラムの起動に失敗して無言で落ちているためであり、ターミナルで直に起動しようとしてみると以下のような出力が得られます。

# OneShotがインストールされたディレクトリに移動
$ cd ~/.local/share/Steam/steamapps/common/OneShot
# 起動 (失敗)
$ ./oneshot
./oneshot: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by /home/grey/.local/share/Steam/steamapps/common/OneShot/libgio-2.0.so.0)
./oneshot: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by /home/grey/.local/share/Steam/steamapps/common/OneShot/libglib-2.0.so.0)
./oneshot: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.28' not found (required by /home/grey/.local/share/Steam/steamapps/common/OneShot/libsystemd.so.0)

 これは、システムにインストールされたglibcライブラリのバージョンが古いと言っています。システムの情報とglibcのバージョンを確認してみると、以下のようでした。

# 環境情報の確認
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.2 LTS"
grey@timberwolf:~$ /lib/x86_64-linux-gnu/libc
libc-2.27.so  libc.so.6

# glibcのバージョン確認
$ /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.3.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

 おしい! 1つだけ足りない! それにしてもglibclsといった各コマンド自体も依存している動的ライブラリですので、ディストリビューションが提供するアップデートなしには容易に差し替えられません。ちなみに「OneShot用のglibcコンパイルしてゲーム本体のディレクトリに置けばいいじゃん」とやってみたところダメでした。差し替えたglibcを向くようにLD_PRELOAD環境変数で指定しても.so中のオブジェクト等の位置が異なるのかセグフォになります。むずかしいですね。

対処方法

 このスレッドにも書かれているのですが、glibcを差し替えて解決するのではなく新しいglibcに依存しているライブラリを使わないようにするのが楽かつスマートです。

steamcommunity.com

 つまり、OneShotディレクトリにある新しいglibcに依存する.soは削除もしくはリネームしてしまい、システムにapt等で対応するライブラリを入れましょう。

$ cd ~/.local/share/Steam/steamapps/common/OneShot
$ rm libgio-2.0.so.0 libglib-2.0.so.0 libsystemd.so.0

 これでglibcのエラーはでなくなります。

そのあと

 ぼくの環境では、この後Error initializing SDL: Could not initialize UDEVというエラーがでてまだ起動できませんでした。udevとはユーザ空間でデバイスドライバを動的にロードしたりするライブラリっぽいのですが、きっとシステムのものではないudevを用いているせいかもしれない(libudev.so.1が実際ある)のでこれも消してしまいましょう。

 そうするとエラーの内容がCould not initialize Steamworks APIに変わります。きっとStreamが起動していて認証されてないと動かないのでしょうね。そこで、今まで./oneshotを起動していたのですが同じディレクトリにある./steamshimを起動してみましょう。Steamへの認証っぽいログがでたあと…。

f:id:t-sin:20190502000343p:plain
OneShot起動成功!

 やったー!!

 あとは遊びましょう。

Clozure CLでプロファイラを利用する

2019-05-02 19:51追記

プロファイル取得用のイメージを作成する手順がありますが手順を間違えると処理の実行のたびにfaslのダンプが行われて計測結果を汚すので注意してください。イメージ作成手順にも追記します。

TL;DR

 Clozure CLの以下ドキュメントに書いてある通りです(本記事では素のCCLではなくRoswell上での利用方法が書かれています)。

ccl.clozure.com

動機

 PortAudio/cl-portaudioを用いてCommon Lispサウンドプログラミング実験をしています(どんなことをしているのかはざっくりこちらのスライド参照。いつか記事かなにかにしたい)。

github.com

 ある程度音が鳴らせるようになってきたので、ちょっと複雑な音を鳴らしてみようと思いました。が、プログラムの実行速度が遅いのかバッファアンダーランが発生して音がぶちぶちに切れてしまい、これでは音楽つくるどころではありません。しかしここまで大きく育ったプログラムのどこがボトルネックか、ぼくにはパッとわかりませんでした。

 なのでプロファイラを用いてどの処理が重いのかとても知りたいです。

 ちなみに普段使っている処理系はClozure CLなので、SBCLに組込みのプロファイラは使えません(さらに言うと何かがおかしいのかpukunuiをSBCLでロードすると型エラーになったような気がする)。

環境や準備

環境

使うもの

手順

 ここではサンプル用のコードを説明用に用意せず、pukunuiをそのまま使って手順を説明します。しかし使うだけならそんなに難しくないはず…。

pukunui関係準備

 Pukunuiではcl-portaudioを通してlibportaudio2を利用しますので、aptでインストールしてください。Pukunui本体も.roswell/local-projectsとかに置いている前提です。

プロファイルを取るための準備

 Oprofileでは起動前・起動後の対象を指定してプロファイラを起動し、その実行中の情報をOSを介して取得するしくみです。対象に指定できるのは以下です:

  • システム全体
  • 指定したpidのプロセス
  • 引数に指定した実行可能ファイル(とその引数)を起動したプロセス

 今回はpidを指定してプロファイルを取得します。まずCCLでしておくべきことは

  1. プロセス内のシンボル一覧をファイルに保存
  2. 上記シンボル一覧と関数が対応する処理系のイメージを作成

です。ros use ccl-binが済んでいれば簡単なのでやってみましょう。

$ ros run
Clozure Common Lisp Version 1.11.5/v1.11.5  (LinuxX8664)

For more information about CCL, please see http://ccl.clozure.com.

CCL is free software.  It is distributed under the terms of the Apache
Licence, Version 2.0.

;; シンボルテーブルを保存
? (require :elf)  ; 必要なパッケージをインポート
ELF
? (ccl::write-elf-symbols-to-file "elf-symbols")  ; `elf-symbols`にシンボルが書き出される

;; `elf-symbols`に関数が対応した処理系イメージを作成し終了
? (save-application "pukunui-prof-image" :prepend-kernel "elf-symbols")
$ 

2019-05-02 19:54追記

ちなみにライブラリのasdf:load-systemや計測に必要なLispプログラムのloadccl:save-application前にやっておきましょう。「ライブラリロードはイメージ作成前に、プログラムのロードは計測直前に」などとするとClozure CLではロードしたプログラムのfaslへのダンプ処理が計測対象実行の度に走るようです。これによりCCL::FASL-DUMP実際のコードでは使っていない処理が計測結果に登場してプロファイリングを困難にします。ご注意ください。

プロファイルを取る

 ここからはターミナルを複数立ち上げて作業します。

 まずCCLの処理系を先程のイメージで起動します。Roswell越しにCCLにオプションを渡し、イメージを指定します。その後、プロファイルを取得する前の準備(プログラムのロードや初期化)をします。 2019-05-02 19:54追記 プログラムのロード等はイメージ作成前に行ってください

$ ros run -- -I ./pukunui-prof-image
...

;; プログラムの初期化 (ここではpukunuiのロードと音を鳴らすプログラムの準備)
? (ql:quickload :pukunui)
PUKUNUI
? (load (asdf:system-related-pathname :pukunui "examples/04-seq.lisp"))
#P"pukunui/examples/04-seq.lisp"

 これでpukunuiの初期化は完了です。スタートする前に、プロファイラを別ターミナルで起動しておきます。

$ ps aux | grep lx86cl64   # 上で起動したCCLのpidを調べる
$ sudo operf --pid xxxx
...

 プロファイルを取得するためにCCLのREPLで(pukunui:start)を打ちます。カービィの音が鳴り終わったら、プロファイラのほうでCtrl-xを押してプロファイラを止めます。プロファイラを止めるとoprofile_dataというディレクトリができており、そこにプロファイル結果が入っています。サマリを見てみましょう。このとき、実行したCCLのイメージを指定すると内部のシンボル名が何回呼ばれたかが表示されます。

$ opreport -l pukunui-prof-image  | head
Using /home/grey/Dropbox/code/pukunui/examples/oprofile_data/samples/ for samples directory.

WARNING! Some of the events were throttled. Throttling occurs when
the initial sample rate is too high, causing an excessive number of
interrupts.  Decrease the sampling frequency. Check the directory
/home/grey/Dropbox/code/pukunui/examples/oprofile_data/samples/current/stats/throttled
for the throttled event names.


WARNING: Lost samples detected! See /home/grey/Dropbox/code/pukunui/examples/oprofile_data/samples/operf.log for details.
CPU: Intel Ivy Bridge microarchitecture, speed 3900 MHz (estimated)
Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (No unit mask) count 100000
samples  %        image name               symbol name
4434      7.9773  pukunui-image            CCL::FASL-DUMP-EPUSH
4358      7.8405  pukunui-image            RANDOM
2882      5.1850  pukunui-image            CCL::FASL-DUMP-DISPATCH
2780      5.0015  pukunui-image            CCL::%BIGNUM-RANDOM
2553      4.5931  pukunui-image            CCL::%STRING-HASH-FOLDING-CASE
1948      3.5047  pukunui-image            <Compiled-function.(:INTERNAL.CCL::GET-LOCAL-VALUE.CCL::ARGUMENTS-AND-LOCALS).(Non-Global)..0x300000A0412F>
1815      3.2654  pukunui-image            FLOAT-SIGN

 なにか警告がでていますが、全イベントが取得できなかったみたいです。まだよくわかってないですが、改善の余地があるということなのでしょう。

所感

 プロファイルを取得するときは本当に計測したい部分でのみプロファイラを起動(=プロファイルデータを集める)しましょう。pukunuiは例えば信号処理プログラムですが、最初はライブラリの初期化を含めて計測してしまったため、信号処理には関係ない(本来の計測対象ではない)処理が上位に出てきてしまいます。

 結果を見ている感じ、Pukunuiに関してはたぶん自前実装オブジェクトシステムもどきのあたりがボトルネックになってそうでした(ccl:%string-hash-folding-caseとかあるので。自前オブジェクトは型とメソッドの対応表がハッシュテーブルに入っている)。あとRANDOMってなんじゃい(上のプロファイル結果のプログラム 06-applications.lispでは使っていない)。Pukunuiが変な挙動をしているのかもしれません。

 やはりKiwi Lightweight Object Systemが必要か…。

所感追記 (2019-05-02 19:54)

 上のプロファイル結果にCCL::FASL-DUMP-EPUSHなどが現れていますが、これはpukunuiのコードで行われている処理ではありません。上で述べたようにプロファイリング用イメージの作成手順を誤ると走ってしまうようです(詳細な原因は不明)。

 また、randomexamples/06-applications.lispを用いていれば登場しないはずですので、計測用イメージの作成ミスかもしれません。計測の準備は注意ぶかく行いましょう。

2018年振り返り

2018年振り返り

 今年はいろいろあったので、振り返りしてみようという気になった。触発されたというのもある。

働いた

 2017年末にはなんか落ち込みが発生して自信をかなり損なっていた時期があったけど、2018年1月からは働きはじめた。対話AIをやっている会社で、それまでSIer (Java)やWeb屋さん(Python)としてやってきたぼくとしては新たなチャレンジだった。とはいえWebまわりをやっているけれど、いろいろなことを学んだし、(Lisperなのに)(遅いけど)対話AI、というか人工知能に興味を持つことができた。神戸大学LISPマシンを見にいったときのことも大きなきっかけである(第7回関西Lispユーザ会に行ってきた)。

読書

 あんま読んでない…。スループットは悪く、年に24冊だった。内訳としては、小説多め。今年は『エコープラクシア』『最後にして最初のアイドル』『リスの生態学』『貨幣の新世界史』『エル・アレフ』『死刑 その哲学的考察』『ペンギンの島』『トランスヒューマンガンマ線バースト童話集』、などなど。SFは収穫多かった。就職祝い(セルフ)に買ったイーガンの直交三部作は本棚をしっかりと支えています。読みたい。あとは、後述するけど音声信号処理関連の読んだけど、まだわからん…。あと、ついに『LET OVER LAMBDA』を読んだよ!! マクロって書いていいんだね!!

太った

 なぜだかわからないけど太った。きっと運動を控えめに、食事を盛大に行ったためだと思われる。怖いので体重を計測していない。帰省しているので、戻ったらまずは計測して、そして、運動をしてやせたい。

プログラミング

 いくつかトピックがあるので分けて書く。

シンセサイザーづくり

 つくってみたいもののひとつだったので、えいやっとつくりはじめてみた。とりあえずノリで実装してみてたけど、平行して書籍を読むこともした。『サウンドプログラミング入門――音響合成の基本とC言語による実装』で基本を押さえ、『サウンドエフェクトのプログラミング―Cによる音の加工と音源合成』でエフェクトのつくりかたを…わからなかったけど…。音を鳴らすのは簡単(でもない)けど、シーケンサを生やすためのイベントスケジューラを書けなくて、実力の不足を実感した。シーケンサ、生えろ。

Nim -> Rust

 興味があったのでNimを去年からいじっていた。初めてのLispをつくってみたり、上のシンセサイザーつくりに利用してみたりした。ただ、なぜか処理系の不具合を踏み抜いたようで、シンセづくりが完全にストップしてしまったのであきらめた。そのままRustに入門して、絶賛勉強中。ちなみにシンセはCommon Lispでもつくりはじめてみた。シーケンサがやっぱり鬼門。

言語を処理でき…そう…?

 去年末はNimでLispをつくっていたけど、仕組みがわからないので引き続き勉強していた。パタヘネ読んだし、仮想機械のサーベイをしたり、実際にCPUエミュレータVMを実装してみたりした。そしてForthとの出会い。そのあたりはこちら『プログラミング言語Forthに魅せられて。』にしたためておいた。定数を扱える、規格の動作をすこしずつ再現できてきているので、もしかしたらいわゆる「ちゃんとした言語」というやつになるかもしれない。

来年は

 いろいろ結実して、生えてきてほしいなあ。

セルオートマトン暗号(未完)

遅刻してますが、この記事はセルオートマトンアドベントカレンダーの23日目の記事です。

あらまし

 離散的な力学系であるところのセルオートマトンは、カオス的なふるまいをすることで知られています。カオス的であるなら、そのふるまいを暗号学的なアレとかソレとかに利用できるのでは…、とはもう研究があるようですがここでは割愛。いっぽうで、セルオートマトンの中には可逆性をもつものがあります(ここらへんも研究がありますが割愛)。

 あとは…わかるね?

 ということでとりあえずElementary Cellular Automataを用いてガッとメッセージの暗号化・複合化を実装しようとしたけど3時間ではだめでした、という残念な記事です。ざんねん。

ソースコード

文字をつらつら書くほど内容はないのでさっさとソースコードを示します。ひさしぶりにビット演算をキメた気がする。以下のことはできています:

  • 局所遷移関数をルール番号から生成(コンパイル時に)
  • 周期的境界条件(キアイで)
  • なんとなく様相を指定回数ぶん遷移
  • ファイルをビットベクタにしてECAの様相に変換しブンまわす

gist.github.com

使用感

 なんとなくこんな感じになります。

CL-USER> (let ((vec (vector 0 0 0 1 0 0 0 1 0)))
           (run 10 vec (make-array (length vec))
                (make-local-transition-fn-from-rule 150)))
#*000100010
#*001110111
#*110100010
#*000110110
#*001000001
#*111100011
#*111010101
#*110010100
#*001110111
#*110100010
#(0 0 0 1 1 0 1 1 0)

 なんとなく大域遷移はできていそう。Roswellスクリプトにしておいたので、引数にファイル名を与えるとファイルの内容で遷移してくれます。しかしビットベクタをバイトベクタとして書き出してしまっているので出力はおかしいですがもうつかれました。

まとめ

 このセルオートマトンという分野、人工生命というだけではなく暗号学の方面の研究もあり、さらに情報の圧縮や、果ては様相の遷移を群と見なして解析する純粋数学的な分野もあり、とてもたのしそうです。やりたい。

参考文献

Common Lisp (Roswell)とDockerで実行可能ファイルをビルド

 この記事はLispアドベントカレンダーの7日目の記事です。

ざっくり

 Common Lispの処理系マネージャ・スクリプティング環境であるRoswellとライブラリバージョン固定化ツールQlotを用いて、

  1. Common Lispのプロジェクト(システム)の実行可能バイナリを作成し、
  2. Common Lisp環境が入っていない人でも実行できるようDockerコンテナ化し、
    • 推し進め、コードに変更があったときでも高速にビルド
  3. Common Lispプロジェクトのビルド環境としてDockerコンテナを利用

します。そしてその結果がこのリポジトリ(のsolsticeブランチ)です。

github.com

背景・動機

 おしごとで書けたり、あとは自分でつくっているプログラムであったりを、ひとつの実行可能ファイルにまとめて、さっと配布したりデプロイしたりしたいなあ、という動機です。

 自分でつくっているプログラムについてはこの記事のやつです。

octahedron.hatenablog.jp

 次のバージョンをめざしていろいろ実験中です。ビューのHTMLや画像をメモリに持ってみたり、テンプレートエンジンをLSXにしてみたり(問題の ツイート)、シングルバイナリ化もそうだし、あと、Dockerコンテナ内高速ビルド(二回目以降)など。

 Dockerコンテナ化には、もともとはおしごとで、パートナーがMac使いだったために発生したものです(ぼくはGNU/Linux使い)。そこでマルチステージビルドによるコンテナイメージの作成過程をキャッシュする方法などを覚え、そこそこストレスのない感じになっています。

RoswellとQlotについて

 去年の拙作のこの記事にも初心者向けな感じで書いていますが、いま一度。

qiita.com

Roswell

 Common Lispには処理系が複数あり、マイナーな言語であるため最新のバージョンがaptのようなパッケージリポジトリに入っていないので、常用している場合にCommon Lisp環境をさっと導入するのはあまり簡単ではありません。

 では、RubyのrbenvやPythonのpyenvのようなものがほしくなるのですが、あります。それがRoswellです。

 Roswellは基本的には処理系マネージャなのですが、そのほかにも以下のような機能が提供されています(公式のREADMEより):

 Common Lisp各処理系のコマンドラインインターフェースはばらばらであり、元来処理系ポータブルなスクリプトを書くことはめんどくさいものでした。Roswellのスクリプティング機能は、処理系ポータブルに設計されており、ひとつのスクリプトを複数の処理系で動かすことができます。非常に便利です。また、Roswellではメモリのダンプイメージ作成機能を(一部の処理系で)備えており、Roswellのスクリプトを読み込んで実行可能ファイルとして吐き出すことも可能です。

 なので、ここではそれらを組み合わせて、実行可能ファイルのコマンドラインインターフェースとしてRoswellを利用しました。

Qlot

 Common Lispでは「システム」(他言語でいうパッケージ)の取得にQuicklispを利用することが多いです。でも、システムのバージョンを個々に指定する機能はないです。そのため依存するシステムのバージョンが導入のタイミングが異なるために変わってしまう、という状況があり得ます。

 そんな状況でお困りのあなたに、Qlotです。Qlotを利用すると、システムの依存システムを固定してしまうことができるので、過去につくったプログラムが依存システムの仕様変更などなどで動かない、ということを防ぐことができます。

 Node.jsのnpmみたいな感じです。

 これまではライブラリ的なもの(inquisitor)や、小さなもの(rosa、依存システムなし)ばかりつくっていたので必要性をあまり感じなかったのですが、依存ライブラリが増えてくると、その必要性を感じはじめて、導入しました。

配布・デプロイまでの道

1. プログラムの実行可能ファイルを作成する

 まず、システムの構成を考えます。どのようなシステムであれ、Common Lispにおける開発にはきっと対話的な環境を利用することでしょう(GNU EmacsとかLemとか)。なので、基本の構造は対話的に呼べるようにします。コマンドラインから呼ぶときには、

  1. Roswellスクリプトの中でシステムをql:quickloadし、
  2. コマンドライン引数をパースして、
  3. 対応した関数を呼ぶ、

という流れにします。ざっくりとしたフォルダ構成はこんな感じです(細部は省略しています)。

.
|-- Dockerfile
|-- LICENSE
|-- README.md
|-- main.lisp
|-- niko.asd         # 通常のASDFシステム
|-- qlfile
|-- qlfile.lock
|-- release.sh       # コンテナビルド用(後述)
|-- roswell
|   `-- niko.ros     # CLIで起動するRoswellスクリプト
`-- setup.sh         # コンテナビルド用(後述)

 Roswellスクリプトの中では、REPLでも叩けるように用意した種々の関数を、パースしたコマンドライン引数に応じて呼び出すだけです。つまりだいたいこんな感じです:

;; (中略)
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp(ql:quickload '(:niko) :silent t)
  )

(defpackage :ros.script.niko.3752905480
  (:use :cl))
(in-package :ros.script.niko.3752905480)

;; (中略)

(defun main (&rest argv)
  (if (= (length argv) 0)
      (format *error-output* +usage+)
      (case (intern (string-upcase (string (first argv))) :keyword)
        (:help (format *error-output* +usage+))
        (:version (format *error-output* "~a~%" (niko/util:version)))
        ;; (中略)
        (t (format *error-output* "Unknown command '~a'~%" (first argv))
           (format *error-output* +usage+)))))
;;; vim: set ft=lisp lisp:

 これで、まずは実行できるようになりました。動作確認すると、

$ qlot exec roswell/niko.ros version
0.9.0

よさげです。これをビルドするには、こうやります。

# ビルド
$ qlot exec ros build roswell/niko.ros

# ビルドした実行可能ファイルで動作確認
$ ./roswell/niko version
0.9.0

 こうして生成した実行可能ファイルを他の環境に持っていくときに気をつけることは、依存するシステムを、qlfile.asdファイルに可能な限り書いておくことです。

 Qlotはqlfileに書かれたシステムのみをプロジェクトのルートディレクトリに導入しますが、そのシステムたちがさらに依存するシステムは、グローバルな(Roswell環境下の)Quicklisp環境を利用するようです。そのため、動的にロードするシステムを切り替えるようなシステムに依存している場合にそれをqlfileに書かずにいると、動的なロードに失敗します。また、.asdファイルに書かずにいるとql:quickload時にASDFがロードしてくれず、実行時に探しに行って存在しなくエラーとなったりもします。

2. Dockerコンテナ上でビルド&実行

 プログラムを引き渡す相手がDocker使いだったり、異なるOS使いだったり、プログラム実行のためだけに環境を入れるのはなあ、という場合は、Dockerfileをつくってそれを渡してあげるとよいでしょう。

 Dockerコンテナのビルドは、OSのapt update && apt installからRoswellのビルド、Qlotのqlot install、そしてros buildまでを一発で実行してもよいのですが、毎度それらを実行していたらストレスで発狂しそうになります。

 なので、Dockerのマルチステージビルド

docs.docker.com

を用いてコンテナのビルド過程を細分化し、各過程をキャッシュしてしまいました(Dockerすごい)。多少ディスク容量を食いますが、ビルド時間が長いよりはだいぶマシです。

 これがDockerfileです。

#### This is a Dockerfile for portable building and executing Common Lisp program.
#### This requires some preliminalies to the program which built with this:
####
#### - The program shall be executed as a single executable file
#### - Entry point of the program is written as a roswell script
####     - More details, see [Roswell](https://github.com/roswell/roswell)
####
#### This Dockerfile can mainly used in two cases:
####
#### - To build the executable file
#### - To run the program by **not Common Lisp user**


### Base image for building
#
# If you want to build only once, you should atatch a tag individually like this:
# `$ sudo docker build --target niko-cl-base -t niko-cl-base .`
FROM ubuntu:18.04 as niko-cl-base

# Builder requires some dependent not-Common-Lisp library, because of `ql:quickload`.
RUN apt update && apt install -y libev-dev build-essential libcurl4-gnutls-dev autoconf git wget unzip
RUN wget https://github.com/roswell/roswell/archive/master.zip && unzip master.zip
RUN cd roswell-master && ./bootstrap && ./configure && make && \
    make install && ros setup && ros install qlot


### Execution environment
#
# If you want to build only once, do this:
# `$ sudo docker build --target niko-runner -t niko-runner .`
FROM ubuntu:18.04 as niko-runner

RUN apt update
RUN apt install -y libev-dev libcurl4-gnutls-dev autoconf git


### Dependency installed environment (to reduce build speed)
#
# If you want to build only once, do this:
# `$ sudo docker build --target niko-deps -t niko-deps .`
FROM niko-cl-base as niko-deps

ADD ./qlfile /app/
ADD ./qlfile.lock /app/
RUN cd /app && $HOME/.roswell/bin/qlot install


### Build environment
FROM niko-deps as niko-builder

ADD ./ /app/
RUN cd /app && $HOME/.roswell/bin/qlot exec ros build roswell/niko.ros


### Execution environment
#
# You can use this container to run the program or copy executable file built
FROM niko-runner

COPY --from=niko-builder /app/roswell/niko /usr/bin/niko
CMD [ "/usr/bin/niko", "version" ]
EXPOSE 5000

 FROMが現れるたびにビルドステージが変わります(イメージが分かれる)。前の段階のイメージは、一度つくってしまえば次はつくる必要がありません。Roswellのインストールなどがそうです。なので、そのようにしてみました。asの後の名前でタグをつけておけば、それが次回より利用されるようになります。タグ付けビルドをさっと実行できるよう、setup.shを用意してあります。

$ sudo ./setup.sh
$ sudo docker build .
0.9.0

 コンテナのビルド過程で、qlfile.asdに追加すべきだった隠れた依存ライブラリを発見できるという点で、Dockerコンテナ内でのクリーンなビルドはかなり意味があるなと感じました。

3. ビルド成果物を取り出し

 ところで、リリース成果物を作成するのに、このDockerコンテナ使えないかしら……。

 使えるんです。そう、Dockerならね。

 一度走らせたコンテナであれば、中からファイルを取り出したり、その逆にファイルを入れたりすることができます。なので、リリース環境とDocker環境のOSの種類やバージョンを合わせておけば、あらふしぎ、リリース成果物が自動でつくれちゃうんです。そしてそれを行うのがrelease.shです。

#!/bin/sh

VERSION=$( qlot exec ros run -e "(format t \"~a\" (slot-value (asdf:find-system :niko) 'asdf:version))" -q)
RELEASE_DIR="Niko_v$VERSION"

mkdir $RELEASE_DIR
cp ./README.md $RELEASE_DIR

sudo docker build -t niko .
sudo docker run -t niko:latest
sudo docker ps -af ancestor=niko:latest -q
sudo docker cp "$(sudo docker ps -af ancestor=niko:latest -q):/usr/bin/niko" $RELEASE_DIR
sudo docker ps -f ancestor=niko -q | xargs sudo docker rm

tar cf - $RELEASE_DIR | gzip > "$RELEASE_DIR.tar.gz"

ls $RELEASE_DIR/

rm -rf $RELEASE_DIR

 こいつを実行すると、3分だけ待ってやるだけで配布物を固めた.tar.gzができています。

 ここで注意する点としては、あたりまえですが実行環境とビルド環境の種類・バージョンを一致させることです。debian:stretchでビルドしたものをubuntu:18.04で実行したりなんかすると、たとえば「libssl.so.1.0.0がないよー」などといってプログラムが落ちます。CFFIがダイナミックリンクする対象を決めているのはそのシステムがロードされたとき(つまりビルド時)なので、ビルド時の環境と実行時の環境で.soのファイル名やバージョンが異なっているときなどにこの問題が発生します。

まとめ

 RoswellとQlotを用いて、Common LispプログラムをDockerでどっかーんとビルドした。

 Dockerfileがあると、Docker内でプログラムを実行させることによって、Common Lisp環境をつくることなしにプログラムを実行してもらうことができた。

 また、コンテナのビルド結果をキャッシュし、高速にビルドが完了するDockerfileを書くことができた。

 そして、コンテナでのCommon Lispプログラムのビルド成果物を用いて、さくっとデプロイ準備を完了するこができた。

 Dockerってすごいね。

Common Lispと時間とタイムゾーン

Common Lispと時間とタイムゾーン

この記事はLispアドベントカレンダーの4日目の記事です。

あらまし

 Common Lispで日時を扱う場合、ANSI仕様には日時のための関数がいくつか定められていますが極めて基本的なものしか存在しません。日時から文字列への変換とその逆、日付の比較や計算といったものは自前で実装する必要があります。が、そんなのいちいちやっていられない。そんなあなたのためのライブラリが日時ライブラリlocal-timeです。

Common Lisp標準仕様における日時

 まずは、Common Lispが日時についてどのような機能を提供しているか確認しましょう。この節の内容はCommon Lisp Hyperspecの時刻の章を参考資料として、要約する形で書いています。

 Common Lispの仕様では、日時のデータとして以下のものを提供しています:

  • Decoded time
  • Universal time
  • Internal time
  • Second

 各個を簡単に説明します。

Decoded time

 これは九つの値のリストです。日時の内部表現としては通常UNIX timeのように、ある基準日時からの秒数という形がとられることが多いと思いますが、それを人間が扱いやすい形にしたものです。リストには次の内容が含まれます:「秒」「分」「時」「日」「月」「年」「曜日」「日中」「サマータイムの影響下か否か」「タイムゾーンオフセット」。get-decoded-time関数で現在時刻を取得したり、後述のuniversal timeから変換して得ることができます。

Universal time

 これは整数値で、GMTにおける1900年1月1日の0:00からの秒数です。なので精度は秒です。Decoded timeと違うのは、日時の計算(三時間後とか前日零時など)や比較(この日時は今日か、など)が行いやすいことです。ただし、秒と所望の単位との変換にそれなりの労力を必要とします。

Internal time

 これも整数値ですが、こちらは秒以下の精度がほしいときに使うものです。計算機内部のタイマーや、可能ならばHPETを利用して値を取得するようですので、精度は環境に依存します。秒にいくつの「単位」が詰まっているか、要するに精度ですが、internal-time-units-per-secondで確認できます。

Second

 これは整数値で、秒を表します。sleep関数の引数となるものです。


 以上四つがCommon LispANSI仕様で公式に定義されている日時表現です。プリミティブすぎるので、実際に実用する際には抽象化を施す必要がありそうです。そして、その抽象化をしてくれるライブラリがあるのです。それが次の節で述べる、local-timeというライブラリです。

日時ライブラリ: local-time

 ANSI仕様で提供された日時関数群では、たとえば以下のようなことをするのにひと手間ふた手間かけなければなりません:

  • 日時を所望のフォーマットの文字列に変換する
  • 文字列で表現された日時を日時データに変換する
  • ある日時の三日後、二年前、といった日時を計算する
  • タイムゾーンAsia/Tokyoといった文字列で指定し、計算や比較のときに考慮する

 そんな手間を一挙に引き受けてくれるのが、ライブラリlocal-timeです。

 とりあえず現在時刻を取得しましょう。こんなふうにすると、時刻がlocal-time:timestampクラスのインスタンスが返ってきます:

CL-USER> (local-time:now)
@2018-12-02T12:28:33.527163+09:00

 よくある処理の例として、ある時点においてセッションが有効かどうかの判定を行うとします。セッションは4時間で無効になるとしましょうか。それはこんなふうな処理になります。

;; 4時間以内に作成されたセッションは有効
CL-USER> (defun session-available-p (session-created)
           (local-time:timestamp<= (local-time:timestamp- (local-time:now)
                                                          4 :hour)
                                   session-created))

SESSION-AVAILABLE-P

;; 有効なセッション
CL-USER> (defparameter session-created (local-time:now))
SESSION-CREATED
CL-USER> (session-available-p session-created)
T

;; 5時間前につくられたセッション
CL-USER> (defparameter unavailable-session
           ;; decoded-timeの各値を指定してtimestampを生成
           (local-time:encode-timestamp 0 0 0 0 2 12 2018))
UNAVAILABLE-SESSION
CL-USER> (session-available-p unavailable-session)
NIL

 時差についてはどのように扱えばよいのでしょうか。まず、local-timeはシステムのタイムゾーン設定を読みとって、その内容をlocal-time:*default-timezone*にセットしています。たとえばぼくのマシン(Asia/Tokyo)で見てみるとこんな感じです。

CL-USER> local-time:*default-timezone*
#<LOCAL-TIME::TIMEZONE LMT JDT JST JST>

 たとえばこれを外部サーバのRDBMSUS/Easternなので揃えたいという場合、以下のようにしてタイムゾーン情報をロードしたあとに、タイムゾーン情報を名前で指定して設定する、ということをします。

;; タイムゾーン情報を読み込み
CL-USER> (local-time:reread-timezone-repository)
; No value
CL-USER> (local-time:find-timezone-by-location-name "US/Eastern")
#<LOCAL-TIME::TIMEZONE EDT EST EWT EPT>
T

;; タイムゾーンをUS/Easternに設定(と前後の確認)
CL-USER> (local-time:now)
@2018-12-03T23:56:26.228703+09:00
CL-USER> (setf local-time:*default-timezone* (local-time:find-timezone-by-location-name "US/Eastern"))
#<LOCAL-TIME::TIMEZONE EDT EST EWT EPT>
CL-USER> (local-time:now)
@2018-12-03T09:56:31.047229-05:00

 ちなみにもし揃えたいタイムゾーンUTCの場合、定数local-time:+utc-zone+にはじめから設定してあるので、こちらを利用するとよいでしょう。

 どうでしょう。地味ながら、それなしではつくれないソフトウェアがぼろぼろありそうな、とても感謝なライブラリということがおわかりいただけたと思います。

まとめ

 この記事ではANSI Common Lisp仕様における日時の扱い方を解説し、より実践的で便利なライブラリであるlocal-timeを紹介しました。日時の扱いは間違いを生みやすく、そして時々刻々と変化するため再現の難しいバグを生みやすいです。基本的には自分で実装したりせず、信頼できるライブラリを用いましょう。





……ん? 記事はもう終わりですよ?





Common Lispと時間とタイムゾーン 〜 設定のブレに気をつけろ!

 ある日、日時を扱うソフトウェアをつくっていてハマったことがあったんです。

 それは、おしごとでのある案件でねぇ、そのときはlocal-timeの不具合だったように見えたんですよ。わたしはねぇ、「Lispアドベントカレンダーのネタにしよう」と思ってわくわくしていたんです。わくわくして記事を書いて、ふと、再現実験させてみちゃったりする。すると、再現しないんですよ。恐ろしくてね、こう、冷や汗がタラ〜って、流れてしまいましてねぇ。

——これは、そんな哀しみの物語。

発生現象

 そのとき書いていたソフトウェアはあるウェブアプリケーションのバックエンドプログラムでした。そのシステムではアクセス可能時間を制御しており、DBに記録される最終操作日時が特定の時間を過ぎるまで操作が行えないという処理が必要でした。処理系にはSBCLを選択し、RDBMSとしてMySQLを利用しておりました。各RDBMSのクライアントライブラリはlocal-timeい依存しておらず、したがってDBから取得される日時は素のCommon Lispで扱えるuniversal timeでやってきます。

 そこで、Universal timeを受けとり、現在操作が可能かを判定する述語として以下のようなready-pを用意してAPIのほうで呼び出していました。

;; プログラムの最初でタイムゾーンをUTCにしている、こんなコードで:
;; (setf local-time:*default-timezone* local-time:+utc-zone+)
;; 理由は`local-time:today`はUTCでの0:00を返すため
(defun ready-p (created-at)
  (or (null created-at)
      (let ((created-at (universal-to-timestamp created-at)))
        (timestamp<= created-at
                     (timestamp+ (local-time:today) 2 :hour))))))

 このプログラムのフロントエンドを担当していたプログラマMac使い(ぼくはUbuntu使い)で、Common Lispで実行可能バイナリを作成可能とはいえ、Ubuntuでつくったバイナリをそのまま渡しても動きません。なので、Docker上にUbuntu環境をつくってその上でビルドし、そのDockerfileを渡すようにしました(そのへんの話は12/7(金)のLispアドベントカレンダー記事(TODO:リンクを貼ること)に書きます)。

 そして、渡したDockerfileをビルドした彼が云って曰く、
「なんか日時の処理がおかしい…🤔」

 具体的には、システム時計が2018-12-04T17:00:00Zのときに上記のコードにcreated-at引数に2018-12-04T2:00:01Z相当のuniversal timeを与えてready-pを呼ぶと、nilが返ってくるのです(同日の2:00:012:00:00より後なので、tとなるのが正しい)。

 なんでじゃ。なんでなのじゃ…。

調査開始

 時刻の不具合といえばやっぱりタイムゾーンがあやしいです。なのでいろいろ話したり試したり調べたりしてみたところ、次のようなことがわかりました:

 タイムゾーンについてほかの影響要素はないかと調べると、POSIXシステムにおいてはTZ環境変数を通じてタイムゾーンをユーザが変更できるようでした(参考: GNU libcのマニュアル)。

 立てた仮説はこうです。
TZ環境変数の値によって、local-time:universal-to-timestampの返す値が異なるのではないか?」

 そこで、手元マシン、タイムゾーンUTC仮想マシンを作成し、こんな感じで検証してみました。

$ TZ='Asia/Tokyo' ros run -s local-time -e '(setf local-time:*default-timezone* local-time:+utc-zone+) (print (local-time:universal-to-timestamp 3800000000)) (terpri)(quit)'

@2020-06-01T11:33:20.000000Z
$ TZ='UTC' ros run -s local-time -e '(setf local-time:*default-timezone* local-time:+utc-zone+) (print (local-time:universal-to-timestamp 3800000000)) (terpri)(quit)'

@2020-06-01T11:33:20.000000Z

 お、おぉ……? 同じ日時になっとるやんけ。あのおしごとでの解決方法はなんだったんだ…? その場では上記仮説に従い、

(defun universal-to-timestamp* (ut)
  (multiple-value-bind (sec min hour date month year)
      (decode-universal-time ut)
    (encode-timestamp 0 sec min hour date month year)))

というコードを追加して、UTCに直す、ということをやったのですが、それは間違っていたということなの…?

追加調査

 結論からいえば、間違っていました。正しい原因はこうです。
mysqldCommon Lispのプログラムとの間でタイムゾーンの設定が異なると、cl-mysqlがtimestamp (SQLの型)をuniversal timeに変換するときに時間がずれる」

 家で上記の確認をしても、一向に現象が再現できないため、新たな方向で調べはじめました。確認のために書いたコードはこれです。タイムゾーンUTCに設定されたVMを立て、その中で、データベースの作成後にまずcraete-tableinsert-rowを流し、その後tztestテーブルの中身を、TZ環境変数の値を変えつつ見てみます。

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp(ql:quickload '(:uiop :cl-dbi :dbd-mysql :sxql :local-time) :silent t)
  )

(defpackage :ros.script.tztest.3752729426
  (:use :cl))
(in-package :ros.script.tztest.3752729426)

(defmacro with-db ((var) &body body)
  `(cl-dbi:with-connection (,var :mysql :database-name "tztest"
                                 :host "localhost"
                                 :username "root"
                                 :password "root")
     (cl-mysql-system::set-character-set "utf8mb4")
     ,@body))

(defun execute-sql (conn sxql)
  (multiple-value-bind (query params)
      (sxql:yield sxql)
    (apply #'dbi:execute (dbi:prepare conn query) params)))

(defun create-table ()
  (with-db (conn)
    (let ((query (sxql:create-table :tztest
                     ((id :type :bigint :not-null t
                          :auto-increment t :primary-key t)
                      (ts :type :timestamp
                          :default (local-time:parse-timestring "2000-01-01"))))))
      (execute-sql conn query))))

(defun insert-row (ts-str)
  (with-db (conn)
    (let ((query (sxql:insert-into :tztest
                   (sxql:set= :ts ts-str))))
      (execute-sql conn query))))

(defun select-rows ()
  (with-db (conn)
    (let ((query (sxql:select (:id :ts)
                   (sxql:from :tztest))))
      (dbi:fetch-all (execute-sql conn query)))))

(defun print-db-timestamp (row)
  (format t "~a: ~a~%" (getf row :|id|)
          (local-time:universal-to-timestamp (getf row :|ts|))))

(defun main (&rest argv)
  (declare (ignorable argv))
  ;; (create-table)
  ;; (insert-row "2018-12-02 04:00:00")
  (let ((rows (select-rows)))
    (format t "***** TZ: ~a, *dt*: ~a~%"
            (uiop:getenv "TZ") local-time:*default-timezone*)
    (mapc #'print-db-timestamp rows)
    (terpri)
    (setf local-time:*default-timezone* local-time:+utc-zone+)
    (format t "***** TZ: ~a, *dt*: ~a~%"
            (uiop:getenv "TZ") local-time:*default-timezone*)
    (mapc #'print-db-timestamp rows)
    (terpri)))
;;; vim: set ft=lisp lisp:

 mysqldTZUTCの状態で固定し、roswellスクリプトのほうはTZの値を変えて実行した結果がこちらです。

$ TZ=Asia/Tokyo sudo ./tztest.ros
***** TZ: Asia/Tokyo, *dt*: #<TIMEZONE LMT BST GMT BDST BST BST GMT GMT>
1: 2018-12-01T19:00:00.000000Z

***** TZ: Asia/Tokyo, *dt*: #<TIMEZONE UTC>
1: 2018-12-01T19:00:00.000000Z

$ TZ=UTC sudo ./tztest.ros
***** TZ: UTC, *dt*: #<TIMEZONE LMT BST GMT BDST BST BST GMT GMT>
1: 2018-12-02T04:00:00.000000Z

***** TZ: UTC, *dt*: #<TIMEZONE UTC>
1: 2018-12-02T04:00:00.000000Z

 見事、cl-mysqlの返す日時にちょうど9時間、UTCJSTの間の時間分の差が生まれています。

オチ

 複数のプログラムが協調して動くシステムを構築するとき、とくに日時や時刻を扱う場合には、走らせるソフトウェアの間でタイムゾーンの設定が同じになるように注意しましょう。