ASDF
ASDF или "Another System Definition Facility" это система определения и/или установки `систем`, реализующих CL программы. Она используется как базовая система расширений в SBCL (OpenMCL, ECL, ACL etc.), на ней основывается common-lisp-controller, и большое количество различных CL приложений. Де факто asdf это стандарт в распространении CL программ.
Краткий обзор
Каждая система в смысле ASDF - некая совокупность файлов которые могут некоторым образом зависеть друг от друга, файлы эти могут представлять пакеты (в смысле CL системы пакетов), или же определения пакетов могут быть вынесены в отдельные файлы (pakages или pkgdcl).
Известный подход таких языков как C и С++ к распределению и иерархии файлов исходного кода это совместное использование заголовочных файлов .h и .c файлов, при этом существует модель КИС (клиент-интерфейс-сервер) в которой отдельные функционально значащие части программы - сервера и клиенты - выделяются в отдельные файлы .c, а интерфейс между ними оказывается распылён в .h файлах так, что фактическая структура этого интерфейса не лежит на поверхности. Подход ASDF совсем другой - весь интерфейс, представляемый деревом, описывается в одном файле .asd, в нём же и назначаются все функции действующие на дереве интерфейса.
Объектная система
ASDF написана на CLOS - автор творчески подошёл к вопросу представления всего что мы имеем (исходники) и всего что мы можем с этим сделать (компиляция, загрузка, тесты и т.д.) :) Самый общий класс это system - представляет собой программное обеспечение. От этого класса происходит класс module (сужая поля, и наоборот system наследуется от module), который обычно представляет подсистемы большой программы, эти подсистемы определяют в отдельные директории. За классом module следует класс component являющий собой класс для отдельных файлов. Ну и наконец отдельные файлы тоже делятся на подклассы:
component <- module <- system
source-file <- component
cl-source-file <- source-file
c-source-file <- source-file
java-source-file <- source-file
static-file <- source-file
doc-file <- static-file
html-file <- doc-file
Как мы видим потенциально ASDF мог бы работать не только с CL исходниками - соответствующие операции можно написать и для других классов исходного кода. На самом деле в некоторых пакетах средствами ASDF осуществляется компиляция Си файлов, которые просто встраиваются в структуру CL программы.
Однако в ASDF операции с файлами, модулями и системами это не просто методы, они тоже образуют классовую иерархию. (Если мы можем строить классы на данных, почему бы не строить их на функциях?). Факт наличия действия общего вида прописывается в классе operation, в нём же предусмотрены такие важные вещи как способы обхода дерева исходников. От класса operation наследуются классы действий более специального вида:
operation
compile-op <- operation
basic-load-op <- operation
load-op <- basic-load-op
load-source-op <- basic-load-op
test-op <- operation
В этом дереве классов мы имеем три вида базовых действий - компиляция, загрузка и проведение тестов. Теперь должно быть понятно что автор программы сам волен выбирать способы проведения всех этих действий как для системы в целом, так и для её отдельных частей.
Вся логика работы с экземплярами классов компонентов и действий осуществляется в методах, обычные же функции в ASDF - просто некоторые утилиты, некоторые из которых бывают весьма полезны.
Перед тем как приступить к практической части заметим ещё, что у любой системы может иметься поле зависимостей от других систем :depends-on, но его ASDF не обрабатывает - этим должна заниматься система установки более высокого уровня. С другой стороны - есть способы использования ASDF, позволяющие вставлять свой код поиска систем и, таким образом, осуществлять рекурсивный поиск систем - по запросу одной будет осуществляться загрузка всех зависимых.
Как получить?
Одним файлом - asdf.lisp (или с этого сервера в раскрашенном виде :), для большинства целей этого достаточно. Но можно получить и архив с дополнениями : asdf.tar.gz. Для доступа к git-репозиторию:
$ git clone http://common-lisp.net/project/asdf/asdf.git
Как загрузить саму ASDF?
Для начала нужно посмотреть, не установлен ли ASDF уже:
(require 'asdf)
; Если ASDF обнаружен, но не загружен - он загружается, функция возвращает ("ASDF")
; Если ASDF уже загружен (например в сеансе SLIME) функция вернёт NIL
; Но если ASDF не найден - будет ошибка
В SBCL ASDF установлен по умолчанию. На некоторых других реализациях - имеем третий вариант, поэтому загружаем asdf.lisp и определяем в директорию, которую назначено просматривать при загрузке (зависит от реализации), например CLISP при require просто пытается осуществить load после поиска в своих доверенных директориях:
; Метод загрузки "на лету":
(load ".../asdf.lisp") ; путь к asdf.lisp
; Иначе, когда asdf.lisp лежит в директории по умолчанию:
(require 'asdf)
Общий алгоритм загрузки систем
Как было объяснено - загружать файлы .asd с помощью load не есть хорошая практика - это будет работать только в простых случаях, но в целом нужно использовать операцию load-op (экземпляр класса load-op) - при её выполнении система ASDF начинает поиск по всем местам определённым в списке asdf:*central-registry*, затем передает управления всем пользовательским функция поиска в списке asdf:*system-definition-search-functions* и по нахождению системы задействуются методы - компиляции системы, настройки окружения для системы, загрузки системы в окружение.
Это всё заставляет выделить три метода управления системами в ASDF:
- 1. Манипуляции с asdf:*central-registry* - добавление директорий в которых могут находится .asd файлы, метод неудобен тем что директории проектов могут быть вложенными, и слишком много добавлений будет требоваться
- 2. От недостатка предыдущего метода можно избавится определив одну директорию в asdf:*central-registry* и накапливать в нём символические ссылки на реальные .asd файлы. Недостаток - необходимость в специальных приёмах эмуляции симлмнков в Windows.
- 3. Наконец запись функций поиска в asdf:*system-definition-search-functions*. Этот метод почти безупречен - все .asd файлы могут легко находится, а системы компилироваться или загружаться по мере надобности. Единственный недостаток - время рекурсивного поиска, но его можно ограничить установлением максимальной глубины просмотра директорий (обычно хватает 2-3 уровней).
Теперь нужно сконцентрироваться на отдельных ОСах и Лиспах :) Расмотрим первый вариант общий длявсех ОС-м, второй - отдельно для *X и Windows и третий вариант в самом конце.
1. Central-Registry: CL-PPCRE
Библиотека для работы с регулярными выражениями CL-PPCRE использует файл cl-ppcre.asd для установки. Допустим, директория для ваших лисп-библиотек /home/room/lisp, тогда вариант с использованием *central-registry*:
$ pwd
/home/room/lisp
$ wget http://common-lisp.net/project/asdf/asdf.tar.gz
...
$ tar zxvf asdf.tar.gz
...
$ wget http://www.weitz.de/files/cl-ppcre.tar.gz
...
$ tar zxvf cl-ppcre.tar.gz
...
$ ls
asdf/ cl-ppcre/
$ vi run-ppcre
(load "/home/room/lisp/asdf/asdf.lisp")
(push (truename #P"/home/room/lisp/cl-ppcre/") asdf:*central-registry*)
(asdf:oos 'asdf:load-op :cl-ppcre)
$ clisp -i run-ppcre
...
[1] (in-package :cl-ppcre)
[2]
Таким же образом скачивая и разархивируя пакеты, прописывая их пути в central-registry мы получаем самую простую из возможных систем - линейную.
2. Sym-links: CFFI
Часто бывает, что пакет имеет зависимости - например, чтобы поставить CFFI нужно поставить ещё три пакета. Чтобы узнать о зависимостях нужно посмотреть в cffi.asd часть :depends-on (alexandria trivial-features babel). Три перечисленых пакета зависят только друг от друга, их можно найти на CLiki. После скачивания всех четырех архивов, разархивируем их в папку с библиотеками:
$ wget http://common-lisp.net/project/cffi/releases/cffi_latest.tar.gz
$ wget http://common-lisp.net/~loliveira/tarballs/inofficial/alexandria-2008-07-29.tar.gz
$ wget http://common-lisp.net/project/babel/releases/babel_latest.tar.gz
$ wget http://common-lisp.net/~loliveira/tarballs/trivial-features/trivial-features_latest.tar.gz
$ tar zxvf cffi_latest.tar.gz
$ tar zxvf alexandria-2008-07-29.tar.gz
$ tar zxvf trivial-features_latest.tar.gz
$ tar zxvf babel_latest.tar.gz
Но теперь попробуем использовать символические ссылки:
$ cp -l ./cffi/cffi.asd ./asdf/cffi.asd
$ cp -l ./alexandria/alexandria.asd ./alexandria.asd
$ cp -l ./babel/cffi.asd ./babel.asd
$ cp -l ./trivial-features/cffi.asd ./trivial-features.asd
;; load-cffi.lisp
(load "/home/room/lisp/asdf/asdf.lisp")
(push (truename #P"/home/room/lisp/asdf/") asdf:*central-registry*)
(asdf:oos 'asdf:load-op :trivial-features)
(asdf:oos 'asdf:load-op :alexandria)
(asdf:oos 'asdf:load-op :babel)
(asdf:oos 'asdf:load-op :cffi)
Таким образом мы используем разветвленную иерархию репозитория, но параметризируем её линейно - только один путь входит в *central-registry*.
2. Clisp + Windows
То же самое можно сделать и для Windows систем с использование ярлыков вместо символических ссылок:
1. Установить Clisp штатным установщиком для Win
2. Создать директорию для ASDF и положить туда asdf.lisp
3. Вычислить в Clisp
(user-homedir-pathname)
т.е путь к домашней директории
4. Создать в домашней папке файл .clisprc.lisp - он загружается при старте Clisp'a. И наполнить его такими строками:
;; Определим пути, к примеру:
(defvar *asdf.lisp* #P"C:\\programs\\clisp\\asdf\\asdf.lisp")
(defvar *asdf-regs* #P"C:\\programs\\clisp\\asdf\\")
;; некий код
(load *asdf.lisp*)
(push (truename *asdf-regs*) asdf:*central-registry*)
;; ещё код
5. Наполнять папку *asdf-regs* = "C:\programs\clisp\asdf\ ярлыками на все .asd файлы, можно создать скрипт ярлычения...
Теперь выполнение
(asdf:oos 'asdf:load-op 'Whatever-you-want)
Должно привести к загрузке всего что вам угодно.
3. Сложный репозиторий (с SBCL)
Попробуем собрать репозиторий библиотек этак из 50 систем своими руками - это продемонстрирует необходимость в наличии некоторой более продвинутой системы установки.
Определим sbcl в /usr/local/bin/sbcl, а sbcl.core и все библиотеки sbcl-contrib в /usr/local/lib/sbcl/
При запуске sbcl из консоли или в сеансе Slime все стандартные библиотеки sbcl-contrib грузятся средствами require (правильнее было бы использовать load-op, но для SBCL это не существенно). Например можно про-мапить функцию require на список нужных библиотек:
;;; мапим require на библиотеки sbcl-contrib
;;; Но в принципе это не нужно!!!
(mapcar #'require
'(:asdf
:asdf-install
:sb-grovel
:sb-bsd-sockets
:sb-posix
:sb-rotate-byte
:sb-md5
:sb-cover
:sb-executable
:sb-simple-streams
:sb-queue
:sb-sprof
:sb-rt
:sb-cltl2
:sb-introspect ))
;;; для порядка можно и с помощью load-op:
(require :asdf)
(mapcar (lambda (system) (asdf:oos 'asdf:load-op system))
'(:asdf-install
:sb-grovel
:sb-bsd-sockets
:sb-posix
:sb-rotate-byte
:sb-md5
:sb-cover
:sb-executable
:sb-simple-streams
:sb-queue
:sb-sprof
:sb-rt
:sb-cltl2
:sb-introspect ))
Теперь условимся делить все прочие библиотеки CL по следующим признакам:
imps - имплементации
tools - расширения языка и базовые утилиты
streams - работа с потоками
crypto - криптография и кодировки
strings - обработка строк
system - интерфейс к ОС
gui - вобщем GUI
data - работа с данными различных форматов
crypto-2 - ещё криптография
net - сетевые протоколы
db - базы данных
ml - языки разметки
web - веб-фреймворки
test - тестовые вреймворки
это будут наши супер-системы - в директории /usr/local/lib/cl создадим все указанные поддиректории, а в них будем распаковывать обычные CL системы. Наша задача - просто взять wget-list, который может делать например clbuild, и пакетно всё скачивать и разархивировать в нужные папки, без переименования и всего прочего. В итоге получится большой репозиторий который можно заставить работать с помощью пользовательских функций поиска ASDF.
Ограничемся к примеру следующими системами:
tools
alexandria
arnesi
iterate
split-sequence
trivial-features
trivial-garbage
metabang-bind
closer-mop
streams
trivial-gray-streams
flexi-streams (trivial-gray-streams)
chunga (trivial-gray-streams)
odd-streams (trivial-gray-streams)
crypto (tools streams)
md5
cl-base64
trivial-utf-8
ieee-floats
net-telent-date
cl-unicode (flexi-streams)
babel (trivial-features alexandria)
strings (streams crypto)
cl-ppcre (flexi-streams)
cl-interpol (cl-unicode flexi-streams)
system (tools crypto)
cl-fad
bordeaux-threads
trivial-timers
local-time (cl-fad)
garbage-pools
clon (bordeaux-threads trivial-timers)
cffi (alexandria trivial-features babel)
iolib (alexandria trivial-garbage trivial-features babel bordeaux-threads cffi)
data (tools streams)
salza
salza2
zpb-ttf
zip (salza trivial-gray-streams flexi-streams)
cl-pdf (iterate salza2 zpb-ttf))
cl-typesetting (cl-pdf)
crypto-2 (streams system)
cl+ssl (trivial-gray-streams flexi-streams cffi)
ironclad
net (tools streams crypto crypto-2 system strings)
rfc2388
usocket (split-sequence)
hunchentoot (flexi-streams chunga cl-base64 md5 cl+ssl cl-fad bordeaux-threads cl-ppcre rfc2388 usocket)
puri
drakma (puri cl-base64 chunga flexi-streams usocket cl+ssl)
cl-recaptcha (drakma)
db (tools crypto system net)
postmodern (md5 usocket ieee-floats trivial-utf-8 closer-mop bordeaux-threads)
ml (tools system net streams)
cl-who
documentation-template (cl-who)
html-template
cl-libxml2 (cffi iterate puri flexi-streams alexandria garbage-pools metabang-bind)
cl-wbxml
test (tools)
fiveam (arnesi)
lift
Тут указаны зависимости как систем так и супер-систем. После скачивания и разахивировани (нужно набросать скрипт, или делать это по мере надобности) напишем в ~/.sbclrc
;;; Вспомогательные методы конкатенации
;;; Строк, символов, имен файлов.
;;; Между прочим полиморфные :)
(defmethod +. ((arg-0 string) &rest args)
(with-output-to-string (result)
(princ arg-0 result)
(dolist (arg args) (princ arg result))))
(defmethod +. ((arg-0 symbol) &rest args)
(values (intern (apply #'+. "" arg-0 args))))
(defmethod +. ((arg-0 pathname) &rest args)
(merge-pathnames
arg-0
(with-output-to-string (str)
(dolist (arg args) (princ arg str)))))
;;; простенький split
(defun split-string (string char)
(loop for i = 0 then (1+ j)
as j = (position char string :start i)
collect (subseq string i j)
while j))
;;;
;;; Directory walker из PCL
;;;
(defun component-present-p (value)
(and value (not (eql value :unspecific))))
(defun directory-pathname-p (p)
(and
(not (component-present-p (pathname-name p)))
(not (component-present-p (pathname-type p)))
p))
(defun pathname-as-directory (name)
(let ((pathname (pathname name)))
(when (wild-pathname-p pathname)
(error "Can't reliably convert wild pathnames."))
(if (not (directory-pathname-p name))
(make-pathname
:directory (append (or (pathname-directory pathname) (list :relative))
(list (file-namestring pathname)))
:name nil
:type nil
:defaults pathname)
pathname)))
(defun directory-wildcard (dirname)
(make-pathname
:name :wild
:type #-clisp :wild #+clisp nil
:defaults (pathname-as-directory dirname)))
(defun list-directory (dirname)
(when (wild-pathname-p dirname)
(error "Can only list concrete directory names."))
(let ((wildcard (directory-wildcard dirname)))
#+(or sbcl cmu lispworks)
(directory wildcard)
#+openmcl
(directory wildcard :directories t)
#+allegro
(directory wildcard :directories-are-files nil)
#+clisp
(nconc
(directory wildcard)
(directory (clisp-subdirectories-wildcard wildcard)))
#-(or sbcl cmu lispworks openmcl allegro clisp)
(error "list-directory not implemented")))
(defun file-exists-p (pathname)
#+(or sbcl lispworks openmcl)
(probe-file pathname)
#+(or allegro cmu)
(or (probe-file (pathname-as-directory pathname))
(probe-file pathname))
#+clisp
(or (ignore-errors
(probe-file (pathname-as-file pathname)))
(ignore-errors
(let ((directory-form (pathname-as-directory pathname)))
(when (ext:probe-directory directory-form)
directory-form))))
#-(or sbcl cmu lispworks openmcl allegro clisp)
(error "file-exists-p not implemented"))
(defun pathname-as-file (name)
(let ((pathname (pathname name)))
(when (wild-pathname-p pathname)
(error "Can't reliably convert wild pathnames."))
(if (directory-pathname-p name)
(let* ((directory (pathname-directory pathname))
(name-and-type (pathname (first (last directory)))))
(make-pathname
:directory (butlast directory)
:name (pathname-name name-and-type)
:type (pathname-type name-and-type)
:defaults pathname))
pathname)))
(defun walk-directory (dir &key (action #'print) (test (constantly t)))
(labels
((walk (name)
(cond
((directory-pathname-p name)
(dolist (x (list-directory name)) (walk x)))
((funcall test name) (funcall action name)))))
(walk dir)))
;;; Используя общий walk-directory, создаем функцию регистрации пользовательской
;;; функции рекурсивного поиска для директории. Мы бы должны еще ограничить глубину
;;; поиска, но...
(defun require-system (super-system)
(pushnew (lambda (system)
(let ((result nil))
(walk-directory super-system
:test
(lambda (f)
(let ((name (car (last (split-by-/ (write-to-string f))))))
(if (string= name (+. system ".asd\"")) t nil)))
:action
(lambda (f) (setf result f)))
result))
asdf:*system-definition-search-functions*))
(defvar *cl-lib-path* #p"/usr/local/lib/cl/")
;;; Этот код, выполняемый при загрузке, не делает особой работы
;;; он просто регистрирует функции поиска. Реальный поиск будет
;;; производится при require.
(mapcar #'require-system
(+. *cl-lib-path* "tools/")
(+. *cl-lib-path* "streams/")
(+. *cl-lib-path* "crypto/")
(+. *cl-lib-path* "strings/")
(+. *cl-lib-path* "system/")
(+. *cl-lib-path* "data/")
(+. *cl-lib-path* "crypto-2/")
(+. *cl-lib-path* "net/")
(+. *cl-lib-path* "db/")
(+. *cl-lib-path* "ml/")
(+. *cl-lib-path* "test/"))
;;; и в том же духе для других супер-систем,
;;; содержащих ASDF системы
Зайдём в SLIME и наберём:
(require 'iolib)
(require 'hunchentoot)
Что приведёт к тотальной и поголовной компиляции и загрузке :)
Рецепты
Более простой регистратор функций поиска
(in-package #:asdf)
(defvar *subdir-search-registry* '(#p"/my/lisp/libraries/"))
(defvar *subdir-search-wildcard* :wild)
(defun sysdef-subdir-search (system)
(let ((latter-path (make-pathname :name (coerce-name system)
:directory (list :relative
*subdir-search-wildcard*)
:type "asd"
:version :newest
:case :local)))
(dolist (d *subdir-search-registry*)
(let* ((wild-path (merge-pathnames latter-path d))
(files (directory wild-path)))
(when files
(return (first files)))))))
(pushnew 'sysdef-subdir-search *system-definition-search-functions*)
Процедура перекомпиляции "битых" fasl файлов
(defmethod asdf:perform :around ((o asdf:load-op) (c asdf:cl-source-file))
(handler-case (call-next-method o c)
(#+sbcl sb-ext:invalid-fasl
#+allegro excl::file-incompatible-fasl-error
#+lispworks conditions:fasl-error
#+cmu ext:invalid-fasl
#-(or sbcl allegro lispworks cmu) error ()
(asdf:perform (make-instance 'asdf:compile-op) c)
(call-next-method))))