Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

openSuSE Tumbleweedでlemを動かす(ncursesトラブル)

lemを動かすのにちょっと苦労したのでメモ.

TL;DR

  • ros install cxxxr/lem
  • vi ~/.roswell/lisp/quicklisp/dists/quicklisp/software/cl-charms-****/src/low-level/curses-bindings.lisp
    • "libncursesw.so.6"を読み込んでいる箇所をコメントアウトし,libncursesw.so.5が読み込まれるようにする
  • lem

lemとは

lemとは,Common Lispで書かれたEmacsライクなエディタである.

github.com

Common Lispで設定を記述することができ(拡張を書くことができ),内部でCommon Lisp処理系を起動したり,外部で起動したCommon Lisp処理系にSWANKプロトコルで接続することもできる,Common Lispとの相互運用を意識した高機能なエディタである. また,よく使う機能(pareditやシンタックスハイライト,auto-complete)は最初から移植されており,いきなり使えるようになっている.

利用開始時にコードや拡張がコンパイルされるため,かなり高速に起動するのが特徴である.

lemが動かない

自分もこれをインストールしようと思っていたが,ncursesまわりのトラブルで起動すらできなかった.以下のようなエラーだ.

% ros install lem
% lem
Unhandled SIMPLE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING{10005305B3}>:
Trying to access undefined foreign variable "COLORS".
0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002C84F93}> #<unused argument> :QUIT T)
1: (SB-DEBUG::RUN-HOOK SB-EXT:*INVOKE-DEBUGGER-HOOK* #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002C84F93}>)
2: (INVOKE-DEBUGGER #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002C84F93}>)
3: (ERROR "Trying to access undefined foreign variable ~S." "COLORS")
4: (CFFI::FS-POINTER-OR-LOSE "COLORS" CL-CHARMS/LOW-LEVEL::LIBCURSES)
5: (CL-CHARMS/LOW-LEVEL::%VAR-ACCESSOR-*COLORS*)
6: (LEM.TERM:TERM-INIT)
7: ((:METHOD LEM-INTERFACE:INVOKE (LEM-NCURSES::NCURSES T)) #<unused argument> #<CLOSURE (LAMBDA (&OPTIONAL LEM::INITIALIZE LEM::FINALIZE) :IN LEM:LEM) {1002C8446B}>) [fast-method]
8: (SB-INT:SIMPLE-EVAL-IN-LEXENV (APPLY (QUOTE MAIN) ROSWELL:*ARGV*) #<NULL-LEXENV>)
9: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) #<NULL-LEXENV>)
10: (SB-EXT:EVAL-TLF (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) NIL NIL)
11: ((LABELS SB-FASL::EVAL-FORM :IN SB-INT:LOAD-AS-SOURCE) (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) NIL)
12: (SB-INT:LOAD-AS-SOURCE #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> :VERBOSE NIL :PRINT NIL :CONTEXT "loading")
13: ((FLET SB-FASL::THUNK :IN LOAD))
14: (SB-FASL::CALL-WITH-LOAD-BINDINGS #<CLOSURE (FLET SB-FASL::THUNK :IN LOAD) {7F757CD5753B}> #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}>)
15: ((FLET SB-FASL::LOAD-STREAM :IN LOAD) #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> NIL)
16: (LOAD #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> :VERBOSE NIL :PRINT NIL :IF-DOES-NOT-EXIST T :EXTERNAL-FORMAT :DEFAULT)
17: ((FLET ROSWELL::BODY :IN ROSWELL:SCRIPT) #<SB-SYS:FD-STREAM for "file /home/windymelt/.roswell/bin/lem-ncurses" {1002BB1AE3}>)
18: (ROSWELL:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses")
19: (ROSWELL:RUN ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))
20: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROSWELL:RUN (QUOTE ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))) #<NULL-LEXENV>)
21: (EVAL (ROSWELL:RUN (QUOTE ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))))
22: (SB-IMPL::PROCESS-EVAL/LOAD-OPTIONS ((:EVAL . "(progn #-ros.init(cl:load \"/usr/local/etc/roswell/init.lisp\"))") (:EVAL . "(ros:run '((:eval\"(ros:quicklisp)\")(:script \"/home/windymelt/.roswell/bin/lem-ncurses\")(:quit ())))")))
23: (SB-IMPL::TOPLEVEL-INIT)
24: ((FLET SB-UNIX::BODY :IN SB-EXT:SAVE-LISP-AND-DIE))
25: ((FLET "WITHOUT-INTERRUPTS-BODY-27" :IN SB-EXT:SAVE-LISP-AND-DIE))
26: ((LABELS SB-IMPL::RESTART-LISP :IN SB-EXT:SAVE-LISP-AND-DIE))
unhandled condition in --disable-debugger mode, quitting
Unhandled SIMPLE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING
                                    {10005305B3}>:
  Trying to access undefined foreign variable "stdscr".
Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {10005305B3}>
0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002CB4D53}> #<unused argument> :QUIT T)
1: (SB-DEBUG::RUN-HOOK SB-EXT:*INVOKE-DEBUGGER-HOOK* #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002CB4D53}>)
2: (INVOKE-DEBUGGER #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002CB4D53}>)
3: (ERROR "Trying to access undefined foreign variable ~S." "stdscr")
4: (CFFI::FS-POINTER-OR-LOSE "stdscr" CL-CHARMS/LOW-LEVEL::LIBCURSES)
5: (CL-CHARMS/LOW-LEVEL::%VAR-ACCESSOR-*STDSCR*)
6: (LEM.TERM:TERM-FINALIZE)
7: ((FLET "CLEANUP-FUN-2" :IN LEM-INTERFACE:INVOKE)) [cleanup]
8: ((:METHOD LEM-INTERFACE:INVOKE (LEM-NCURSES::NCURSES T)) #<unused argument> #<CLOSURE (LAMBDA (&OPTIONAL LEM::INITIALIZE LEM::FINALIZE) :IN LEM:LEM) {1002C8446B}>) [fast-method]
9: (SB-INT:SIMPLE-EVAL-IN-LEXENV (APPLY (QUOTE MAIN) ROSWELL:*ARGV*) #<NULL-LEXENV>)
10: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) #<NULL-LEXENV>)
11: (SB-EXT:EVAL-TLF (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) NIL NIL)
12: ((LABELS SB-FASL::EVAL-FORM :IN SB-INT:LOAD-AS-SOURCE) (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) NIL)
13: (SB-INT:LOAD-AS-SOURCE #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> :VERBOSE NIL :PRINT NIL :CONTEXT "loading")
14: ((FLET SB-FASL::THUNK :IN LOAD))
15: (SB-FASL::CALL-WITH-LOAD-BINDINGS #<CLOSURE (FLET SB-FASL::THUNK :IN LOAD) {7F757CD5753B}> #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}>)
16: ((FLET SB-FASL::LOAD-STREAM :IN LOAD) #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> NIL)
17: (LOAD #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> :VERBOSE NIL :PRINT NIL :IF-DOES-NOT-EXIST T :EXTERNAL-FORMAT :DEFAULT)
18: ((FLET ROSWELL::BODY :IN ROSWELL:SCRIPT) #<SB-SYS:FD-STREAM for "file /home/windymelt/.roswell/bin/lem-ncurses" {1002BB1AE3}>)
19: (ROSWELL:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses")
20: (ROSWELL:RUN ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))
21: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROSWELL:RUN (QUOTE ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))) #<NULL-LEXENV>)
22: (EVAL (ROSWELL:RUN (QUOTE ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))))
23: (SB-IMPL::PROCESS-EVAL/LOAD-OPTIONS ((:EVAL . "(progn #-ros.init(cl:load \"/usr/local/etc/roswell/init.lisp\"))") (:EVAL . "(ros:run '((:eval\"(ros:quicklisp)\")(:script \"/home/windymelt/.roswell/bin/lem-ncurses\")(:quit ())))")))
24: (SB-IMPL::TOPLEVEL-INIT)
25: ((FLET SB-UNIX::BODY :IN SB-EXT:SAVE-LISP-AND-DIE))
26: ((FLET "WITHOUT-INTERRUPTS-BODY-27" :IN SB-EXT:SAVE-LISP-AND-DIE))
27: ((LABELS SB-IMPL::RESTART-LISP :IN SB-EXT:SAVE-LISP-AND-DIE))
unhandled condition in --disable-debugger mode, quitting

まとめると,COLORSstdscrという変数にアクセスできなかったようだ.

lemは,複数の画面描画手段を使うことができるが,そのうちの標準的な手段がncursesである.これを使って画面制御を行うのだが,ncursesはC言語ライブラリなので直接Common Lispから呼び出すことができない. Common LispからC言語ライブラリを呼び出すといった,言語感のプログラムの呼び出しにはFFI(Foreign Function Interface)という仕組みを使う. またCommon Lisp上で動作するFFI実装として,CFFIという著名なモジュールがあり,lemでもこのモジュールを経由してncurses等の外部ライブラリを呼び出している. 今回のエラーも,CFFIによってncurses上で定義されているはずの変数を呼び出そうとしたが,該当する変数が存在しなかった,というエラーである.

どこが問題だったのか

実は不明である.

これはncursesの互換性の問題らしい.らしいというのは,リリースノートを見たが具体的な変更がどこかよく分からなかったからだ.

lem側の問題なのか,どちらかといえばncurses側の問題なのかを切り分けるのに時間がかかってしまったが,後述するcl-charmsのexampleを走らせたところエラーが生じたことから,ncurses(のラッパ)で問題が生じていることがわかった.

解決策

ncurses5を利用する.

これは全く天下り的な方法で発見した.つまり適当に読み込むライブラリのバージョンを5にしたら動いたというだけの話で,どこが問題なのかはよくわからなかった. ともかく,ncurses5では動作したので,これを使えばいい.

ところでopenSuSE Tumbleweedはncurses6を標準的に採用しているので,ncurses5だけ使うわけにはいかない.今回は,lemが呼び出しているライブラリをncurses5に制限するという方法で問題を回避する.

cl-charms

lemは,FFIを介して直接ncursesを使うのではなく,cl-charmsというncursesのラッパーライブラリを使うことで画面操作を行う. このcl-charmsはコード中にCFFIncursesを読み込む箇所がある.これを以下に示す.

#+unicode
(cffi:define-foreign-library libcurses
  (:darwin (:or "libncurses.dylib" "libcurses.dylib"))
  (:unix (:or "libncursesw.so.6"
              "libncursesw.so.5"
              "libncursesw.so.14.0"
              "libncursesw.so"))
  (:windows (:or "pdcurses" "libcurses"))
  (t (:default "libcurses")))

#-unicode
(cffi:define-foreign-library libcurses
  (:darwin (:or "libncurses.dylib"
                "libcurses.dylib"))
  (:unix (:or "libncursesw.so.6"        ; XXX: is this the right thing
                                        ; to load? Should we also add
                                        ; libncursesw.so as a
                                        ; fallback?
              "libncurses.so.6"
              "libncurses.so.5"
              "libncursesw.so.14.0"
              "libncurses.so"
              "libcurses"))
  (:windows (:or "pdcurses"
                 "libcurses"))
  (t (:default "libcurses")))

これを見ればわかるとおり,cl-charmsはまずOSを判別し,その後でncursesを読み込んでいく.UNIXの場合その優先順位は,

  1. libncursesw.so.5
  2. libncursesw.so.6
  3. libncursesw.so.14.0
  4. libncursesw.so

である.これらのうちどれかが発見されれば,それが読み込まれる.

ちなみに#+unicode#-unicodeとしている箇所は,処理系のUnicode対応によって分岐している箇所である*1.Unicode対応であればlibncurseswが使われるが,そうでない場合はlibncursesが使われるようだ(なぜかlibncurseswを読み込む箇所もあるが).

また,roswellでlemをインストールする場合は,cl-charms~/.roswell/lisp/quicklisp/dists/quicklisp/software/cl-charms-****/src/low-level/curses-bindings.lispに保存されているので,これを書き換えればよい.

このファイルが存在しない場合は,ros runして(ql:quickload :cl-charms)などとしてダウンロードさせるとよい.

libncurses6を読み込まないようにする

さて,ncurses6を読み込まないようにする.手順は至って簡単で,libncursesw.so.6libncurses.so.6となっている箇所をすべてコメントアウトすればよい.実施後を以下に示す.

#+unicode
(cffi:define-foreign-library libcurses
  (:darwin (:or "libncurses.dylib" "libcurses.dylib"))
  (:unix (:or ;"libncursesw.so.6"       ; ここ
              "libncursesw.so.5"
              "libncursesw.so.14.0"
              "libncursesw.so"))
  (:windows (:or "pdcurses" "libcurses"))
  (t (:default "libcurses")))

#-unicode
(cffi:define-foreign-library libcurses
  (:darwin (:or "libncurses.dylib"
                "libcurses.dylib"))
  (:unix (:or ;"libncursesw.so.6"       ; ここ ; XXX: is this the right thing
                                        ; to load? Should we also add
                                        ; libncursesw.so as a
                                        ; fallback?
              ;"libncurses.so.6"        ; ここ
              "libncurses.so.5"
              "libncursesw.so.14.0"
              "libncurses.so"
              "libcurses"))
  (:windows (:or "pdcurses"
                 "libcurses"))
  (t (:default "libcurses")))

3点コメントアウトした.これでncurses6cl-charmsからは読み込まれなくなった.

後は,通常通りlemと実行するだけでlemが起動する.

うまくいかないときは,ros install lemとすることでダンプされたバイナリが削除され,再度lemがビルドできるようになる.

まとめ

ncurses6が足枷になっているとは思いもよらなかったが,あまりncurses6は人気がないのだろうか. もとよりcl-charmsは原作者がメンテナンスを放棄したものを有志がメンテしている状況なので,あまり開発が活発でないという側面があることも付け加えておきたい.

便利な資料

blog.8arrow.org

qiita.com

qiita.com

*1:Feature expression, 処理系が対応している機能によって,式読み取り時に式を分岐させるための記述CLHS: Section 24.1.2.1

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?