Docker上のEmacsのビルドでハマった話

Docker上でEmacsをビルドしようとしてハマったので対処方法と、ついでに簡単に調査したメモです。

現象

環境は次のような感じでした。

  • ホスト: CentOS 7.2 (Conoha VPSのデフォルトイメージ利用)
  • Docker 1.10.3

まず、確認用に次のDockerfileを用意します。なお、centos:7としても現象は同じでした(ただし、yumのインストール対象にmakeを追加する必要があります)。

FROM centos:6

ARG emacs=emacs-24.5
RUN yum install -y gcc lcurses-devel wget ncurses-devel

ARG work_dir=/tmp/setup
RUN mkdir ${work_dir}

RUN cd ${work_dir} && \
    wget http://mirror.jre655.com/GNU/emacs/${emacs}.tar.gz && \
    tar zxf ${emacs}.tar.gz && \
    cd ${emacs} && \
    ./configure --without-x && \
    make

これをビルドしてみると・・・

$ docker build --no-cache -t test-emacs-build .
~略~
Finding pointers to doc strings...
Finding pointers to doc strings...done
Dumping under the name emacs
**************************************************
Warning: Your system has a gap between BSS and the
heap (22442648 bytes).  This usually means that exec-shield
or something similar is in effect.  The dump may
fail because of this.  See the section about
exec-shield in etc/PROBLEMS for more information.
**************************************************
/bin/sh: line 7:  5952 Segmentation fault      (core dumped) ./temacs --batch --load loadup bootstrap
make[1]: *** [bootstrap-emacs] Error 1
make[1]: Leaving directory `/tmp/setup/emacs-24.5/src'
make: *** [src] Error 2

makeで死にます。

対処方法その1

とりあえず、メッセージに従ってetc/PROBLEMS(テキストファイル)でexec-shieldを検索してみます。見てみると、Linuxのセキュリティ機構であるExec-shield(プロセスのメモリ配置のランダム化?)が問題のようです。

解決方法だけ抜粋すると、下記の通りです。

    echo 0 > /proc/sys/kernel/exec-shield
    echo 0 > /proc/sys/kernel/randomize_va_space

ということで以下のようにして、「ホスト側で」一時的にrandomize_va_spaceを無効化してからdocker buildすればOKです(exec-shieldの方は無関係でした)。

$ cat /proc/sys/kernel/randomize_va_space
2
$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
$ docker build --no-cache -t test-emacs-build .

emacsのビルド部分が通った後はrandomize_va_spaceを元に戻して問題ありませんでした。といっても、Emacsのビルド部分を通るたびにこれが必要なのは何かと不便そうです。

少し調査

randomize_va_spaceというキーワードが手に入ったのでGoogle先生に聞いてみると、次のissueが引っ掛かりました。

Ubuntu 16.04: Dockerfile cannot build emacs · Issue #22801 · docker/docker · GitHub

これはDocker側のissueですが、ここからEmacs側のスレッド(同じ人?)にもリンクが張られています。

https://debbugs.gnu.org/cgi/bugreport.cgi?bug=23529

で、斜め読みしかしていないのですが、Emacsのスレッドで示されている下記コードのpersonalityというsyscallが問題になっているようです。これで、一時的にrandomize_va_spaceに0をセットするのに相当する操作をしているようですが、これがDockerのゲスト上ではうまく働かない(ホスト側を変更する必要があるのにそれができない)ということだと思います。

https://github.com/emacs-mirror/emacs/blob/master/src/emacs.c#L802-819

結局のところ、Docker側がpersonalityが動作するようなオプションを入れるかEmacs側がこの動作を修正するかですが、前者はDockerのポリシー的におそらく難しく、後者は望ましいが技術的に難しいという話になっているようです(斜め読みなので違ったらすみません)。

対処方法その2

最新版の利用や細かいバージョンの指定ができないのは残念ですが、この辺りの融通が効くのであれば大人しくRPM使っとけというのが多くの場合正しいでしょう。

個人的には24系が入ればそれで良かったので、CentOS6系であれば、「centos6.5にemacs24.5をrpmからインストールする | joppot」を参考に以下のようにすればOKでした。なお、CentOS7系であれば単にyum installするだけで24系のEmacsが入るはずです。

FROM centos:6

RUN yum install -y gcc lcurses-devel wget ncurses-devel gpm-libs alsa-lib perl gnutls-devel

RUN cd /etc/yum.repos.d && \
    wget https://gist.githubusercontent.com/AaronTheApe/5540012/raw/5782a8d6a95f76daeed6073dc0c90612fefe2fb3/emacs.repo && \
    yum --disablerepo="*" --enablerepo="emacs" --nogpgcheck -y install emacs-nox

追記(2016/08/11)

今現在は/proc/sys/kernel/randomize_va_spaceが2でも、上記の問題は起きず問題なくビルドできています。

Emacsの描画崩れの問題でDocker側を1.11にアップデートしたのですが(参考:「Dockerを1.10から1.11へアップデート on CentOS7 - eshamster’s diary」)、その影響でしょうか?ただ、上記のissueの時点では1.11.1や当時のmaster(1.12)でも再現しているようですが…。続報もないので(また、改めて1.10に戻して試す気もないので)結局良く分からないです。

一応バージョンはissueの時点よりも少し進んで、1.11.2だという差はあります。

$ docker --version
Docker version 1.11.2-cs3, build c81a77d

JavaScriptのモジュール定義構文をParenscriptで抽象化(マクロで遊ぶ)

前書き

JavaScriptを書いていて「ここでマクロがあれば…」と思う事案があったので、マクロ欲を満たすためのエントリです。

JavaScriptでのモジュール定義

JavaScriptcounterモジュールを作ってみます。

var counter = (function() {
  var count = 0;
  var add = function(x) { // こんな感じでprivateな関数も書けます、というためだけに用意した関数
    count += x;
    return count;
  };
  
  return {
    get: function() { return count; },
    increment: function(x) { return add(x); },
    decrement: function(x) { return add(-x); }
  };
}());

// 使い方
counter.get();         // -> 0
counter.increment(10); // -> 10
counter.decrement(3);  // -> 7
counter.get();         // -> 7

モジュール定義用の構文という訳でもないですが、下記のような基本的な要素の組み合わせでクラスっぽい機能が実現できています。暗記アレルギーな人間としては、こういう考えれば辿れる系のものは覚えやすくて好きです。

  • クロージャ
  • ハッシュの要素にはドットでアクセス可能
  • 変数を通した関数アクセスに(Common Lispのfuncallのような)特別な処理は不要

とはいえ、何度も書いているとやはり面倒です。ここでマクロがあれば…ということで、Common Lisp(のサブセット)をJavsScriptに変換するライブラリParenscriptを使ってCommon Lispで書きなおしてみます。

Parenscriptで書き直す

準備:JavaScriptと(ほぼ)1対1の書き方へ

Parenscriptで上記のJavaScriptと1対1に対応するコードを書くには少し準備が必要です。

Parenscriptではなぜかhash-tableがサポートされていないので、とりあえず必要なサブセットだけサポートします*1

(defpsmacro make-hash-table ()
  `(@ {}))

