KDOC 15: Emacs Caskを読む

CaskはEmacsのプロジェクト管理ツール。ファイルに依存ライブラリを列挙し、同じ環境を再現できる。Caskの仕組みを調べる。

memo

機能

そもそもどんな機能があるか。

define-error 各エラーの定義。

defstruct は構造体を定義する Common Lisp の関数。使い方がわからない。

check-parens カッコの対応をチェックする関数。

helpの出力

そもそもinstall以外の機能を知らない。

  USAGE: cask [COMMAND] [OPTIONS]

Emacs dependency management made easy

COMMANDS:

 pkg-file                                Write a ‘define-package’ file.

                                         The file is written to the Cask project root path with name
                                         {project-name}-pkg.el.
 install                                 Install all packages specified in the Cask-file.

                                         The dependencies to packages are also installed.  If a package
                                         already is installed, it will not be installed again.
 update                                  Update package versions.

                                         All packages that are specified in the Cask-file will be updated
                                         including their dependencies.
 upgrade                                 Upgrade Cask itself and its dependencies.

                                         This command requires that Cask is installed using Git and that
                                         Git is available in ‘exec-path’.
 upgrade-cask                            Upgrade Cask itself and its dependencies.

                                         This command requires that Cask is installed using Git and that
                                         Git is available in ‘exec-path’.
 exec [*]                                Execute ARGS with correct ‘exec-path’ and ‘load-path’.
 version                                 Print version for the current project.
 list                                    List this package dependencies.
 info                                    Show info about the current package.
 help [command]                          Display usage information or documentation for COMMAND-NAME.
 load-path                               Print ‘load-path’ for all packages and dependencies.

                                         The output is formatted as a colon path.
 exec-path                               Print ‘exec-path’ for all packages and dependencies.

                                         A dependency will be included in this list of the package has a
                                         directory called bin in the root directory.

                                         The output is formatted as a colon path.
 eval <form>                             Eval FORM with the ‘load-path’ set according to the project.
 path                                    Print ‘exec-path’ for all packages and dependencies.

                                         A dependency will be included in this list of the package has a
                                         directory called bin in the root directory.

                                         The output is formatted as a colon path.
 package-directory                       Print current package installation directory.
 outdated                                Print list of outdated packages.

                                         That is packages that have a more recent version available for
                                         installation.
 files                                   Print list of files specified in the files directive.
                                         If no files directive or no files, do nothing.
 build                                   Build all Elisp files in the files directive.
 clean-elc                               Remove all byte compiled Elisp files in the files directive.
 link [*]                                Manage links.

                                         A link is just that, a symbolic link.  The purpose of the link
                                         command is that you should be able to work with local
                                         dependencies.

                                         For example, let’s say you are developing an Emacs package that
                                         depends on f.el. Consider what happens if you need to extend f.el
                                         with some function that your package requires.

                                         With the link command, you can checkout f.el locally, add it as a
                                         link in your local package.  That means that when you require
                                         f.el, you will require the local package instead of the one
                                         fetched from the ELPA mirror.  Now you add the desired function
                                         to f.el and use your library to try it out.

                                         COMMAND-OR-NAME can be one of: delete, list or a link name.
                                         ARG is sent to some of the commands.

                                         Commands:

                                          $ cask link list

                                           List all project links.

                                          $ cask link name path

                                           Add local link with NAME to PATH.

                                          $ cask link delete name

                                           Delete local link with NAME.
 package [target-dir]                    Build package and put in TARGET-DIR or dist if not specified.
 emacs [*]                               Execute emacs with the appropriate environment.

OPTIONS:

 --proxy <host>                          Set Emacs proxy for HTTP and HTTPS to HOST.
 --http-proxy <host>                     Set Emacs proxy for HTTP to HOST.
 --https-proxy <host>                    Set Emacs proxy for HTTPS to HOST.
 --no-proxy <host>                       Set Emacs no-proxy to HOST.
 --version                               Print Cask’s version.
 -h [command], --help [command]          Display usage information or documentation for COMMAND-NAME.
 --debug                                 Turn on debug output.
 --path <path>                           Run command in this PATH instead of in ‘default-directory’.
 --verbose                               Be verbose and show debug output.
 --silent                                Be silent and do not print anything.

eplって何

パッケージ関連のライブラリ。

例外を返すcask–exit-error

渡されたエラーの種類によって正しいメッセージを返す。

