(сказаное ниже относится к версии 2.019.2)

Это 2-ая статья цикла. Первая статья.

Центральное место в ASDF занимают две иерархии классов, одна с корневым элементом component, другая с корневым элементом operation:

<img src="/img/asdf-architecture.png" />

Для простоты изучения следует разделить систему (концептуально) на части, в которых прослеживается сильная связность в используемых сущностях и слабая связанность между ними. Этими частями могут быть:

  1. Определение систем: определяет систему обрабатывая *.asd файлы.
  2. Поиск систем: ищет системы в оперативной памяти и файлы *.asd в путях поиска.
  3. Загрузка систем (operate, load-system).

    3.1 Планирование операций: планирует последовательность операций с каждым зависимым компонентом.

    3.2 Выполнение операций: выполняет компиляцию, загрузку и пользовательские операции.

  4. Определение путей: определяет пути анализируя иерархию компонентов и вычисляет пути сохранения *.fasl файлов.

Ниже представлено какими ф-иями и методами реализуется каждая из подсистем:

  1. Определение систем:
    • defsystem
    • parse-component-form
    • union-of-dependencies
  2. Поиск систем:
    • find-system
    • compute-source-registry
    • resolve-location
  3. Загрузка систем (operate).
    • operate
    • make-sub-operation
    • find-component
    • Планирование операций:
      • traverse
        • do-traverse
        • do-dep
    • Выполнение операций:
      • perform-plan
      • perform-with-restarts
      • perform
  4. Определение путей

    • apply-output-translations
    • compute-output-translations
    • input-files
    • output-files
    • component-pathname

А вот наглядная схема:

<img src="/img/asdf-subsystems.png" />

Опишу вкратце логику работы:

Подсистема загрузки.

После того, как пользователь запросил загрузку системы, выполнив например (asdf:load-system :restas), подсистема загрузки обращается к методу OPERATE. Для поиска определений систем, этот метод обращается к подсистеме поиска. Затем, планируется необходимые операции соответствующей подсистемой и запланированые операции выполняются подсистемой выполнения операций. В процессе всего этого необходимо анализировать иерархию компонентов и создавать иерархию операций, для этого используются метод FIND-COMPONENT и ф-ия MAKE-SUB-OPERATION.

Подсистема поиска.

Активизирует подсистему поиска вызов метода FIND-SYSTEM. После активизации, подсистема пытается найти определние разыскивамой ASDF-системы в оперативной памяти и если ей это не удаётся, она пытается найти её на диске, используя для этого пути заданные в central-registry. Если опять не получилось она обращается к следующей возможности найти .asd файл. А именно, она использует для поиска *source-registry, которая перед первым обращением к ней инициализируется с использованием COMPUTE-SOURCE-REGISTRY. Эта ф-ия вычисляет пути руководствуясь (по усмолчанию) файлом source-registry.conf или директорией source-registry.conf.d, которые (опять же по умолчанию) в случае ОС Linux должны быть расположены в home/$USER.config/common-lisp/ директории. Внутри этой ф-ии, работает хитрый конвейр который обрабатывает некий мини-DSL предназначенный для очень гибкого, декларативного описания путей поиска. Ф-ия RESOLVE-LOCATION работает в недрах compute-source-registry и занимается непосредственно анализом значительной части этого самого мини-DSL.

Подсистема загрузки. Подсистема планирования операций.

После всех приключений с поисками системы, управление возвращается в подсистему загрузки. Далее вступает в работу "суб-подсистема" планирования операций - вызывается главный метод этой подсистемы TRAVERSE. Он не делает особо много работы: корректирует кое-какой слот, определяет ф-ию для сбора значений и готовится к приёму и обработке результата, который должен вернуть, возможно самый сложный метод asdf, метод DO-TRAVERSE. Главная его задача в том, чтобы ходить по иерархии компонентов системы и собирать операции, которые необходимо выполнить с компонентами. Для определения и сбора операций, которые необходимо выполнить с зависимостями компонентов служит ф-ия DO-DEP. Она делает неявный рекурсивный вызов do-traverse по пути делая кое-какую полезную работу (например, проверяет версию компонента).

Подсистема загрузки. Подсистема выполнения операций.

Итак операции запланированы, теперь начинает работать подсистема выполнения операций начиная с метода PERFORM-PLAN. Метод получает список запланированных операций и вызывает метод PERFORM-WITH-RESTARTS для каждой пары операций-компонент (список пар был возвращен методом traverse). Как видно из названия, метод устанавливает несколько рестартов: для перекомпиляции, рестарт позволяющий повторить операцию с компонентом, а также рестарт позволяющий считать операцию выполненой. Далее perform-with-restarts передаёт управление методу PERFORM, которая, наконец уже, выполняет операцию с компонентом. Если необходимо добавить какую-то дополнительную обработку во время компиляции и/или загрузке компонентов, то как правило добавляют методы (основные и декораторы) имено к обобщёной ф-ии perform. По умолчанию имеются только два декоратора:

  • :before - гарантирует существование директории для последующего сохранения скомпилированного файла.
  • :after - сохраняет в компоненте текущую метку времени, чтобы отметить завершение операции. При выполнении операций компиляции и загрузки, методам требуется вычислять абсолютные пути к исходным и скомпилированным файлам, для этого используется подсистема определения путей.

Подсистема определения путей.

Один из главных методов, отвечающих за вычисление путей для операций это - INPUT-FILES. Если требуется вычислить путь к исходнику, то этот метод вызывает метод COMPONENT-PATHNAME. Принцип работы метода заключается в прохождения пути от компонента, до его самого старого предка и собирания имен, встречающихся на пути компонентов, для формирования абсолютного пути. Если требуется вычислить путь для будущего скомпилированного файла, то input-files вызывает метод OUTPUT-FILES. Он также использует метод component-pathname для получения абсолютного пути к исходнику но при этом, дополнительно обрабатывает путь с помощью ф-и APPLY-OUTPUT-TRANSLATIONS. Обработка происходит с использованием внутренней специальной переменной output-translations, которая инициализируется перед первым использованием. За инициализацию отвечает ф-ия COMPUTE-OUTPUT-TRANSLATIONS, она работает подобно ф-ии compute-source-registry - это тоже конвейрная обработка с анализом мини-DSL'а для определения путей.

Подсистема определения систем.

Для планирования операций и определения путей, компоненты системы и их зависимости организуются в иерархию. Для построение этой иерархии применяется всем знакомый макрос DEFSYSTEM. Да, это тот самый defsystem который используется в *.asd файлах. На данный момент, он ничего не делает а просто передаёт управление ф-ии do-defsystem. Основная рабочая лошадка в do-defsystem это ф-ия PARSE-COMPONENT-FORM. Она разбирает слегка подкорректированный в do-defsystem древообразный список опций. Для разрешение зависимостей и переопределения порядка операций с этими завимостями используется ф-ия UNION-OF-DEPENDENCIES совместно со слотами in-order-to и do-first компонентов.