Регистрация | Войти
Lisp — программируемый язык программирования

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)
)
)
)

@2009-2013 lisper.ru