(defun cask–exit-error (bundle err)

読み進めるread

(read (current-buffer))

エラーをシグナルする関数signal

signalはエラーをシグナルする関数。

ローカル定義関数を作るcl-flet

ローカル定義関数を作る。

メタプロパティを付与するdeclare

Declare Form (GNU Emacs Lisp Reference Manual)

関数やマクロにメタプロパティを付与するのに使う。陳腐化マークをつけたり、TABインデント規則をつけたりできる。たとえば通常defunでは第3引数に来るdoc stringを、ほかのマクロで定義するのに使う。

cask–with-file

引数のcaskが存在すれば、bodyを評価するマクロ。

f-file?
file-regular-p のエイリアス。regular file って何

commanderパッケージ

CLIの実行には、commanderというパーサーライブラリを使っている。これを使ってEmacs Lispで書かれた関数をシェルから呼び出せるようにしている。サブコマンド・オプション・ヘルプ表示などをommanderでしている。

Tasks

Archives

DONE cask-printの仕組み   Write

cask-print内でgreen関数を使って出力を色付けできる。直接green関数は中でないと利用できない。これはどういう仕組みになっているのだろう。

(cask-print "----" (green "green") "----")

-—green-—

(cl-defmacro cask-print (&rest body &key stderr &allow-other-keys) “Print messages to `standard-output’.

The BODY of this macro is automatically wrapped with `with-ansi’ for easier colored output.“ (delq :stderr body) `(when (or (not (boundp ‘cask-cli–silent)) (not cask-cli–silent)) (princ (with-ansi ,@body) ,(when stderr ’(function external-debugging-output)))))

  • cask-print
    • with-ansi
(with-ansi "----" (green "green"))

-—green

(defmacro with-ansi (&rest body)
  "Shortcut names (without ansi- prefix) can be used in this BODY."
  (if ansi-inhibit-ansi
      `(ansi--concat ,@body)
    `(cl-macrolet
         ,(mapcar
           (lambda (alias)
             (let ((fn (intern (format "ansi-%s" (symbol-name alias)))))
               `(,alias (string &rest objects)
                        ,(list 'backquote (list fn ',string ',@objects)))))
           (append
            (mapcar 'car ansi-colors)
            (mapcar 'car ansi-on-colors)
            (mapcar 'car ansi-styles)
            (mapcar 'car ansi-csis)))
       ,(cons 'ansi--concat body))))
  • with~系はだいたいマクロで、バインドされた関数をbodyで渡すフォームで使用できることを示している
  • with-ansi関数の概略
    • 色のリスト(black white …)を使って、関数のエイリアスを割り当てる
      • (ansi-black) (ansi-white) マクロがある。それらを、with-ansi 内では(black) (white) で呼び出せるようにエイリアスを張る
    • macroletの第1引数でマクロ名と関数の中身を定義してるっぽい
      • だから、第2引数のbody部分では↑マクロが使える
        • ,(cons 'ansi--concat body) の箇所
    • consでbodyをansi–concatの引数にしてる。body内のblackとかは、macroletで定義したマクロで解釈される

DONE fetcherとは何か

どのバージョン管理システムを使ってダウンロードするかみたい。

cask-fetchers

(:git :bzr :hg :darcs :svn :cvs)

セットされてないケースもある。ない場合はローカルのファイルから処理する、などの分岐がある。

DONE どうやってinstallしているか

  • cask-install
    • cask–with-environment バインドして、bodyを実行する
      • cl-destructuring-bind 実行結果で変数をバインドする
        • cask–dependencies-and-missing 依存関係を集める
        • cask–install-dependency 個別にインストールを実行
        • missing-dependencies のときはエラーを吐く

bundle構造体に対して、さまざまなアクセサがある感じか。bundleがよくわかってなくてピンときてない感じ。ほとんどの関数はbundleを引数にとる。

  • cask–install-dependency
    • print関係やってる。見覚えがある
    • 本質的にはepl-install-file か epl-package-install を使ってインストール
      • install-fileはファイルからインストールし、package installは名前からダウンロードか
      • fetcherの有無で分岐する
      • インターネットからダウンロードするか、ローカルファイルからインストールするか、ということか
    • epl-refresh package descriptionを更新する
    • cask–checkout-and-package-dependency パッケージのパスを返す
    • cask-dependency 系はCaskのdepends-on関数で指定されるもの。各依存パッケージが入っている。
    • cask–with-package 引数がパッケージであればbodyを評価し、パッケージでなければ例外を返す
    • パッケージ名が存在するか、また必要な値を持っているかチェックして、最後にインストール
      • 途中でおかしいところがあれば例外を出す
      • インストールは epl-package-install を使う
        • 内部的にpackage-install を使う
      • ダウンロード + 評価される

まとめると、depend-onの情報を元にパッケージ名を特定する。チェックして、既存のインストール関数を使うことでインストールする。fetcherを変更したりもあるので、その分岐も入っている。

DONE どこでbundle structをセットしているか

テストを見ればよさそうに見える。

  • cask-test/with-bundle Caskファイルの中身を渡して、生成する + body評価。bundleがバインドされてる
    • cask-test/with-sandbox
      • f-with-sandbox
  • (cask-setup cask-test/sandbox-path) みたいな感じでbundleが生成されている。プロジェクトディレクトリを指定してcask-setupによってパースを開始する、ということか。パース結果がbundleである
  • CLIから実行した場合も、全く同じようにcask-setupでbundleを用意する。CLIのパスが使われ、キャッシュとして変数に実行結果が保存されるというだけの違い
(cask-bundle-name (cask-setup "~/.emacs.d"))
(cask-bundle-version (cask-setup "~/.emacs.d"))
(cask-bundle-description (cask-setup "~/.emacs.d"))
(cask-bundle-runtime-dependencies (cask-setup "~/.emacs.d"))
(cask-bundle-development-dependencies (cask-setup "~/.emacs.d"))
(cask-bundle-path (cask-setup "~/.emacs.d"))
(cask-bundle-patterns (cask-setup "~/.emacs.d"))
(cask-bundle-sources (cask-setup "~/.emacs.d"))
  • cask-bundleの中にcask-dependency[]がある。dependencyにnameやversionが含まれていて、名前を元にダウンロードする
  • Caskに書かれているコードはリストとして処理される。構造体に格納され、伝播する

DONE ansi–define

Caskというかansiパッケージの話。どういう仕組みになっているか。 (ansi–define red) という風に色を定義している。

(defmacro ansi--define (effect)
  "Define ansi function with EFFECT."
  (let ((fn-name (intern (format "ansi-%s" (symbol-name effect)))))
    `(defun ,fn-name (format-string &rest objects)
       ,(format "Add '%s' ansi effect to text." effect)
       (apply 'ansi-apply (cons ',effect (cons format-string objects))))))
  • ansi–define(my-test) とすると、ansi-my-test関数が定義される
    • defunの次の行はdocstring
    • ansi-apply 内で呼び出されるansi–code関数で、色名と番号を紐づけている。だからansi–defineでの引数がgreenだとgreenでansi-applyされて対応する32が取り出され、その内容でansi-green関数が定義される
    • 適当な名前でansi-defineしても、関数は定義される。しかし、対応するコードが存在しないため実行時エラーになる
(ansi-green "green")

green

DONE どうやってDSLを定義しているか

Cask-fileではいくつかのDSLが使えるが、その仕組みはどうなっているか。

驚くほどシンプルに実装できる。

  • cask–eval で定義している
    • dolistで各リスト処理。それぞれのcarを見て、cl-case分岐
    • キーワードがcaseで引っかかるようになっていて、その処理が走る
      • たとえば引数formsが(source 1) の場合は、case条件でsource が一致してそこの処理が走る、というだけのこと
    • DSLの引数はcl-destructuring-bindでformからバインドする

これによって、処理全体中の、ファイルから構造体に読み込む部分が理解できた。

  • たとえばdepend-onの場合は、bundleに値を追加する。DSLを処理する段階ではグローバルな状態を保持するbundle structに集積していく
  • あとから、bundleからまとめて必要な値を取り出してインストールする、みたいな感じ

CLOSE recipeとは何か   Write

たまに見るがどういう意味なのだろう。パッケージのビルドと関係ありそうに見える。Melpaのレシピでは、パッケージ名とかURLを指定するが…ここではどういう意味なのだろう。

  • package-recipe.el がある
    • package-recipeというクラスがある
    • 組み込みでpackage-build というコマンドがある。対応する何かなのだろう

CLOSE cask installが遅い理由   Write

わからない。

すでに存在していても、0.5秒くらいかかるので、300パッケージあると150秒かかる。

ひとつひとつ通信しているように見える。ローカルだけだとこんなにかからないはずだ。なぜかMacだとすぐ終わっていたはずだが、わからない。

  • ダウンロードは、いくつかチェックがあって最終的に走る
  • すでにダウンロードされていると判定すれば、途中で抜けるので早いはず、だがここがあまり早くない
  • その前の判定に比較的時間のかかる箇所があるように見える
  • cask–dependency-installed-p ですでにダウンロードされてるかの判定をしている
    • 内部的に epl-package-installed-p を使っている
    • 引数に構造体epl-packageを取る。epl-package-create 関数を使って構造体を初期化する
    • 通信は発生してなさそう
      • さらに内部でpackage-installed-p を使っている
      • package-activated-list でチェック