Как было замечено ранее, ASDF выполняет 2 связанные, но все же довольно разные по требованиям со стороны пользователей функции: описание систем и управление их сборкой. В этой статье я постараюсь перечислить распространенные (и не очень) шаблоны его применения для этих задач, собранные мной из более 70 open-source Lisp библиотек, с которыми приходилось работать. Я думаю, что систематизация этих знаний сослужит хорошую службу Lisp-сообществу и будет полезна как начинающим, так и экспертам, которые смогут усовешенствовать свои техники.
Изначально в этой статье я планировал разобрать все основные шаблоны использования ASDF, но материала оказалось слишком много, поэтому более сложным темам его применения как build-инструмента будет посвящена следующая серия.
Прямые ссылки на полезные вещи:
- новый проект c помощью ASDF за 2 минуты
- пакет для defsystem-формы
- правильные символы в именах систем и пакетов
- имена и пути компонент
- платформо-зависимые описания систем
Начало работы с ASDF
ASDF нельзя назвать инструментом, который можно освоить за 5 минут. Я сам, например, очень долго вникал в идеологию и особенности его работы, и делал это в основном методом проб и ошибок (мануал мне плохо помогал :). Однако, сейчас мне кажется, что эти трудности связанны не с особенностями ASDF (какими-то неудачными архитектурными решениями и т.п.) или Lisp'а, а, в первую очередь, с отсутствием должной документации и best-practices. Именно этот пробел я и хочу заполнить. Да, всё, что будет показано ниже, почерпнуто из широкодоступного кода Lisp-библиотек, однако часто нужны скорее tutorial'ы, которые позволят быстро начать работу и сразу получить ожидаемый результат. Поэтому начнем именно с такого самого простого примера.
Итак, чтобы создать новый проект с помощью ASDF — нужно создать директорию проекта (пусть будет
testprj
), в которой поместить имеющиеся lisp-исходники (пусть это будет файл app.lisp
) и создать в этой директории файл testprj.asd
, в который поместить следующую форму:(asdf:defsystem #:testprj
:components ((:file "app")))
Для загрузки нового проекта в Lisp-среду нужно, чтобы наш testprj.asd
файл был в известных ASDF местах. На данный момент поддерживается 2 способа задания таких мест: аналог PATH (*central-registry*
) и source-registry
(использующий конфигурационные файлы). Самый распространенный подход (в POSIX-окружении) — это создать ссылку на asd-файл в какой-то специальной директории (например, ~/.lisp
), которая добавлена в *central-registry*
.В общем полная настройка такого варианта выглядит так:
$ ln -s <path-to-testprj.asd> ~/.lisp/testprj.asd
;; обычно это делается в rc-файле lisp'а (например, .sbclrc)
CL-USER> (push "~/.lisp/" asdf:*central-registry*)
;; далее можно выполнять (для ASDF 2):
CL-USER> (asdf:load-system :testprj)
;; или для любой версии ASDF:
CL-USER> (asdf:oos 'asdf:load-op :testprj)
;; или в большинстве окружений (например, SBCL), даже:
CL-USER> (require :testprj)
Вот и всё.Другие формы в ASD-файле
На самом деле, ASD-файл — это обычный lisp-исходник, просто с другим расширением, что помогает найти его ASDF'у, поэтому в нем могут находится и другие lisp-формы. Впрочем, прежде, чем помещать туда произвольные формы, стоит очень хорошо подумать. Во всяком случае, ASD-файл следует сохранить полностью декларативным, поскольку большинство инcтрументов, созданных вокруг ASDF, должны иметь возможность просто читать этот файл, не исполняя.
Пакет для defsystem
Единственный класс форм, размещение которых в ASD-файле необходимо (помимо
defsystem
и некоторых других ASDF-специфичных форм, о которых будет сказано далее) — это формы работы с пакетом.Сейчас в Lisp-сообществе есть 2 конкурирующих взгляда на то, как это делать:
- простой — определять системы в пакете ASDF
(in-package :asdf)
- скурпулезный — определять отдельный пакет только для ASDF-описания системы:
;; Пример из описания системы ARCHIVE:
(defpackage :archive-system (:use :cl :asdf))
(in-package :archive-system)
#:testprj
или :testprj
). Единственным случаем, когда вариант отдельного пакета может оказаться препочтительнее — это какие-то очень сложные описания систем с зависимостями от дополнительных пакетов. Впрочем, это верный признак того, что вы делаете что-то не так и, просто, не пользуетесь всеми возможностями ASDF.Использование символов
Имена ASDF-систем в форме
defsystem
, как и CL пакетов в defpackage
, можно задавать несколькими способами:- символом:
(defsystem testprj ...)
- кивордом:
(defsystem :testprj ...)
- неинтернированным символом:
(defsystem #:testprj ...)
- строкой:
(defsystem "TESTPRJ" ...)
Вариант строки, в принципе, эквивалентен, но не эстетичен, киворда — приводит к "засорению" пакета keywords,— а просто символа — подвержен риску конфликта имен (его точно не стоит использовать, если описывать систему внутри ASDF-пакета).
Задание зависимостей
Я бы сказал, что ключевой функцией ASDF является разрешени зависимостей между исходными файлами в рамках одной системы и между разными системами. Все зависимости в ASDF задаются ключевым словом
:depends-on
в описании соответствующего компонента (файла, модуля, системы и т.д.)Зависимости от других систем
Разумеется, любая серьезная система существует не в вакууме, а использует множество других библиотек. Несмотря на миф об их отсутствии в lisp-экосистеме :), большие проекты, над которыми мне приходилось работать, как правило, использовали порядка 20-30 сторонних библиотек (включая рекурсивные зависимости).
Предположим, что наш проект использует библиотеку утилит
RUTILS
. В таком случае нам нужно немного расширить его описание:(asdf:defsystem #:testprj
:depends-on (#:rutils)
:components ((:file "app")))
Опять же для задания имени зависимости мы используем неинтернированный символ. ASDF позволяет дать и более точное описание такой зависимости (которое пока используется крайне редко, и о котором в отдельной статье, посвященной версиям).Зависимости между файлами
Если в проекте больше одного lisp-файла, то стандартной практикой является добавления файла
packages.lisp
, в котором описываются 1 или несколько пакетов, которые будут использоваться (создавать любой неигрушечный проект в рамках cl-user
пакета строго не рекомендуется :).Предположим, что помимо
app.lisp
, мы также используем файл support.lisp
. В таком случае наше описание приобритет следующую форму:(asdf:defsystem #:testprj
:depends-on (#:rutils)
:components ((:file "packages")
(:file "support")
(:file "app")))
Однако мы не задалт порядок загрузки отдельных файлов, что может привести к неожиданным результатам (скажем, Lisp будет ругаться на форму (in-package #:testprj)
в файле support.lisp
, поскольку файл packages.lisp
еще не загружен. Поэтому эту форму нужно доработать:(asdf:defsystem #:testprj
:depends-on (#:rutils)
:components ((:file "packages")
(:file "support" :depends-on "packages")
(:file "app" :depends-on "support")))
Тут мы не пишем, что app
зависит от packages
, поскольку зависимости транзитивны.Такой последовательный (serial) вариант зависимостей характерен для доброй половины проектов, поэтому для него есть специальная декларация для
defsystem
: :serial t
. С ней наше описание снова упростится:(asdf:defsystem #:testprj
:depends-on (#:rutils)
:serial t
:components ((:file "packages")
(:file "support")
(:file "app")))
В основе ASDF лежит расширяемая объектная модель компонентов и операций над ними. Некоторые разработчики используют в описании систем прямое имя класса :cl-source-file
, а не :file
. А вот класса file
в ASDF как раз нет: конкретный класс определяется из слота default-component-class
модуля (система — потомок модуля) и по умолчанию, конечно, является как раз lisp-исходником.Помимо этого описан ряд других вариантов, компонент, а также всегда можно описать свой собственный (об этом — в следующей статье).
Например, интересной практикой является подобная декларация компонента:
:components ((:static-file "cl-oauth.asd")
...
(static-file
— это любой статичный файл, который не обрабатывается компилятором, например файл лицензии или, как в данном случае, собственно файл описания системы — ведь он обрабатывается только ASDF. Зачем его добавлять? Например, если мы расчитываем, что будем реализовывать какую-нибудь операцию, типа publish-op
, для создания дистрибутива из исходных файлов).Модули
Модули ASDF — это логические компоненты системы, которые объединяют несколько других компонент. Использование модулей позволяет решить 2 задачи:
- аггрегированно управлять зависимостями
Скажем, у нас есть 2 части системы: бэкенд и фронтенд, которые зависят от общего файла утилит. И при изменении каждой из них мы не хотим перекомпилировать другую. В таком случае логично будет описать каждую часть в виде отдельного модуля - распределить исходники по разным директориям (в какой-то степени это аналог модулей в Python, но без управления видимостью — об этом в следующей статье)
(defsystem :arnesi
...
:components
((:module :src
:components ((:file "accumulation"
:depends-on ("packages" "one-liners"))
(:file "asdf" :depends-on ("packages" "io"))
(:file "csv" :depends-on ("packages" "string"))
(:file "compat" :depends-on ("packages"))
(:module :call-cc
:components ((:file "interpreter")
(:file "handlers")
(:file "apply")
(:file "generic-functions")
(:file "common-lisp-cc"))
:serial t
:depends-on ("packages" "walk"
"flow-control" "lambda-list"
"list" "string"
"defclass-struct"))))
...))
:pathname
, который позволяет явно задать путь к нему. Однако его использование имеет свои особенности — об этом дальше.Имена компонент и путь к ним
У любого ASDF-компонента есть обязательный аттрибут имя, который используется при его поиске и разрешении зависимотстей. Однако этот аттрибут *не задается* декларацией
:name
в описании компонента.(defsystem :ch-image
:name "ch-image"
...)
В этом коде :name "ch-image"
несет чисто эстетический смысл (не верите? :)Имя задается первым символом в декларации компонента: в данном случае
ch-image
, или же в случае модуля выше — :src
, или же "accumulation"
для файла там же. Все внутренние функции ASDF умеют работать как с символьным, так и со строковым представлением имен, описанных выше.Кроме того, у каждого компонента есть аттрибут
pathname
, который определяет его положение в файловой системе. Однако, в отличие от имени, его как раз можно задать соответствующей декларацией. Например, этот пример задает относительный собственно ASD-файла путь к модулю io.multiplex
библиотеки IOLIB
: :pathname (merge-pathnames #p"io.multiplex/" *load-truename*)
.Если же путь не задавать явно, то он вычисляется из имени, расширения (которое определяется типом компонента) и положения в иерархии модулей. Таким образом для примера задания модуля в
arnesi
(выше) для компонента :file "interpreter"
будет вычислен такой путь: src/call-cc/interpreter.lisp
.Ну и ответ на вопрос, как разбросать файлы по директориям, не используя модули: задать для всех файлов явный
:pathname
.Мета-информация
Defsystem
-форма позволяет задать большое количество полезных метаданных для системы. Очень важно указать как минимум следующие::version
, например:version "0.0.1"
(подробнее о версиях — в отдельной статье):author
или:maintainer
(с указанием e-mail'а, чтобы к вам впоследствии смогли обратиться и предложить миллионы за доработку и поддержку вашей прекрасной библиотеки :):licence
— чтобы люди знали, как они могут пользоваться вашими поделками
Платформо-зависимое описание систем
CL предоставляет исключительно удобный механизм условной компиляции и выполнения кода (
#+/#-
). И как раз в описаниях систем он, разумеется, находит широкое применение:- предотвращение загрузки системы в целом — тут интересны примеры из двух альтернативных библиотек для FFI:
#+(or allegro lispworks cmu openmcl digitool cormanlisp sbcl scl)
(defsystem uffi ...)
;; CFFI: этот вариант, безусловно, правильнее, чем просто тихо ничего не сделать
#-(or openmcl sbcl cmu scl clisp lispworks ecl allegro cormanlisp)
(error "Sorry, this Lisp is not yet supported. Patches welcome!") - вынесение функций, зависящих от конкретной lisp-среды в отдельные файлы — пример из все того же CFFI:
:components (#+openmcl (:file "cffi-openmcl")
#+sbcl (:file "cffi-sbcl")
#+cmu (:file "cffi-cmucl")
#+scl (:file "cffi-scl")
#+clisp (:file "cffi-clisp")
#+lispworks (:file "cffi-lispworks")
#+ecl (:file "cffi-ecl")
#+allegro (:file "cffi-allegro")
#+cormanlisp (:file "cffi-corman") - закладка нескольких вариантов построения библиотеки, в зависимсоти от каких-то условий. Тут проще всего привести примеры:
;; использовать ли acl-regexp2-engine?
(defsystem :cl-ppcre
:version "2.0.3"
:serial t
:components ((:file "packages")
(:file "specials")
(:file "util")
(:file "errors")
(:file "charset")
(:file "charmap")
(:file "chartest")
#-:use-acl-regexp2-engine
(:file "lexer")
#-:use-acl-regexp2-engine
(:file "parser")
#-:use-acl-regexp2-engine
(:file "regex-class")
#-:use-acl-regexp2-engine
(:file "regex-class-util")
#-:use-acl-regexp2-engine
(:file "convert")
#-:use-acl-regexp2-engine
(:file "optimize")
#-:use-acl-regexp2-engine
(:file "closures")
#-:use-acl-regexp2-engine
(:file "repetition-closures")
#-:use-acl-regexp2-engine
(:file "scanner")
(:file "api")));; какие бэкенды генерации графических файлов доступны?
(defsystem :ch-image
...
(:module
:io
:components
(#+ch-image-has-tiff-ffi
(:cl-source-file "tiffimage")
#+ch-image-has-cl-jpeg
(:cl-source-file "jpegimage")
#+(and ch-image-has-zpng)
(:cl-source-file "pngimage")
(:cl-source-file "imageio"
:depends-on (#+ch-image-has-tiff-ffi
"tiffimage"
#+ch-image-has-cl-jpeg
"jpegimage")))
:depends-on (:src))
;; :name не имеет значения
CL-USER> (defsystem :ch-image
:name "ch-image1")
#<SYSTEM "ch-image" {AA7A911}>
CL-USER> (describe *)
#<SYSTEM "ch-image" {AA7A911}>
[standard-object]
Slots with :INSTANCE allocation:
NAME = "ch-image"
VERSION = #<unbound slot>
IN-ORDER-TO = NIL
DO-FIRST = ((COMPILE-OP (LOAD-OP)))
INLINE-METHODS = NIL
PARENT = NIL
RELATIVE-PATHNAME = #P"/home/vs/lib/lisp/ch-image_0.4.1/"
OPERATION-TIMES = #<HASH-TABLE :TEST EQL :COUNT 0 {AA7AB61}>
PROPERTIES = NIL
COMPONENTS = NIL
IF-COMPONENT-DEP-FAILS = :FAIL
DEFAULT-COMPONENT-CLASS = NIL
DESCRIPTION = #<unbound slot>
LONG-DESCRIPTION = #<unbound slot>
AUTHOR = #<unbound slot>
MAINTAINER = #<unbound slot>
LICENCE = #<unbound slot>