[О блоге] [наверх] [пред] [2024-02-18 10:41:48+03:00] [7e1dbd0539c7ea5c6bd5e8831abeea4796da693e]
Темы: [bsd][djb][redo][zsh]

Новый проект: zwoki

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

У нас работе нет devops-ов, нет достаточного кол-ва админов, вообще
народу не шибко много. Поэтому если хочешь какую-то систему CI для
сборок -- ну бери и делай. Виртуальные машины или железо тебе выделят, а
вот дальше возись сам. И у нас было n-ое кол-во BuildBot установок.
Часть из них уже никем не поддерживается и из-за изменений
инфраструктуры и не в рабочем состоянии. Что-то на них подправить никто
не знает как. На некоторых, к тому же, всё установлено через Nix, к
которому мало у кого есть охота изучения. Даже те, кто прежде его
использовал активно -- плюнули, из-за постоянных изменений в upstream-е,
требующим постоянное обновление правил сборки (либо сидеть на версиях
годовалых давностей и обновлять только собственные пакеты).

Какие CI системы я знаю? В живую имел дело с Jenkins и BuildBot. Первый
-- на Java и монструозен. Просто воспоминания о том как возиться с его
правилами сборки -- отталкивают от одной мысли его использовать. Второй
написан на Python. А это означает: ee3156341baf276877e601325bb9555ce5743fb1
что хрен его поставишь, не скачивая руками все его зависимости и вручную
подсовывая в virtualenv. Если авторы не предоставили vendored
зависимости (чего конечно же из Python разработчиков штатно никогда не
делает (а вот в моём PyDERASN, кстати, все зависимости приложены в
tarball)), то это просто неуважение к своему времени и силам в попытках
это всё развернуть. Кроме того, современные версии BuildBot имеют WebUI
требующий JavaScript -- поэтому если и ставить, то старьё.

И задался вопросом: насколько это сложная задача (CI) и нельзя ли
написать своё, раз нету готового, достаточно простого и удобного?
Честно, то, что я постоянно за последние годы пишу какой-то софт как
(sane) альтернативу имеющемуся -- самого бесит и раздражает. Но что
поделать, если много софта не удовлетворяет, а переписывать выходит?

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

Я хотел было сделать что-то похожее на BuildBot. Раздаются задачи,
выполняются, отчитываются о каждом шаге, логируют, показывают кто там
упал. И всё это проецируется в некий HTML dashboard со сводкой. Но сразу
и требования родились:

* оно должно работать на разных платформах: как минимум GNU/Linux
  (Astra, Debian) и FreeBSD
* в идеале, slave выполняющий задачи, должен быть как можно более
  минималистичной ОС из коробки. Вот поставили голую FreeBSD или Astra
  -- и вот она уже должна мочь выполнять задачи раздаваемые. Чтобы можно
  было быстро вводить в строй новые машины для сборок

BuildBot никоим образом не поможет с последним требованием. Если для
сборки нужен PostgreSQL например -- ну или добавляй его сборку/установку
в шаги сборки проекта, или ставь прямо в саму систему slave-а. Последним
админы BuildBot-ов и занимались. Если появляется новый проект с совсем
иными требованиями (например для сборки многого моего софта нужен redo),
то это ставится в slave-ы вручную. Ну или через Nix, Ansible, Puppet,
whatever. Но не средствами BuildBot.

Нужно что-то, что позволяет собирать и устанавливать "пакеты". Хотелось
бы, чтобы сборка проекта на первом шаге просто сказала что мне нужен
"python", "postgres" и это как-то в эту сборку подсунулось. Да, примерно
таким и Docker может заниматься. Nix тот же. Но Nix поддерживает только
GNU/Linux (хотя когда-то давно была поддержка и BSD систем). Docker...
спасибо, но нет, плюс его вроде бы и нет на FreeBSD (лень проверять, как
и возиться с ним).