(defpsmacro gethash (key hash-table)
  `(aref ,hash-table ,key))

まずはこれを直接使って書いてみます。

(defun make-js-module-1 ()
  (ps (defvar counter
        (funcall (lambda ()
                   (let* ((count 0)
                          (add (lambda (x)
                                 (incf count x)))
                          (public-body (make-hash-table)))
                     (setf (gethash :get public-body)
                           (lambda () count)
                           (gethash :increment public-body)
                           (lambda (x) (add x))
                           (gethash :decrement public-body)
                           (lambda (x) (add (* x -1))))
                     public-body))))))

少し脇道ですが、make-js-module-1関数を呼び出すと次のようなJavaScriptコードが得られます。

var counter = (function () {
    var count = 0;
    var add = function (x) {
        return count += x;
    };
    var publicBody = {  };
    publicBody['get'] = function () {
        return count;
    };
    publicBody['increment'] = function (x) {
        return add(x);
    };
    publicBody['decrement'] = function (x) {
        return add(x * -1);
    };
    return publicBody;
})();

さて、純JSなコードに比べると、make-js-module-1では一時変数public-bodyを利用していたりと、ハッシュの扱いが不格好です。Common Lispにハッシュの初期化構文に相当するものがないためですが、なければ作ればよいですね。

(defmacro+ps init-hash-table (&rest pairs)
  (let ((hash (gensym)))
    `(let ((,hash (make-hash-table)))
       ,(cons 'setf (mapcan (lambda (pair)
                              `((gethash ,(car pair) ,hash)
                                ,(cadr pair)))
                            pairs))
       ,hash)))

これを使ってmake-js-module-1を書き直すと次のようになります。

(defun make-js-module-2 ()
  (ps (defvar counter
        (funcall (lambda ()
                   (let* ((count 0)
                          (add (lambda (x)
                                 (incf count x))))
                     (init-hash-table
                      (:get (lambda () count))
                      (:increment (lambda (x) (add x)))
                      (:decrement (lambda (x) (add (* x -1)))))))))))

これで純JSのコードと大体1対1の対応になったので、ようやくスタートラインです。

マクロでイディオムを隠蔽する

まずは、頭のfuncalllambdaが鬱陶しいのでdefmoduleマクロで隠してみます。

;; make-js-moduleと番号を合わせるため1, 2は欠番
(defmacro+ps defmodule-3 (name &body body)
  `(defvar ,name
     (funcall (lambda ()
                ,@body))))

(defun make-js-module-3 ()
  (ps (defmodule-3 counter
        (let* ((count 0)
               (add (lambda (x)
                      (incf count x))))
          (init-hash-table
           (:get (lambda () count))
           (:increment (lambda (x) (add x)))
           (:decrement (lambda (x) (add (* x -1)))))))))

これだけだと、むしろ分かりにくくなっています。funcalllambdaが消えたことで、let*init-hash-tableの意味合いが不明瞭になったためです。

ということで、次の「ルール」を導入することで、この2つを隠します。

  • モジュール名の次にはプライベートな名前・値のペアをリストで渡す
  • 以降はパブリック名前・値のペアを並べる
(defmacro+ps defmodule-4 (name private-vars &body body)
  `(defvar ,name
     (funcall (lambda ()
                (let* ,private-vars
                  (init-hash-table ,@body))))))

(defun make-js-module-4 ()
  (ps (defmodule-4 counter
        ((count 0)
         (add (lambda (x)
                (incf count x))))
        (:get (lambda () count))
        (:increment (lambda (x) (add x)))
        (:decrement (lambda (x) (add (* x -1)))))))

だいぶすっきりしました。

さらに、パブリックな値の定義部分にあるlambdaを省略します。ただし、定数を直接公開するような使い方ができないという制限がつきます*2

(defmacro+ps defmodule (name private-vars &body body)
  `(defvar ,name
     (funcall (lambda ()
                (let* ,private-vars
                  (init-hash-table
                   ,@(mapcar (lambda (method-def)
                               `(,(car method-def) (lambda ,@(cdr method-def))))
                             body)))))))

(defun make-js-module ()
  (ps (defmodule counter
        ((count 0)
         (add (lambda (x)
                (incf count x))))
        (:get () count)
        (:increment (x) (add x))
        (:decrement (x) (add (* x -1))))))

そんな訳でBefore, Afterです。いくつかの「ルール」や制限*3の導入と引き換えに、モジュール作成に本質的には無関係なキーワードがきれいサッパリなくなりました。

1対1のコードから、たった8行でこれを実現できるLispのマクロは実に強力で気分が良いです。

// Before
var counter = (function() {
  var count = 0;
  var add = function(x) {
    count += x;
    return count;
  };
  
  return {
    get: function() { return count; },
    increment: function(x) { return add(x); },
    decrement: function(x) { return add(-x); }
  };
}());
;; After
(defmodule counter
  ((count 0)
   (add (lambda (x)
          (incf count x))))
  (:get () count)
  (:increment (x) (add x))
  (:decrement (x) (add (* x -1))))

マクロの功罪

今回の狭い範囲から見えるマクロの功罪は次のような感じでしょうか。

    1. 構文を簡単に抽象化できる
    2. マクロ名で元になった構文の意図を明確にできる
    1. 構文を簡単に抽象化できすぎる
    2. 知らなければならないルールが増える
    3. 意図的かどうかを問わず、何らかの制限がつく

何か書こうかと思っていましたが、こう並べてみると抽象化一般の功罪と変わらないですね。プログラムの中でもより基盤に近い部分を触るので、影響がより際立つ感じでしょうか。

コード貼り付け

最後に、ここまでを一通りまとめたRoswellスクリプトです。

*1:オプションが足りないのは見て通りですが、他に問題として 'hoge のようなquoteされたシンボルをキーにできないという問題があります。対応するには(quote hoge)のようなリストが来た場合に、hogeの部分を取り出すようにする必要があります。

*2:atomが来たらそのまま返すようにマクロを拡張することで対応は可能です

*3:ルールを追加することで制限を緩和することは可能で、トレードオフの関係です。

[JavaScript] ブラウザからSuperAgentでファイルをPOST

ブラウザからSuperAgentでファイルをPOSTしようとしてハマったのでメモ。なお、SuperAgentAJAX通信に特化した軽量なJavaScriptライブラリです。

SuperAgent紹介記事リンク:jQuery.ajaxの代わりにSuperAgentを使う - Qiita

ハマった部分

まず、HTMLでformのsubmitを使ってファイルを送る場合は次のようになります。enctype="multipart/form-data"が唯一ポイントで、後はtype=fileなinputを利用するだけです。

<form name="main_form" action="/some-url" enctype="multipart/form-data" method="POST">
  <input name="submit_file" type="file">
  <input type="submit" value="ファイル送信">
</form>

もう少し柔軟に制御したかったのでSuperAgentで自分でAJAX通信をしようと思いました。そこでドキュメントを見ると、ファイルを送るには.attach(name, [path], [filename])関数を使うように書いてあります。

// SuperAgentのドキュメントから引用
 request
   .post('/upload')
   .field('user[name]', 'Tobi')
   .field('user[email]', 'tobi@learnboost.com')
   .attach('image', 'path/to/tobi.png')
   .end(callback);

第2引数のpathって何さと思い、フルパスを渡してみたりと色々的はずれな試行錯誤をしていました。しかし、どうやらこの記述はNode.jsすなわちサーバ側で利用する場合の説明であって、ブラウザ側ではまた違うものを渡す必要があるようです。

そしてそのブラウザ側での話は書かれていない(はず)…。

結論

結局どうするかですが、Fileオブジェクトを渡せば良いようです。冒頭のformを例に取ると、Fileオブジェクトは下記のように取り出せます。

document.main_form.submit_file.files[0]

実際、SuperAgentのソースで.attach()関数のコメントを見るとFileオブジェクトかBlobオブジェクトを渡せと書いてあります。

引用元:https://cdnjs.cloudflare.com/ajax/libs/superagent/1.2.0/superagent.js

/**
 * Queue the given `file` as an attachment to the specified `field`,
 * with optional `filename`.
 *
 * ``` js
 * request.post('/upload')
 *   .attach(new Blob(['<a id="a"><b id="b">hey!</b></a>'], { type: "text/html"}))
 *   .end(callback);
 * ```
 *
 * @param {String} field
 * @param {Blob|File} file
 * @param {String} filename
 * @return {Request} for chaining
 * @api public
 */

…なんとなく尻切れ感がありますが、他に書きたいこともないので終わり。

[Common Lisp] システム内のパッケージ間の関係をグラフ化

システム内に存在するパッケージ間の参照関係をgraphvizでグラフ化するRoswellスクリプトmake-package-tree.rosを書いてみました。リファクタリングに使える…かもしれません。

github.com

前説

これを作ったきっかけの話です。

Common Lispを始めた頃に、とりあえず練習用でオセロのプログラムを書いていました。単純なミニマックス探索(αβ法)と単純なモンテカルロ木探索(UCT)*1を備えていて、簡単なCUIインタフェースもつけてます。

そうして右も左も分からない中で書いたのがothello-clです*2。パッケージを分けたり、SBCL, CCLの両対応にしたりと、これをもう少し整えたものが下のcl-othelloです。

github.com

このとき、とりあえずテストを通すこと優先で各パッケージでひたすらexportしていたのですが、だいぶ余計なものをexportしている気がしました。そんなわけで、リファクタリングついでにグラフ化して見てみよう、というのが今回のスクリプトを作った動機です。

使用感

使い方

引数なしでヘルプが見れます。こうしたヘルプの生成兼コマンドライン引数の処理にはCL-CLIを利用しています。

$ ./make-package-tree.ros
./make-package-tree.ros [OPTION]... SYSTEM-NAME

 [ OPTIONS ]

Global options:
  -P,--only-package                Show only packages (doesn't show symbols)
  -o,--output        <file>        Place the output into <file> (default: 
                                   temp.png)
  -e,--exclude       <package names> Exclude packages from graph (if you 
                                   exclude multiple packages, write them 
                                   separating by space)
  • システム名には(ql:quickload ...)でロードできるシステムの名前(小文字可)を入れます
  • アウトプット名は特に解析していないので、拡張子に関わらずPNGしか出ません
  • excludeの複数指定は"cl-othello cl-othello.utils"のような感じです

次に適用結果の一部を拡大して見方を説明します。

f:id:eshamster:20160321034045p:plain

  • 四角いボックスはパッケージ
    • 内部の楕円はエクスポートしているシンボル
      • 入ってくる矢印は他パッケージからの参照
        • ただし、use-pacakgeされている場合は省略
    • 小さい円はパッケージ自身*3
      • 入ってくる矢印はuse-packageされていることを示す(上の例にはないですが…)
      • 出て行く矢印はシンボルをimportもしくはパッケージをuse-packageしていることを示す

cl-othelloに適用した結果

Before: おおむねothello-clからの移植が終わった時点。コミットID:ba72a26...

f:id:eshamster:20160321012823p:plain 原寸大リンク(3047x4615)

After: 記事時点のコミット。コミットID:eb3fe18...("CL-OTHELLO"パッケージをexclude)

f:id:eshamster:20160321032745p:plain 原寸大リンク(2925x4126)

なるほど、分からん。

まあパッと見をどうこうするというよりは、ざっと眺めていって、参照されてないシンボルがあるけどこれexport必要だっけ、とか、このパッケージのシンボル1個だけ参照してるけど不要な参照してないっけ、とかを考えるためのものです。

ちなみに、--only-packageをつけるとこんな感じの出力になります。汚い…。

f:id:eshamster:20160321161612p:plain 原寸大リンク(1035x943)

実装について

対象となるパッケージを取り出す

まずは、システム内のパッケージを一通り取り出す部分です。怪しげなので、もっと賢い方法があれば知りたいです…。

;; ロードメッセージ省略コードやreadtableを戻すコードは省略
(defvar *unprocessed-pack-list* nil)

(defun make-macroexpand-hook-fun (old-hook)
  (lambda (fun form env)
    (when (and (consp form)
               (eq (car form) 'cl:defpackage))
      (pushnew (cadr form) *unprocessed-pack-list*
               :test #'string=))
    (funcall old-hook fun form env)))

(defun load-target-package-list (system-name)
  (ql:quickload system-name)
  (let ((*macroexpand-hook* (make-macroexpand-hook-fun *macroexpand-hook*)))
    (asdf:load-system (intern system-name "KEYWORD") :force t)))

ここはql:quickloadのパッケージ名出力コードを参考にしています。*macroexpand-hook*にフックをかけて、defpackageが来たらそのパッケージ名をリストに入れるというのが基本的な考え方です。しかし、ql:quickloadでは依存パッケージが全て読まれてしまうため、どれが対象のsystem下のパッケージか分からないという問題があります*4。そこで次のような力業に出ています。

  1. とりあえずql:quickloadで全てロード
  2. 上記のフックをかける
  3. (asdf:load-system ... :force t)で対象システムを無理やり読み直す
    • 依存システムはロード済みなので、対象システム下のパッケージだけがとれる…はず

参照している他パッケージのシンボルを取り出す

残りは末尾に全コードを貼り付けたので、書きたいところだけ簡潔に…。

どのパッケージのシンボルをインポートしているかを調べる処理のメインはinterpret-package関数です。do-symbolsで各パッケージ内の全シンボルを調べて、exportされているシンボルと、同システム内の別パッケージから継承したシンボルを記録しています。これで漏れが出ない…はず。

また、パッケージ間の関係を木構造(正確には有向グラフ?)と見た場合、幅優先探索の順序でパッケージを見ています(起点は上記のパッケージ探しで最初に見つけたもの)。この方がgraphviz上で元の木構造に近い形が得られやすいためです。実際、深さ優先探索版をcl-othelloに適用したところ、ほぼ全パッケージが縦に並んでしまい、うまく配置できないようでした。ただ、こうした探索自体が歴史的経緯*5で必要だったもので、今なら単に上記で見つけた順で問題ない気もします。

グラフ化

graphviz用のコードを出すためにs-dotというライブラリを利用しています。graphvizのドット形式をS式で書くためのDSL兼レンダラです。

ただ困ったことに、DSLのキーワードであるnodeやらedgeやらが全てs-dotパッケージのシンボルとeq判定をとっていて、しかもexportされていません…*6。一個や二個ではないので、対症療法としても一々s-dot::nodeのように書くのも面倒です。そんなわけで、下記のリードマクロで$nodeのように書けるようにしています。

(set-macro-character #\$ #'(lambda (stream &rest rest)
                             (declare (ignore rest))
                             (let ((sym (read stream nil)))
                               (intern (symbol-name sym) "S-DOT"))))

全コード

この記事時点のコードを貼り付けます。

gist.github.com

感想

こうメタな情報に普通にアクセスできるのはなんだか気分がいいですね。


*1:モンテカルロ木探索といえばAlpha Goが話題ですね。Deep Learningばかり話題になっている感もありますが、2004年に登場したモンテカルロ木探索というブレイクスルーあってのものだとこっそり主張しておきたかったりします。個人的には、モンテカルロ木探索がDeep Learningという翼を得てさらに飛翔するのか、翼だけ飛んでいってしまうのか気になってます。

*2:「clなんとか」という名前順になっていない辺り分かってなかった感が目に見えます

*3:本当はボックス(cluster)から直接線を伸ばしたいのですが、それができないので代替手段です

*4:quickloadの方は依存パッケージも全て出力するのでこの問題は関係ありません

*5:元々system内のパッケージ一覧を取り出す方法を思いつかなかったので、システム名と同名のパッケージがあると仮定(ないとエラー)して、そこから辿っていました

*6:まあ大量にexportされても困るのでキーワードにしておいて欲しかったという話です。実際、キーワード利用に変更したs-dot2というプロジェクトがあったりします(quicklispのリポジトリには登録されていませんが…)

Node.js本のサンプルをCommon Lispで書く

こんな本を買いました。タイトルを見てもピンと来なかったのですが、目次やレビューを見ると新し目の技術を広く浅く見るのに良さそうと感じた次第です。

当然サンプルコードはJavaScriptで書かれているわけですが、そのまま書き写しても面白くないので、前回記事で作った環境上を使ってCommon Lispで書いてみることにしました。Parenscriptを利用して、Common Lispで書いたコードをJavaScriptコードに変換する方針です。

github.com

結果としては、次の手順で作れるようになりました。単一ファイルのスクリプトのみ対応済みです。

  1. init.ros(Roswellスクリプト)でテンプレートからlispファイルを作成
  2. プログラム本体や(Node.jsの)依存ライブラリを記述
  3. run.rosJavaScriptへ変換&実行

テンプレート:skelton/skelton.lisp

次のようなテンプレートを用意しました。

<% @var name %>などと書いているのはcl-emb(紹介記事「HTML内にコードを埋め込めるテンプレートエンジン「CL-EMB」 - Qiita」)によるコード埋め込み書式です。

大体次のような感じで使います。

  • npm installしたい依存ライブラリは*dependencies*に書く(例.'("cheerio-httpcli" "request")
  • casperjsコマンドなど、nodeコマンド以外から実行したい場合は、*executor*を書き換える
  • メインの処理はwith-use-ps-pack内に、その他関数などは特定の書式でトップレベルに書く

一行目でしれっとps-experimentなるものをquickloadしていますが、自作のParenscript拡張です。quicklispリポジトリには入っていないので、適切なフォルダ下(前回記事の手順であれば~/.roswell/local-projects/下)にgit cloneしておく必要があります。

github.com

どんな感じに使えるのかは記事後半のサンプル参照。

テンプレートからlispファイルを生成:init.ros

上記のテンプレートからlispファイルを生成します。

引数処理が長々しいですが、処理としてはcl-embによる変換を噛ませている程度で、ほぼテンプレートのコピーをしているだけです。次のような感じで使います。この例であれば、part02/download-node.lispを編集して必要な処理を書いていきます。

$ ./init.ros <ディレクトリ名> <プログラム名>
# $ (例)./init.ros part02 download-node
# Create part02/download-node.lisp

一応の事故防止処置として、存在しないディレクトリを指定した場合や、既存のファイル名と被る場合はエラーにしています。

lispファイルをjsファイルに変換して実行:run.ros

編集が終わったら実行です。

長々と処理していますが、やりたいことはmain関数の最後の数行です。

  1. init.rosで生成 & 手で編集したlispファイルをロードし(load
  2. JavaScriptファイルをWORKディレクトリ下に書き出し(output-js
  3. 未インストールの依存npmパッケージをインストールし(install-by-pm
  4. node.jsスクリプトとして実行します(run-shell

3のインストールチェックですが、直下のnode_modulesフォルダに該当する名前のフォルダがあるかどうかだけを見る超簡易判定です。npm install自体にインストールスキップ機能がないか調べましたが、見つからなかったのでこんな感じにしました。

package.jsonリポジトリ内で使うライブラリを全てまとめる…というのが普通のやり方のような気がしますが、たかがスクリプトなのでql:quickloadのように気軽に書きたかった感じです。

使用例

少し長めですが、関数作成とnpmモジュールの利用の2つが揃ったものとしては一番短い例を載せます。Wikipedia日本語版のハムスター*1の記事から画像をダウンロードするコードです。

ここで、ps-experimentの目立った機能としてはdef~.ps系マクロとwith-use-packマクロがあります。def~.ps系マクロはトップレベルでParenscript用の関数やら変数やら(あと構造体)を定義するためのものです*2

これらの定義を含めてJavaScriptコードを吐き出すのがwith-use-packの役割です。第一引数のリストで定義を読み込むパッケージを指定します(:thisは自パッケージのエイリアス)。スクリプトレベルだと関係ありませんが、use-packageの関係にあるパッケージは依存関係と見て再帰的に定義を読み込みます*3。なお、パッケージマネージャもどきなので名前空間を分けてくれたりはしません。

他細かい点。

  • --ps:chainの別名です。安易にこういう別名を提供するのは好きではないですが、chainが余りにも良く出てくるので堪え切れませんでした…*4。インデントにもろに効くのに5文字は長い
  • ドット記法は内部的に@記法?に変換される("a.b.c" → "(ps:@ a b c)")ので安心です。JavaScriptへの変換時にドット記法に戻るため一見無駄に見えますが、ps:with-slotsなどとの兼ね合いで必要です
  • URLエンコードが少々曲者です。マルチバイト文字列はParenscriptがユニコードエスケープ形式に変換する(してしまう)ので、先にURLエンコードしています(url-encodeマクロ)

最後に、生成されるコードは以下のようになります。生成後の方が短いとかいうのは野暮です。こういったスクリプトレベルでは、Lispで書けるという以上の利点は薄いかもしれません。

var CLIENT = require('cheerio-httpcli');
var REQUEST = require('request');
var FS = require('fs');  
var URL = require('url');
var SAVEDIR = __dirname + '/img';
function ensureSavedir() {
    return !FS.existsSync(SAVEDIR) ? FS.mkdirSync(SAVEDIR) : null;
};
function downloadImages() {
    var url = 'https://ja.wikipedia.org/wiki/' + '%E3%83%8F%E3%83%A0%E3%82%B9%E3%82%BF%E3%83%BC';
    var param = {  };
    return CLIENT.fetch(url, param, function (err, $, res) {
        if (err) {
            console.log('error');
            console.log(err);
            return null;
        };
        return $('img').each(function (idx) {
            var src = URL.resolve(url, $(this).attr('src'));
            var fileName = URL.parse(src).pathname;
            var filePath = SAVEDIR + '/' + fileName.replace(/[^a-zA-Z0-9\.]+/g, '_');
            return REQUEST(src).pipe(FS.createWriteStream(filePath));
        });
    });
};
ensureSavedir();
downloadImages();

こういったものは、Clojure Script擁するClojureの方が良いのかもしれないですね。そちらも調べておきたいです。

*1:本ではネコだった気がしますが気のせいです

*2:ちなみに、「def~.ps+」系のマクロを利用すると、Parenscript用定義とCommon Lisp用定義を同時にできます

*3:今は循環参照のチェックをサボっているので、循環参照させると無限ループします… → 2016/02/16 修正済み

*4:ハイフン2つでchain=鎖のイメージです。ClojureJava関数のチェインで".."が使われているのでなぞるべきだと思ったのですが、Common Lispではエスケープが必要になるので断念…。

Common Lisp開発環境を新規に作ったのでメモ

JavaScriptの本「JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック」を買ったのでCommon Lispの環境を新規に作ってみました(正しい日本語です)。

Emacsには抵抗がなく、これからCommon Lispを始めたいという人にもちょうど良いぐらいの内容ではないかと思います。ということで、そんな体で書いていきます。

以下の状況からスタートします。基本的には本書の「第1章03 環境を構築しよう」が完了した時点と思えば良いです。

CL開発環境の構築は基本「Modern Common Lisp」を参照すれば良いと思っていますが、主な差分は以下の点です。

  • 処理系はRoswellを通して入れましょう
    • 色々楽になる
    • quicklispのインストールも不要になる
  • Emacsは24以降を入れましょう
    • 上記で紹介されているあれこれのEmacs拡張が簡単にインストールできる

ここではほぼ入れ方しか書いていないので、使い方は同リンクを参考にすると良いです。

Emacs24.5のインストール

Emacs 24以降は標準でパッケージマネージャ(init.elからはpackage-installで利用可)がついているのでこれをインストールします(yumで入るのはもう少し古いはずです)。下記は執筆時点で最新の24.5の例です。

sshからの利用を前提にしているので、--without-xを入れている辺りがポイントでしょうか。

$ sudo yum install lcurses-devel
$ wget http://mirror.jre655.com/GNU/emacs/emacs-24.5.tar.gz
$ tar zxvf emacs-24.5.tar.gz
$ cd emacs-24.5
$ ./configure --without-x
$ make
$ sudo make install

Roswellをインストール

※この先はおおむねスクリプト化しています(記事末尾に貼り付け)。Emacsだけスクリプトに入っていないのは単にそういう手順で入れてしまったからで、特に意味はありません。

少しだけRoswellの説明

Common LispではSBCLやらClozure CLやら処理系が色々あったり、意外とインストールに失敗したりと入り口でつまづきがちです。

今ならとりあえずRoswell入れておけばこうした煩わしさから解放されます。このRoswell自体は処理系ではありません。入れておくとシェル上からros install sbcl-binros install ccl-bin*2とコマンドを一発叩くだけで主要な処理系をインストールすることができるようになります。しかも、Common Lispにおけるパッケージ管理のデファクトであるquicklispレディな状態でインストールされるため、quicklispのインストール手順も省略できます。

Roswellは処理系インストールだけが機能ではないので、深町さんによる紹介記事「Common Lispとリアル・ワールドを繋ぐ「Roswell」の紹介 - 八発白中」等も参考にすると良いです。

インストール

基本的には普通のconfigure, make, make installですが、先に必要なものを入れておきます。

まずyumで色々入れます。この先で必要なものもまとめてしまったので、どれがRoswell入れる上で最低限だったか覚えていないです…(libcurlぐらいだったような…)。

$ sudo yum -y install libcurl-devel zlib-devel perl-ExtUtils-MakeMaker

CentOS 6系では大抵autoconfのバージョンが古くてconfigureに失敗するので、2.65未満の場合は新しいバージョンを入れておきます。

$ wget http://ftp.gnu.org/gnu/autoconf/autoconf-2.69.tar.gz
$ tar zxf autoconf-2.69.tar.gz
$ cd autoconf-2.69
$ ./configure
$ make
$ sudo make install

いよいよRoswellを入れます。

$ git clone -b release https://github.com/roswell/roswell.git
$ cd roswell
$ sh bootstrap
$ ./configure
$ make
$ sudo make install

ros runと打ってみると(初回のみ)SBCLがインストールされた後にREPLが起動します(下記はインストールの出力は略)。

$ ros run
* (+ 1 2)

3
* (quit)

オプション

ここで説明するものは、Emacs + Common Lispな環境をセットするだけであればオプションですが、下記に貼り付けた.emacs.d/init.elでは前提になっています。概要のみ記述しますので、インストール方法の詳細は末尾のスクリプトを参照してください。

より新しいバージョンのGitを入れる

EmacsからGitを操作できるmagitという拡張がありますが、(執筆時点で)バージョン1.9.4以降のgitを求められます。yumで入るものは1.7系とやたら古いので新しいものを入れておきます。

ちなみに、個人的にmagitで特に重宝しているのは、git branchなどリポジトリのファイルを書き換える操作を行った時に、バッファ内のファイルをアップデートしてくれるという点です。基本的に開いたバッファは開きっぱなしなので大変助かってます。また、いったんM-x magit-statusで開いてしまえば、主要な操作方法はhキーを押せばいつでも見られるという点も助かる点です。

HyperSpecをローカルに入れておく

HyperSpecCommon Lisp仕様書…ではありませんが、事実上仕様書のようなものです。頻繁に利用することになるので、Emacsからローカルに参照できるようにしておくと便利です。

slime-repl-ansi-color.elを入れる

SLIME(参考:モダンCommon Lisp第3回: SLIMEの使い方 基礎編 | ありえるえりあ)で色の表示を可能にする拡張です。

Emacsの設定と準備用スクリプト

新規環境であれば、Emacsを入れたら後は下記のスクリプトを走らせて、.emacs.d/init.elに下記の内容をコピーするだけでも動くはずです。

あくまで自身のメモを兼ねているので、最小限の環境ではないです。が、お勧め設定+αぐらいに割りと収まっていると思います、きっと…。

prepare.sh

上記で書いてきたEmacs以外のインストールをまとめたスクリプトです。注意ですが、平気でprefixなしのsudo make installをしているので既存環境でいきなり走らせることはお勧めしません。

何点か補足。

  • L37: gitのバージョンチェックが超雑です
  • L70: SBCLのインストールも済ませてしまおうと、一回Roswellを起動・終了しています

gist.github.com

~/.emacs.d/init.el

余り害のあるものは入れていないつもりです。ただし、慣れないうちはparedit関係のものはコメントアウトしておくと良いと思います。;; paredit(108行目)とコメントがある少し下のadd-hook3つをコメントアウトしておけば特に害はないはずです。なお、気になってきたら「ParEdit チュートリアル」辺りを参考に試してみると良いと思います。慣れれば非常に快適です。

Common Lispに直接関係のあるものは81行目(;; ----- Lisp ----- ;;とコメントのある行)以降です。

何点か補足。

gist.github.com

*1:どう見てもCentOS 6.5が落ちてくるようなアドレスですが、試した時には6.7でした。今後もアップデートされそうな気がします

*2:CentOS 6系ではClozure CLの最新版はGLIBCが古くて動かないです。その場合、ros install ccl-bin/1.9 とすれば古いものを入れられます

認証付きプロキシ環境でも(多少は)快適なpackage-installの利用

Emacs24を入れてから、list-packagesで適当にパッケージを追加してきたのですが、そろそろ.emacs.d/init.elのポータビリティが怪しくなってきたので、環境を見直し始めました。当初は、CaskやEl-getといったモダンな?パッケージマネージャを使おうと意気込んでいたのですが、認証付きプロキシ環境でどうもうまく動きませんでした…。プロキシが原因なのかはよく分かりませんが。

今後も振り回されるかも知れないと思うと、解決は目指さずに(手元の環境下で)実績のあるデフォルトのpackage-installに頼る方向にしました。

結果、「結局 package.el に戻ってきた / マスタカの ChangeLog メモ」を参考に以下のようにしました。package-refresh-contentsの呼び出しを、新しいパッケージのインストール時に限定している辺りが唯一のポイントです。呼び出すとプロキシのユーザ名・パスワードを求められるため、むき出しにするとEmacsを起動するたびに死にたくなります。かといって呼ばないとインストールできないためこうなりました。

ユーザ・パスワードの入力が新しいパッケージをインストールするときに限定されるので、「多少は」マシかと思います。

(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/") t)
(package-initialize)
 
(defun install-packages (packages)
  (let ((refreshed nil))
    (dolist (pack packages)
      (unless (package-installed-p pack)
        (unless refreshed
          (package-refresh-contents)
          (setq refreshed t))
        (package-install pack)))))
 
(install-packages '(auto-complete
                    magit
                    markdown-mode))

まあsocatなどでローカルにプロキシサーバを立てて認証を代理させるのが筋な気はしますが…。書いてしまったものは仕方がない!

ちなみに、bash環境変数に設定しているhttp_proxyhttps_proxyのプロキシのアドレスは認識してくれたのですが、一緒に書いた認証情報は無視されるようでした。