Jenkins + armhf + deb

Эта заметка о том, как я разворачивал CI для около-железячных софтовых проектов и каких проблем я огреб с этим безобразием, пока добился рабочего (хоть и при помощи лома и мата) решения. Возможно даже первая из серии.

Начнем с проблемы. Совсем просто, на случай если это читают дети. У нас есть несколько software проектов. Нам надо периодически делать следующие рутинные действия:

  • Собрать проект и проверить, что он вообще собирается где-то, кроме ноутбука разработчика
  • Прогнать unit-test’ы (Ведь мы же не ленимся писать unit-test’ы, не так ли?)
  • Собрать deb-пакеты, и загрузить куда-нибудь, чтобы пользователи были счастливы

Казалось бы, все давно отлажено, есть OpenSuse Build Service / Open Build Service, Travis, Jenkins. Есть такие страшные штуки как sbuild, schroot и целый выводок утилит от debian-разрабов и просто классных людей, какие могут проблемы? Как всегда, дьявол в деталях. Сразу предупреждаю — текста будет много.

IMG_20151123_010048

Красивые решения отправляются псу под хвост, когда нашему софту надо ВНЕЗАПНО работать с железом и без подключенной к тестовому стенду железки unit-test не пройдет. До кучи проблем добавляет страшное слово «кросс-компиляция», с которой в debian-based дистрибутивах пока все еще не так гладко. Так как сам я уже лет десять использую, в основном, debian-based дистрибутивы, то и все что я тут буду говорить относится в первую очередь к debian’у и его производным, хотя по-хорошему применимо для любого дистрибутива Linux. Итак, приступим.

А в чем собственно проблема с кроссом?

Итак, у нас есть программа написанная, допустим на С. И есть ноутбук (или пачка серверов), допустим, с последним Core i7, а именно архитектурой x86_64. И есть какой-нибудь одноплатный компьютер, допустим, на архитектуре ARM. Так как собирать на нем софт небыстро, мы собираем из исходников при помощи, например, crosstool-ng специальную сборку компилятора: которая работает на нашем быстром Core i7, а генерирует код под наш ARM. Это и называется кросс-компиляцией. Казалось бы, все просто, в чем могут быть грабли?

А первые грабли нас ждут в зависимостях. Более или менее сложная программа использует целую кучу библиотек, с которой динамически линкуется при компиляции. Эти библиотеки тянут за собой другие библиотеки, и так далее. В обычной жизни за нас эти зависимости разруливает менеджер пакетов. Но когда мы пишем в коде

#include <mylib.h>

И компилируем кросс-компилятором — заголовочные файлы берутся не из /usr/include, а из каталога sysroot нашего тулчейна. Обычно такой standalone кросс-компилятор, который нам собрал crosstool-ng не несет с собой в sysroot ничего, кроме библиотеки языка С (Если мы это не собрали и не закинули туда сами). Да и не факт, что версии библиотек и компилятора в нашем cross тулчейне совпадут с теми, что есть, например, в дистрибутиве debian, работающим на железке и нас не будет ждать эпичный облом. Хорошо если сразу не заработает, а ведь может заработать, и потом падать в самом неожиданном месте.

Самое тупое решение — линковать программу статически, чтобы она несла с собой все библиотеки, которые только можно. Но в итоге мы получим жирный бинарный файл, и опять не уйдем от того, чтобы собирать все зависящие библиотеки ручками, как делают «счастливые» пользователи windoze. А потенциально это ад и погибель. Да и не всегда получается избежать динамической линковки.

Капитан Очевидность подсказывает, что чтобы гарантировано все работало в deb окружении, то и собирать надо там же. Но вот простой и доступной информации о том, как «правильно» кросс-компилировать debian пакет под другую архитектуру не так много. С некоторых пор, как в debian появился multiarch подвижки в этом направлении есть. Подробнее можно почитать тут и тут. На практике же здесь все пока очень и очень сыро, и грабли разложены в самых неожиданных местах. Если просуммировать то, что мне удалось накопать по интернетам, у нас есть несколько способов это сделать:

  • Multiarch Cross. За ним будущее, но на практике сыро, приводит к поломаным зависимостям, конфликтам в пакетной системе… В общем, пока еще не готово, а жаль. Убунто-люди в этом плане немного впереди дебиан-человеков, есть надежда что через несколько лет ситуация изменится.
  • chroot в систему с другой архитектурой используя qemu static.
  • Виртуальная машина с полной эмуляцией железки

