Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Contingent: Динамическая система сборки

Оригинал: Contingent: A Fully Dynamic Build System
Авторы: Brandon Rhodes, Daniel Rocco
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: январь 2017 г.

Системы сборки и согласованность

Проблема, описанная выше, характерна не только для системы Sphinx. Мало того, что она присутствует в других системах, таких как LaTeX, она может поразить даже такие проекты, в которых в случае, если в них определенным образом сформировать перекрестные ссылки, с помощью почтенной утилиты make выполняется обычная компиляция.

Поскольку проблема является старой и всеобъемлющей, ее решение также уже давно известно:

   $ rm -r _build/
   $ make html

Если вы удалите все выходные данные, то вы гарантируете полную пересборку! В некоторых проектах для команды rm -r даже есть алиас, который называется clean, так что для того, чтобы выполнить согласованную пересборку, достаточно лишь запустить команду make clean.

За счет того, что стираются все копии всех промежуточных или результирующих данных, команда rm -r может заставить выполнить сборку с самого начала и не использовать закешированные данные — не использовать запомненное в памяти предыдущее состояние, что могло бы привести к неправильным результатам.

Но можно ли разработать более лучший подход?

А что если ваша система сборки пользуется долговременным хранилищем данных (например, базами данных — прим.пер.) и при пересборке результата оттуда берутся названия всех глав, всех разделов и и все перекрестные ссылки, которые могут быть между отдельными фразами? Желательно, чтобы решение относительно того, следует ли пересобирать другие документы после изменения в одном исходном файле, было бы точным, а не полагалось на простых догадках, и правильным, а не выдавало бы нам несогласованные данные.

Можно было бы воспользоваться системой, похожей на старый добрый статический make, но которая бы анализировала зависимости между файлами по мере того, как они пересобирались, и которая бы динамически добавляла и удаляла зависимостей по мере того, как добавлялись, обновлялись или удалялись перекрестные ссылки.

В далее в настоящей главе мы сконструируем на языке Python такую систему, которая будет называться системой Contingent. Система Contingent гарантирует корректность сборки при наличии динамических зависимостей и при этом выполняет минимально возможное количество действий по пересборке документов. Хотя ее можно использовать в любой предметной области, мы будем ее применять для решения одного из вариантов маленькой задачи, которая была описана выше.

Соединяем задачи и создаем граф

В любая система сборки нужен способ связывать между собой входные и выходные данные. Например, в нашем обсуждении, приведенном выше, по каждому из трех текстовых файлов с разметками создается соответствующий выходной файл в формате HTML. Самым естественным способом представить эти отношения в виде наборов прямоугольников и стрелок или, в математической терминологии, вершин и ребер, т.е. сформировать граф (рис. 4.1).

Рис.4.1. По результатам анализа трех входных файлов создаются три результирующи файла.

В каждом из языков, на котором программист может решить написать систему сборки, будут предлагаться различные структуры данных, с помощью которых можно будет представить такой граф, состоящий из вершин и ребер.

Как мы бы могли представить такой граф на языке Python?

В языке Python приоритет отдается четырем базовым структурам данных поскольку они обеспечены непосредственной поддержкой в самом синтаксисе языка. Вы можете создавать новые экземпляры этих больших-четырех структур данных просто набрав в вашем исходном коде их литеральное представление и эти четыре типа объектов доступны в виде встроенных символов, которые можно использовать не импортируя.

Тип кортеж (tuple) представляет собой последовательность элементов, которую можно только читать и элементы которой используются для хранения разнородных данных; каждый элемент в кортеж обычно означает что-то другое. Ниже кортеж объединяет имя хоста и номер порта, и он становится бессмысленным в случае, если элементы будут перепределены:

('dropbox.com', 443)

Тип список (list) является изменяемой последовательностью, используемой для хранения данных одного и того же типа; каждый элемент этого типа, как правило, имеет ту же самую структуру и смысл, как все остальные элементы. Списки можно использовать либо для хранения данных в их исходном порядке, либо их можно переупорядочить или отсортировать для создания новой и более удобной в использовании последовательности данных.

['C', 'Awk', 'TCL', 'Python', 'JavaScript']

В типе данных множество (set) понятие порядок отсутствует. В множестве запоминается только то, было ли добавлено данное значение, а не то, сколько раз оно было добавлено, и, следовательно, их можно рассматривать как структуры данных, предназначенные для удаления дубликатов из потока данных. Например, в следующих двух множествах будет по три элемента:

{3, 4, 5}
{3, 4, 5, 4, 4, 3, 5, 4, 5, 3, 4, 5}

Тип данных словарь (dict) представляет собой ассоциативную структуру данных, предназначенную для хранения значений, доступных с помощью ключа. Словари позволяют программисту выбрать ключ, с помощью которого индексируется каждое значение, а не использовать для этого автоматическую целочисленную индексацию, которая имеется в кортежах и списках. Поиск поддерживается с помощью хэш-таблицы, а это значит, что поиск по ключу работает с той же самой скоростью независимо от того, хранится ли в словаре десяток или миллион ключей.

{'ssh': 22, 'telnet': 23, 'domain': 53, 'http': 80}

Ключевой особенностью языка Python является то, что эти четыре структуры данных можно объединять друг с другом любым образом. Программист может произвольным образом их вкладывать внутри друг друга для того, чтобы создавать более сложные хранилища данных, правила работы с которыми и их синтаксис остаются простыми как и у данных, лежащих в их основе - кортежей, списков, множеств и словарей.