Поэтому первым шагом я начал писать свой пакетный менеджер по сути.
Правила сборки описываются в shell скрипте, который устанавливает
программу в /well/known/permanent/path/hash-progname. А установка
пакета, которая на практике будет происходить в некой $tmp директории
сборки -- это просто вызов stow для создания symlink-ов из /well/known
пути до программы в $tmp/local поддиректорию. Добавив $tmp/local/bin в
$PATH, директории до библиотек и прочего -- можно удобно заиметь в своём
окружении нужный софт. Всё это очень сильно напоминает то, что делает
Nix. Так и есть. Так вышло, что я самостоятельно к этому пришёл. Но
напомню, что Nix не кроссплатформенный и в нём уродский собственный
функциональный язык для описания сборок. У меня же буквально shell
скрипты выполняющие всё нужное по шагам. Можно ли сторонней программе
легко и точно сказать от кого зависит тот, или иной пакет при сборке? В
Nix можно, а у меня нельзя. Ну и где это создаст проблем? В том то и
дело, что нигде. Как в redo: не запустив .do файлов -- никто не знает от
чего они зависят. Здесь аналогично. Зато никакого нового языка, нового
формата -- пиши как и на чём хочешь.

Установка в /well/known/... путь на любой ОС (GNU, BSD) и создание
$tmp/local stowed директории -- работает одинаково. Если есть
особенности сборки для заданной архитектуры, то внутри правил сборки
всегда есть $ARCH переменная и поэтому можно писать if [ $ARCH = ... ].

Пакет устанавливаться должен из единичного файла. Так я захотел, для
удобства. Думал что можно обойтись просто созданием .tar-а с
hash-progname-version директорией, которую распаковывать в
/well/known/..., но понадобилась возможность хранить run-dependencies
(для работы stow нужен perl, для tmux нужен libevent, и т.д.), точное
имя пакета (чтобы сам пакет имел имя progname-version.tar), информация о
сборке (из какого коммита взяты правила, как минимум).

Поэтому начал рождаться и собственный формат пакетов. Я сначала подумал
и придумал, а только потом пошёл смотреть что из себя представляют
пакеты в Debian, RedHat, Gentoo, и т.д.. Оказалось, что я полностью
схожим образом пришёл к точно такому же решению как и в Gentoo:
https://www.gentoo.org/glep/glep-0078.html
Архив с собранным пакетом находится внутри другого архива, в котором
метаинформационные файлы: name, rundeps, buildinfo, и т.д.. К каждому
файлу можно приложить .blake3 какой-нибудь, .sig/.asc подпись. Очень
просто и удобно. Главное следить за тем, чтобы метаинформационные файлы
находились в начале архива, чтобы до них можно бы было быстро
достучаться при потоковой работе:
    curl http://pkg/progname-version | tar xfO - name | read name_with_hash
Архив с программой, конечно же, может быть (и должен быть!) пожат.
    curl http://pkg/progname-version | tar xfO - bin |
        tar xfC - /well/known/$name_with_hash
Ну а через tee и заранее вычитанные .blake3 файлы можно проверить и
целостность на лету. Внешний архив не стоит сжимать, чтобы можно было бы
быстрее производить поиск по нему.

Машина для сборки пакетов из исходников не должна быть подключена к
Интернету. Может, но не обязана. Поэтому я давно за практику взял
разделение шагов скачивания исходного кода и его сборки. Для скачивания
нужны git, wget и всякое такое. Для сборки они, как правило, не нужны.
На одной машине можно запустить скрипты для скачивания, а на другой уже
использовать результат сформированный в distfiles.

Я не раз писал скрипты скачивания, в которых указывается URL до
tarball-а и криптографический хэш для его "аутентификации". Плюс
скачивал и добавлял в репозиторий и файлы с PGP подписями. Но я
использовал единственный хэш -- SHA512. Было бы здорово указывать хэши,
которые перечислены зачастую на страницах скачивания у проектов. Чаще
всего это SHA256. А вот например для Go 1.4, который нужен для сборок
более новых версий -- вообще только SHA1. Начал добавлять возможность
указывать несколько хэшей... и меня осенило: ведь именно всем этим и
занимается Metalink формат! c3ba3d2f29655d06dffe1ec836c9f0b98daec0c9,
2374b93f88e7a3222c0e91999306b259bd9e276c. Я же сам его уже давно создаю
и выкладываю для всех tarball-ов своего софта. В нём можно указывать
URL-ы для скачивания, разные хэши, встраивать подписи. Плюс он
поддерживается и GNU Wget-ом и Aria2. В итоге для скачивания софта я
просто добавляю в репозиторий .meta4 файлы и натравливаю на них
wget/aria2c. Часть софта есть только в виде VCS репозиториев -- ну тут
просто руками выполняются git fetch/clone/whatever и git-archive для
создания tarball. Для формирования .meta4 я использовать свою
meta4ra/meta4-create утилиту, но всё это можно без проблем проделать и
вручную, ведь это же текстовый XML.