Если этих проблем кажется мало, то суровая реальность ™ подсунула нам свинью в виде заголовочных файлов ядра. Если мы работаем с какой-то встраиваемой железкой, то скорее всего там есть разные специализированные блоки для этих самых встраиваемых нужд. Причем нужд не всегда стандартизированных и принятых в основную ветку ядра. Дело в том, что заголовочные файлы ядра, которые «говорят», как надо работать с той или иной железкой, и какие ioctl’ки отправлять устанавливаются в sysroot на этапе сборки тулчейна, потому если у нас будет программа с

#include <linux/mymegadev.h>

которого не будет в основной ветке ядра, то с обычными заголовочными файлами linux’а оно не соберется, как ни старайся. По счастью ломают что-то стандартное теперь уже крайне редко. Потому рекомендованный на lwn способ с этим бороться — просто скопировать нужный файл из сторонней ветки ядра и таскать с собой вместе с исходными кодами нашей программы.

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

Выбираем CI

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

  • Пачка кривых скриптов на баше, запускающаяся по крону или по пинку из консоли (Решение, проверенное поколениями админов!)
  • sbuild
  • Travis
  • Open Build Service / OpenSuse Build Service
  • Jenkins

Первый пункт отпадает потому, что перфекционист во мне таким решением остался крайне недоволен.

sbuild’ом я уже пользовался, фактически пакет собирается в chroot через qemu. Но никаких готовых средств автоматизации сборки по коммиту в репозиторий у него не было. Да и unit-тесты он если и запустит, то случайно при сборке.

К преимуществам Travis’а стоит отнести простую интеграцию с github’ом. Но вот запустить его у себя, тем более собирать пакеты он не умеет. И про arm тоже ничего не знает. По крайней мере в бесплатной версии.

Open Build Service, а вернее его доступный всем желающим инстанс от OpenSuse выглядел как панацея до тех самых пор, пока я его не решил попробовать. Описание процесса сборки руками в таком понятном и простом формате как xml, не слишком понятная документация. Чтобы сделать вполне очевидную цепочку: клонирование из гит репозитория -> сборка с запуском юнит-тестов -> опакечивание я уменя ушло несколько вечеров. С одной стороны вроде бы им пользуются и оно работает, но для моих нужд это было явно не самое лучшее решение.

Сказано — сделано. Раз нет других альтернатив — будем тратить процессорное время впустую и греть атмосферу используя qemu в чруте. Но не сложилось. java заставила qemu упасть, так и не запустив процесс jenkins’а. Честная же виртуалка через тот же qemu положила на лопатки мой i7. Молчу уже про то, что «красивых» инструментов для создания виртуалок типа vagrant’а не получится, скорее всего. И если helloworld таким образом откомпилировать было можно — то что-то более или менее тяжело такое решение не годилось совершенно.

Где-то на этом месте мне пришлось плюнуть и все же вспомнить о том варианте, о котором я совершенно забыл. Из ящика стола был извлечен Android TV Stick на RK3188. Android там даже не был ни разу загружен, в то время как debian очень даже прижился. Когда-то я хотел из него сделать затычку в большой телевизор, чтобы пробрасывать всякий софт на больной экран. Но учитывая состояние поддержки X11 драйверами графического ускорителя mali и поддержку OpenGL ES оконным менеджером kwin, да и еще полное отсутствие аппаратного декодирования видео я от этой затеи отказался. Чтобы довести эту связку до ума нужна была команда full-time разработчиков, а не пара выходных которые ты решил бездарно убить. Железка с тех пор пылилась без дела.

Пока я вспоминал состояние софта на этой платформе, в голове уже примерно выстроился план как эту китайскую поделку подцепить в jenkins’у.

IMG_20151123_010048

Доводим клиента до кондиции