Учитывая, что в каждом из наших ребер графа нужно знать, по крайней мере, его начальный узел и его конечный узел, то простейшим представлением будет кортеж. Верхнее ребро, изображенное на рис.4.1 может выглядеть следующим образом:

    ('tutorial.rst', 'tutorial.html')

Как можно запомнить несколько ребер? Хотя нашим первоначальный решением может быть просто сохранить все наши кортежи с ребрами в списке, оно будет иметь недостатки. Список чувствителен к порядку хранящихся в нем элементов, но о порядке ребер в графе говорить абсолютно не имеет смысла. И в списке можно без всяких проблем хранить несколько копий одного и того же ребра несмотря на то, что нам нужно, чтобы можно было нарисовать только одну стрелку между файлами tutorial.rst и tutorial.html. Поэтому правильным выбором будет множество, которое для рис.4.1. будет представлено как:

    {('tutorial.rst', 'tutorial.html'),
     ('index.rst', 'index.html'),
     ('api.rst', 'api.html')}

Такое представление позволяет быстро перебирать все ребра графа, быстро выполнять обреации добавления и удаления каждого и иметь возможность быстро проверить, имеется или отсутствуеь то или иное ребро.

К сожалению, это не единственные операции, которые нам нужны.

Система сборки, такая как Contingent, должна уметь определять связь между данной вершиной и всеми вершинами, подсоединенными к нему. Например, когда изменяется файл api.rst, система Contingent для того, чтобы свести к минимуму выполняемую работу и, в то же время, гарантировать полную пересборку, должна знать, какие другие вершины, если таковые имеются, зависят от этого изменения. Чтобы ответить на этот вопрос, на какие вершины влияет изменение вершины api.rst, нам нужно найти ребра, исходящие (outgoing) из вершины api.rst.

Но создание графа зависимостей потребует от системы Contingent также уметь работать со входящими вершинами (inputs). Например, какие входные вершины были использованы, когда система сборки пересобирала результирующий документ tutorial.html? Благодаря тому, что система Contingent может определить входные ребра для каждой вершины, она может узнать, что файл api.html зависит от файла api.rst, а файл tutorial.html от него не зависит. По мере того, как меняются файлы с исходным кодом и возникает необходимость в пересборке, система Contingent заново перебирает входящие ребра каждой измененной вершины с тем, чтобы удалить потенциально устаревшее ребро, а также снова определить, какие на это раз ресурсы использует задача.

Наше множество, состоящее из кортежей, не дает простого ответа на любой из этих вопросов. Если нам потребуется знать о взаимосвязи между файлом api.html и остальной частью графа, то нам нужно будет пройти по всем множеству в поисках ребер, которые могут начинаться или заканчиваться в вершине api.html.

Ассоциативная структура данных, такая как словарь dict языка Python, позволила бы упростить все эти хлопоты за счет прямого поиска всех ребер, отходящих от конкретной вершины:

    {'tutorial.rst': {('tutorial.rst', 'tutorial.html')},
     'tutorial.html': {('tutorial.rst', 'tutorial.html')},
     'index.rst': {('index.rst', 'index.html')},
     'index.html': {('index.rst', 'index.html')},
     'api.rst': {('api.rst', 'api.html')},
     'api.html': {('api.rst', 'api.html')}}

Поиск ребер для конкретной вершины теперь выполнялся бы невероятно быстро за счет того, что каждое ребро запоминается дважды: один раз в наборе входящих ребер, и один раз в наборе исходящих ребер. Но для того, чтобы увидеть, какие ребра являются входящими и какие — исходящими, их в каждом наборе потребуется просматривать дополнительно. Кроме того, немного лишним будет повторяющееся запоминание названия вершине в его множестве ребер.

Решение обоих этих недостатков заключается в размещении входящих и исходящих ребер в своих собственных отдельных структурах данных, которые также освобождают нас от повторного запоминания названий вершин для каждого из ребер, в которых они присутствуют.

    incoming = {
        'tutorial.html': {'tutorial.rst'},
        'index.html': {'index.rst'},
        'api.html': {'api.rst'},
        }

    outgoing = {
        'tutorial.rst': {'tutorial.html'},
        'index.rst': {'index.html'},
        'api.rst': {'api.html'},
        }

Обратите внимание на то, что структура с исходящими ребрами outgoing представлена непосредственно с помощью синтаксиса языка Python, причем именно так, как мы это выше изобразили на рис.4.1: исходные документы, изображаемые в левой части, будут преобразованы системой сборки в выходные документы, показанные в правой части. В данном простом примере для каждого исходного документа указывается только один выдаваемый документ - все выходные наборы данных имеют только по одному элементу - но скоро мы покажем примеры, когда один входной узел может иметь несколько результирующих вершин.

Каждое ребро в этой структуре данных, представляющей собой словарь из множеств, действительно представлено дважды, один раз как ребро, исходящее от этой вершины (tutorial.rst → tutorial.html) , и снова как ребро, входящее в этот узел (tutorial.html ← tutorial.rst). Эти два представления описывают одно и то же соотношение, но если его рассматривать с противоположных точек из двух вершин, расположенных на различных концах ребра. Но в обмен на эту избыточность, структура данных поддерживает быстрый поиск, что необходимо в системе Contingent.

Перейти к следующей части статьи.

Перейти к началу статьи.