Начал писать это всё на POSIX shell, чтобы на любой ОС можно было
запустить. Я сделал всё что мог, но всё же дошёл до того, что пришлось
для существенного облегчения жизни перейти на Z Shell. У меня много
pipe-ов используется -- поэтому нужен pipefail включённый. Я думал что
он де-факто есть в любом shell (e3a3ccff5507dd83913a0809b9525e3adabd64d2).
Но как оказалось, POSIX ещё не вышел с ним, поэтому и dash (Debian shell
по умолчанию) pipefail не добавляет! Да и если и добавит, на руках же
куча старых версий дистрибутивов. Переписывать pipe-ы на всякие вызовы с
FIFO файлами и прочими неудобствами я не собираюсь. Только из-за этого
dash я вынужден был плюнуть на POSIX shell. А какая альтернатива? GNU
Bash не имеет права на существование. *ksh? rc? fish? Наиболее разумным
и требующим меньшего порога вхождения я считаю только zsh. Либо писать
вообще не на shell, а на Perl, Tcl, whatever. С zsh хотя бы ещё
существенно всё упрощается в плане экранирования аргументов (чего нет в
bash: 30670475d5bc7b8601a555d33fad188602f96712).

А вот скрипты для создания, раздачи, взятия, запуска задач -- всё это
пока вышло написать на POSIX shell. Сами шаги сборки/проверки
конкретного проекта -- можно писать на чём хочешь, как например и .do
redo файлы. Шаги сборки -- просто исполняемые файлы, а значит и shebang
будет прочтён. Если кто-то захочет написать на zsh их, то заранее в
первых шагах достаточно выполнить pkg-install zsh и он появился в
$tmp/local окружении, став доступным для следующего шага сборки
написанного на zsh.

Скрипт запуска задачи занимает примерно экран POSIX shell. А в нём и
создание окружения, и установка stow+perl+zstd через tar xfO вызовы,
чтобы дальнейшие пакеты уже можно устанавливать через pkg-install
скрипт, учитывающий rundeps. Всё это запускается внутри tmux-а, дабы к
упавшей сборке можно было бы подключиться и попасть буквально в её
окружение где все падения и происходили. На самой системе slave-а иметь
tmux не нужно -- он ставится тут же сразу же из самосборных пакетов.

Скрипт запуска шагов сборки тоже занимает один экран. А это и
перенаправления выводов, демон touch alive файла, проверка не слишком ли
долго выполняется шаг (как в BuildBot -- если нет вывода в течении
часа), убийство зависшей задачи, подчистка всего мусора, и т.д.. Скрипт
создания задачи -- полэкрана.

Задача это директория с $TASK_NUM:$PROJNAME:$REVISION:$ARCH[:$HOST]
именем. Внутри есть code.tar и steps.tar. slave-ы не лазят
самостоятельно в git-ы для получения кода который надо проверять. Всё
это, ради разграничения полномочий, делается на абстрактном master.
Результаты выполняющейся задачи это директория с таким же именем, с:
    alive
    01step/
        stderr.txt
        stdout.txt
        exitcode.txt
    ...
и всё в таком духе. Вывод std* конечно же пропущен через tai64n. Если
нужно сохранить какие-то артефакты сборки, то просто кладём файлы в
эту директорию. Jenkins умел понимать особый XML формат с результатами
прогона unittest-ов и умел анализировать эти XML между соседствующими
сборками и показывать diff (стало падать на пять тестов меньше, и т.д.).
Тривиально положить шагам XML-ку, а сторонними утилитами, пробегаясь по
директориям с результатами, выполнять нужные вычисления.