На момент написания этого текста под RK3188 есть «китайское» ядро версии 3.0.36, с дикими хаками, но где работает с горем пополам все. И неплохо доработанная к этому моменту основная ветка, где не хватает разве что драйвера NAND флешки, графики, и декодирования видео. А еще не хватает возможности изменять частоту работы DDR3 памяти «на лету», которую загрузчик выставляет на 222 Mhz от чего система дичайше тормозит. Быстренько потыкав мейнлайн, я откатился из-за этого дела к «китайскому» ядру, откуда вырезал всю графику, звук, а DDR «разогнал» до 800 Mhz. Так как ethernet на нашем стике нет, а WiFi будет медленным и печальным, то USB-device я перевел в режим cdc_eem, то есть сеть по USB. В этом режиме оно заработало на удивление неплохо, выдав 276 Мегабит/с на iperf. Конечно, не гигабит но сойдет. В комплекте с быстрой, CLASS 10 SD карточкой — самое то.

root@iltharia:~# iperf -c 192.168.30.1
------------------------------------------------------------
Client connecting to 192.168.30.1, TCP port 5001
TCP window size: 64.1 KByte (default)
------------------------------------------------------------
[  3] local 192.168.30.9 port 38138 connected with 192.168.30.1 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.0 sec   329 MBytes   276 Mbits/sec
root@iltharia:~#

Подробно расписывать процесс установки debian jessie armhf на этот TV Stick я не буду, но опишу то, как настраивал систему ибо это то самое место, где потребовалась некоторая «магия».

Проблема в том, что под ARM у нас далеко не только одна система. У debian для ARM есть два варианта: armel и armhf

armhf ожидает наличия в камне блока работы с плавающей точкой, VFPv3, не ниже. armel же могут использовать те, у кого VFP нет вообще. И где-то между ними живет raspbian. У Raspberry Pi, как и у отечественного MB77.07 внутри ядро ARM1176, а в нем есть только VFPv2. Соответственно на armel он будет простаивать, а armhf не заработает. Именно поэтому поклонники Raspberry и сделали специальный Debian для Raspberry, пересобрав пакеты как hardfloat, hf, но для VFPv2.

Итого получаем уже три системы, все из которых смогут работать на нашем RK3188 (А тут у нас Сortex-a9, 4 штуки, если и VFPv3, и NEON SIMD). И это если мы собираем только под стабильный дистрибутив debian. А если еще и testing? Уже 6. А если еще и решим под убунту насобирать чего-то?

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

Начнем с настройки сети. Чтобы usb контроллер подключился к хост-компьютеру его надо пнуть:

 echo 1 > /sys/bus/platform/drivers/usb20_otg/dwc_otg_conn_en

Специфика dwc_otg в китайском ядре. Это я запихнул в /etc/rc.local
Туда же я запихнул небольшое заклинание:

ntpdate pool.ntp.org

Дело в том, что после каждого перезапуска системы, так как на TV-стике отсутствует RTC мы ВНЕЗАПНО оказываемся в 1970м году, от чего современному софту плохеет. И без этой машинки времени, возвращающей нас назад в будущее никак.

Так как единственным usb_gadget драйвером при сборке ядра я указал CDC Ethernet Gadget, то и интерфейс usb0 появляется при старте системы, причем каждый раз со случайным MAC адресом. Именно поэтому в /etc/network/interfaces я записал вот такой вот текст:

auto usb0

iface usb0 inet static
         address 192.168.30.9
         netmask 255.255.255.0
         gateway 192.168.30.1
         hwaddress ether 00:01:04:1b:2C:10

Далее на компьютере оставалось только привязать в Network Manager’е настройки IP адреса к интерфейсу с этим MAC’ом. Так как jenkins у меня (пока) развернут на ноутбуке на которым я постоянно работаю, то и подключать TV стик я пока решил к нему. Уже потом закину на полку к остальному «серверному хозяйству», если потребуется.

Директорию, в которой будет собираться софт я подмонтировал по NFS с ноутбука, чтобы продлить жизнь карточке.

Следующим этапом я установил schroot. Это очень удобный инструмент для управления chroot окружением. Я сварганил при помощи multistrap’а две тестовых корневых файловых системы, в которые вошли компилятор с инструментами в которых я буду проводить непосредственно сборку, скопировал их на карточку в /srv/chroots/ и добавил конфигурационные файлы примерно следующего содержимого в /etc/schroot/chroot.d

