シングルファイルのEmacs Lispをダウンロードして読み込む

Emacsを使っていると小さなEmacs Lispがどんどん増えていく。通常は ~/.emacs.d/init.el に記述したり、 ~/.emacs.d にディレクトリ(例: ~/.emacs.d/lisp )を作り、そのパスをload-pathに追加する事で、利用できるようにする事が多い。

そこそこ育ったEmacs LispはMELPAに登録し、そこからインストールして使用するという方法もある。その方法は、多くの人が使うものであれば望ましいけれど、自分だけの要求を満すだけのLispであれば、自分自身で気軽に修正できる近い距離にあった方が、管理コストを押える事ができるし、その後の保守が行き届く。パッケージアーカイブに登録されたまま、放置され続けるEmacs Lispは沢山ある。そうなるぐらいなら、保守しやすい距離にLispがある方が良いはずだ。

これらの方法はルールがあるようにも思えるけれど、実は特にルールなんて存在しないんじゃないのだろうか。かつては共有されたEmacs Lispを、思い思いの方法で適当なディレクトリにダウンロードし、パスを通して使うような事をしていた筈だ。EmacsWikiに張り付けられたEmacs Lispのように。その取り決めのないカオスな状態から、時代が進むにつれ取り決めができ、手段が整備されてきた。それは良い事だと思うけれど、自分自身が管理する全てのEmacs Lispまで足並みを揃えないといけないなんてルールはない。ルールに従って手数が増えてしまうぐらいなら、ルールに従わず手数が少なくし、その変わりに保守できる方が良い。

そんな事を考えて、自分自身のEmacs Lispの保守の仕方を試行錯誤したりしていた。MELPAの仕組みを参考に自分自身のELPAリポジトリを作成したり、ElgetやQuelpaを使ったり、GitやGistにEmacs Lispをホスティングしたり、それこそ ~/.emacs.d/lisp 配下に雑にelファイルを配置したりした。それぞれの手段は当然良い面もあれば悪い面もある。そういう気付きを得られた事は良かった。

今回はその試行錯誤の一環として、自分用のEmacs Lispのダウンローダを実装した。要はQuelpaやElgetのようなものなのだが、それらより圧倒的に機能が少ない。なぜそんな100番煎じのような事をするのかというと、一重に他のツールは機能が多すぎるからだ。いろんな事が出来るという事は、その分ややこしいとも言える。理想な状態というのは、必要な機能が全てあり、それ以外の機能がない事だと思う。必要な機能というのは人によって異なるから、正解という状態はないのだろうけれど、自分が現在考える最も良い方法を試してみて、実際にどうかを確かめたくなった。

数年前からブログを書いていて、しばしばそこにEmacs Lisp自体も記述している。簡単で小さなコードだから、GitHubに専用のリポジトリを用意したりMELPAに登録したりといった仰々しい管理はしたくない。また記事として文章でコードの説明をしていたりするので、それらをそのまま使用できると良さそうだった。

そこで、次の処理を行う事にした。

  1. Emacs Lispファイルがあるかどうかを確認し、あればそのディレクトリをload-pathに追加する。
  2. Emacs Lispファイルが無ければ、インターネットからダウンロードし、ダウンロード先のディレクトリをload-pathに追加する。
  3. ただし、ダウンロードしたものが正しいものかどうかをチェックするために、事前にハッシュ値を記述しておき、一致したもののみ使える状態とし、一致しなければ破棄する。

コードを掲載しておく。

getelisp.el
;;; getelisp --- Simple Emacs Lisp Library downloader. -*- lexical-binding: t -*-

;; Copyright (C) 2023 TakesxiSximada

;; Author: TakesxiSximada
;; Maintainer: TakesxiSximada
;; Version: 1.2
;; Package-Version: 20231009.0000
;; Package-Requires: ((emacs "29.1"))
;; Date: 2023-10-09

;; This file is part of getelisp

;;; License:

;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU Affero General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; Affero General Public License for more details.

;; You should have received a copy of the GNU Affero General Public
;; License along with this program.  If not, see
;; <https://www.gnu.org/licenses/>.

;;; Code:

(require 'plz)

(defvar getlisp-hash-solt "*GETLISP*"
  nil)

;;;###autoload
(defun getelisp-show-checksum-current-buffer ()
  (interactive)
  (let ((hash-val (secure-hash
		   'md5 (concat getlisp-hash-solt (buffer-string)))))
    (message "Checksum Value: %s" hash-val)
    (kill-new hash-val)))

;;;###autoload
(defun getelisp-show-checksum (url)
  (interactive "sURL: ")
  (plz 'get url :as 'response
    :then (lambda (resp)
	    (let ((hash-val (secure-hash 'md5 (concat getlisp-hash-solt
						      (plz-response-body resp)))))
	      (message "Checksum Value: %s" hash-val)
	      (kill-new hash-val)))))

;;;###autoload
(defun getelisp (file-path url hash-val)
  (let* ((abspath (expand-file-name file-path))
	 (absdir (file-name-directory abspath)))
    (if (file-exists-p abspath)
	(add-to-list 'load-path absdir)
      (plz 'get url :as 'response  ;; ファイルが存在しなければダウンロードを試みる
	:then (lambda (resp)
		;; ハッシュが一致するか確認する。一致しない物は怪しいデータと考えて受け入れない。
		(let* ((body (plz-response-body resp))
		       (hash-val-actual (secure-hash 'md5 (concat getlisp-hash-solt body))))
		  (if (string-equal hash-val hash-val-actual)
		      (progn
			(with-temp-buffer  ;; ファイルへ書き込む
			  (insert body)
			  (make-directory absdir t)                        ;; directoryを作成
			  (write-region (point-min) (point-max) abspath))  ;; ファイルへ書き込む
			(message "append to load-path: %s" absdir)
			(add-to-list 'load-path absdir))  ;; パスの追加
		    (error "bad checksum: expected=%s actual=%s" hash-value hash-val-actual))))))))

(provide 'getelisp)