У меня нет ни одного демона (ну кроме shell скриптов крутящихся в while
true аналоге). Абсолютно всё взаимодействие между master и slave
происходит через операции на файловой системе. Соответственно, между
ними поднимается NFS. Создание задачи: наполнение директории
$TASKS/tmp/$task:, а дальше создание $TASKS/tmp/$task:$arch и жёсткие
ссылки в каждую из этих директорий. Атомарное появление задач для
slave-ов: mv $TASKS/tmp/$task:* $TASKS/cur/. Только один slave должен
мочь взять задачу? mkdir $JOBS/$task:$arch -- только одной из машин это
удастся сделать. Никаких RPC/API демонов, никаких curl вызовов -- только
NFS, ФС и mkdir для атомарных операций.

task-maker-ы -- программы срабатывающие на появление событий от git
hook-ов, не обязаны запускаться на одной и той же машине (на master). По
сути, нет такой роли как центральный master сервер. Должен быть общий
NFS куда разные машины могут выкладывать свои разные $task-и. NFS с
$JOBS-ами или с $PKGS -- могут быть и другими машинами, другими
mountpoint-ами. Для удобства я для всех новых задач постоянно
инкрементирую $TASK_NUM счётчик -- что-то типа уникального
идентификатора задачи. Как это сделать атомарно, если всё что у нас есть
это NFS? Проблема в том, что на нём из коробки не работают lock-и:
https://serverfault.com/questions/66919/file-locks-on-an-nfs
Ну точнее из коробки в FreeBSD на NFSv4 у меня они не сработали. Дальше
не стал разбираться. Поэтому задачу с взятием значения счётчика я решаю
через бесконечный цикл с mkdir-ом инкрементированного значения счётчика
-- если это удалось сделать, то значит значение "наше", а другие пускай
снова пробуют mkdir-ить его инкрементирующееся значение.

Я сразу же условился не бояться copy-paste. Если что-то делается shell
скриптами, то очень многое можно выносить, DRYить в разные маленькие
скрипты. Помню, что берёшь какой-нибудь suckless проект, видишь пару
дюжин скриптов -- и как то вот сразу отпадает желание разбираться в нём,
даже просто прочитать названия этой кучи скриптов. Если во всех скриптах
сборки проекта есть общая часть с созданием временной директории,
trap-ом для её очистки, и всяким таким подобным -- я считаю ничего
страшного чтобы это копировать между всеми скриптами. А то откроешь
такой DRY файл: и видишь с десяток вызовов неизвестных тебе скриптов и
функций и начинаешь прыгать по файлам чтобы понять что они делают. А без
DRY ты просто видишь все шаги as-is. Надо стараться соблюдать некий
баланс между удобочитаемостью и минимальным порогом вхождения и
адекватностью объёма copy-paste. Это же касается и большого кол-ва
переменных окружения неявно приходящих в скрипты и функции: ничего
страшного чтобы постоянно писать $SKELBINS/$ARCH/$hsh-$name/bin -- всё
равно за человека это делает текстовый редактор.

Всё готово на 80-90%. Вышло значительнее меньше по коду чем я
предполагал. Нужно конечно ещё делать и делать описания всяких пакетов и
правил сборки того или иного проекта, но это уже рутина не влияющая на
код самого framework-а.

Проект, кстати, называется zwoki. Я начал отталкиваться от "2nd
continuous integration" фразы, в итоге вышла "zwo" (zwei, два), "ki"
(Kontinuierliche Integration, чтобы вышел не "zwoci", который фиг знает
как правильно прочитать). Должно бы быть конечно "zweiteki" (zweite --
второй, 2nd), но уже длинновато. А так получился "цвоки".

К сожалению, только почти через неделю, вчера перед сном, меня осенило
другое: я даже не задумывался о применении redo. А ведь она
обеспечивает и слежение за зависимостями между собираемыми целями и
атомарно сохраняет результат работы. Надо посмотреть насколько это всё
упрощает и возможно переделать сборку пакетов на использование redo. Не
знаю почему мне сразу это в голову не пришло, ведь у меня уже нет
проектов которые бы не использовали redo (или же в них просто shell
скрипты и нет никаких зависимостей между целями сборки).

    [оставить комментарий]