root@iltharia:~# cat /etc/schroot/chroot.d/debian-jessie-armel
[debian-jessie-armel]
description=Debian Jessie (armel)
type=directory
directory=/srv/chroots/debian-jessie-armel
users=root
groups=sbuild
root-groups=root

После этого мы можем переходить chroot окружение по schroot -c debian-jessie-armel и debian-jessie-armhf соответственно.

Теперь самый главный, пусть и немного грязный трюк. Дело в том, что jenkins подсоединяется к удаленному узлу по ssh. Одни параметры подключения на один узел. Соответственно нам либо надо держать работающий ssh сервер в каждом chroot’е, что честно говоря не очень удобно либо… Либо вспомнить, что ssh умеет chrootить залогинившихся пользователей из соображений безопасности. Именно поэтому, я сделал такую магию:

1. Создал schroot сессии, которые бы сохранялись после перезагрузки системы

schroot -c debian-jessie-armel -b -n armel

schroot -c debian-jessie-armhf -b -n armhf

2. Добавил пользователей armel и armhf, uid и gid при этом сделал у них 0. То есть это получились полные аналоги пользователя root.

3. В /etc/sshd_config я добавил строчки:

Match User armhf
ChrootDirectory /var/lib/schroot/mount/armhf/

Match User armel
ChrootDirectory /var/lib/schroot/mount/armel/

 

schroot на каждый такой переход по умолчанию создает сессию в /var/lib/schroot/mount/имя_сессии. И chroot-ится надо именно туда.

Вуоля. Теперь в зависимости от имени пользователя нас будет перенаправлять либо armel либо в armhf систему. Теперь остается сказать об этом jenkins’у, добавив два новых узла и указав в виде директории для сборки директорию, смонтированную по NFS.

10

В параметрах ноды я поставил режим «запускать on-demand». С текущими настройками, если необходимо собрать пакеты для ARM — достаточно просто воткнуть TV-стик в USB и его chroot’ы через минуту станут доступны для jenkins’а.

11

Остается только создать наш Multi-configuration project, описать параметры сборки, а в виде одной из «Осей» сборки указать сборку на нескольких узлах, а получившиеся на выходе артефакты, коими будут являться deb пакеты загружать куда-нибудь в репозиторий.

12

Такое извращение не лишено минусов.

  • Первое, и, пожалуй, самое главное — пакеты собираются от root’а. По науке надо было бы создавать не алиасов пользователя root, а двух простых пользователей, которым давать право пользоваться своим chroot окружением, а уже в нем получать права root через sudo для выполнения apt-get update и прочего. Мне же было просто лень.
  • Требуется отдельная железка, которая хоть и имеет 4 ядра по сравнению с мощным сервером по производительности далеко не сахар.
  • Если проектов много/собирать надо будет часто, то по производительности оно просядет
  • Так как chroot на SD карточке, то зависимости для сборки постоянно устанавливаются и удаляются, что не очень хорошо сказывается на ее ресурсе. Выход — либо переместить chroot окружения на NFS, либо поставить все build-зависимости руками. Я пока не запариваюсь с этим — посмотрим сколько проживет специально купленная для этого дела карточка

Проект на С, сборка + запуск unit-test-ов + пакетирование:

  • x86_64, Core i7 @ 2.6Ghz: 56 секунд
  • armhf chroot, RK3188T @ 1Ghz: 5 минут 7 с 3 минуты 22 секунды, если в tmpfs

Не сахар, конечно, но жить можно. Скорее всего немного поигравшись с параметрами степпинга для процессора и прочим можно серьезно ускорить процесс. Драйвер dwmmc контроллера в «китайском» ядре каким-то образом умудряется замедлить CLASS 10 карту до 13 мегабайт/с на чтение. На mainline с dwmmc в разы лучше, но проблемы с DDR, который безбожно тормозит. Пожалуй, если решение приживется в хозяйстве пора задумываться о приобретении какого-нибудь 8-ядерного allwinner’а.

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

Добавить комментарий