Haskell + Raspberry Pi Zero W = шаг в embedded-мир
date = fromGregorian 2018 may 19
category = "Проекты"
tags = ["Raspberry", "embedded"]
В последнее время сообщества разработчиков всё больше увлекаются разными встраиваемыми штуками, IoT, роботы всякие. Да и железо идёт нам на встречу: SBC (Single Board Computer) становятся всё более дешёвыми, а их выбор ширится месяц от месяца. Сегодня мы узнаем, как запустить Haskell-программу на известной модели SBC 2017 года, а именно Raspberry Pi Zero W.
Почему Raspberry Pi?
У меня интерес к этой теме сугубо личный. А на Raspberry Pi Zero W выбор пал по следующим причинам:
- Известная марка и не менее известная модель, с большим сообществом вокруг.
- Модель дешёвая (порядка $10) и компактная, к тому же есть куча мест, где её можно заказать.
- Да, есть ещё одна причина, но о ней говорить в этой заметке смысла нет, это отдельная тема.
Впрочем, выбор в пользу данной модели не окончательный и может измениться в будущем (я знаю, что у Raspberry уже есть куча альтернатив).
А Haskell вообще подходит для SBC?
Это я и хотел выяснить. :-) Ну в самом деле, я видел, как на этих малютках запускают программы на Python, и даже на NodeJS, “ну чем я хуже?!” (С) Фрекен Бок. К тому же это не микроконтроллер с 128 КБ памяти: здесь и оперативки 512 MB, и 1 GHz ARM-процессор, да ещё и MicroSD карта имеется (у меня на 16 GB). В общем, есть где развернуться.
К тому же, не стоит забывать и о преимуществах Haskell, а они в моём проекте необходимы, код должен быть максимально надёжным и предсказуемым.
Операционная система
Работать наш SBC будет под управлением Raspbian, официальной
Debian-версии, заточенной специально под Raspberry-устройства. Мы возьмём Raspbian Stretch Lite от 2018-04-18,
свеженькую версию без десктопа. О том, насколько она свеженькая, можно судить по ядру, оно здесь 4.14
. Качаем архив
с образом диска, 2018-04-18-raspbian-stretch-lite.zip
, он понадобится нам позже.
На всякий случай уточню: моя основная система - Debian 9.3 с ядром 4.9, все приводимые далее команды тестировались на ней.
Собираем? Погодите…
Существует несколько способов сборки Haskell-проекта под ARM-архитектуру:
- классическая кросс-компиляция,
- нативная компиляция прямо на Raspberry,
- через QEMU-эмуляцию ARM-системы,
- через QEMU-эмуляцию пользовательского уровня.
Формально, вы можете выбрать любой из этих способов. Реально же - нет, не любой. И вот почему.
Классическая кросс-компиляция - это решение, традиционно считающееся самым правильным. Идея проста: мы собираем кросс-версию
GHC, которая работать будет на одной платформе (на нашей x86_64
), а код производить для другой. За подробностями - сюда. И хотя способ этот считается самым каноническим, я отказался от него по причине сложности. Посмотрев на
документацию по сборке кросс-компилятора, я подумал, что должен быть, просто обязан быть способ попроще.
Нативная компиляция прямо на SBC - да, это решение, вроде бы напрашивающееся самим собой. Раз есть устройство, и Haskell-программа
будет работать на нём - вот на нём и собирай. Но мечты разбиваются о реальность. Haskell - это вам не C. Сборка на
самом устройстве оказалась очень, нет, ОЧЕНЬ долгой. А хуже всего то, что она вообще не завершилась, ибо ghc: out of memory
.
Да, друзья, пол-гигабайта оперативки - это не так уж и много. Даже при том, что сам Raspbian Lite забирает не более 30 MB,
и я поставил для GHC максимальное RTS-ограничение по памяти, выделенной кучи всё равно не хватало. И учтите, что мой проект -
это, на данный момент, лишь 2к строк, причём без тяжёлых зависимостей типа lens
. В общем, не судьба.
QEMU-эмуляция всей ARM-системы - третий путь, который оказался, к сожалению, ещё хуже второго. Идея понятна: QEMU
эмулирует SBC целиком, эдакая VirtualBox для Raspberry. Берём QEMU-ядро и ранее
упомянутый образ Raspbian, затем запускаем qemu-system-arm
- готово, вот вам виртуальный SBC. Однако потолок по памяти
тут ещё ниже, а именно 256 МБ (поэтому компилятору приходится отдавать и того меньше). Вроде бы есть способ указать и 512 МБ,
но я его не нашёл. А потому сборка происходила ещё медленнее, чем на самом устройстве, ведь всё работает в виртуальной среде.
И опять же, довольно быстро мы упираемся в ту же ghc: out of memory
.
К счастью, существует и четвёртый путь, которым мы и пойдём. Его можно рассматривать как компромисс между вторым и третьим путями.
QEMU-эмуляция пользовательского уровня
Идея проста: незачем эмулировать всю систему целиком, сэмулируем лишь окружение, позволяющее прямо на нашей основной
платформе запускать ARM-нативные исполняемые файлы, такие как ghc
и cabal
. То есть запускать будет ARM-овский GHC,
который и построит нам ARM-овский исполняемый файл, готовый для запуска на Raspberry.
Ставим пакеты:
$ sudo apt install qemu-user-static binfmt-support
Пакет binfmt-support
обеспечивает поддержку различных бинарных форматов. А пакет qemu-user-static
предоставляет ту самую
эмуляцию пользовательского уровня, благодаря чему мы сможем на одном CPU запустить программу, скомпилированную для другого CPU.
Теперь:
$ sudo update-binfmts --enable
$ sudo update-binfmts --display
Результатом второй команды будет список активированных платформ, среди которой и нужная нам:
...
qemu-arm (enabled):
package = qemu-user-static
type = magic
offset = 0
magic = \x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00
mask = \xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff
interpreter = /usr/bin/qemu-arm-static
...
Перейдём к диску.
Увеличение образа
Скачанный нами образ диска Raspbian Stretch Lite, после распаковки, будет весить приблизительно 1.8 GB. Но из них самой системой уже занято порядка 900 MB, а оставшегося места явно маловато для установки нужных нам инструментов и сборки Haskell-зависимостей. Поэтому образ желательно сразу же расширить.
Всё будет происходить в папке /tmp/raspbian
:
$ mkdir /tmp/raspbian
$ cd /tmp/raspbian
$ cp ~/Downloads/2018-04-18-raspbian-stretch-lite.zip .
$ unzip 2018-04-18-raspbian-stretch-lite.zip
Как видите, размер образа 1.8 GiB
, увеличим его:
$ truncate -s +2G ./2018-04-18-raspbian-stretch-lite.img
Свяжем образ с виртуальным устройством /dev/loop0
:
$ sudo losetup /dev/loop0 ./2018-04-18-raspbian-stretch-lite.img
А теперь заглянем внутрь:
$ sudo fdisk -l /dev/loop0
И вот что там:
Disk /dev/loop0: 3.8 GiB, 4009754624 bytes, 7831552 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xc7cb7e34
Device Boot Start End Sectors Size Id Type
/dev/loop0p1 8192 96453 88262 43.1M c W95 FAT32 (LBA)
/dev/loop0p2 98304 3637247 3538944 1.7G 83 Linux
Раздел с FAT32
нам сейчас неинтересен: он монтируется как boot
и предназначен для первичных конфигов SBC (таких как
активация SSH и настройка WiFi-доступа). А вот второй раздел, который Linux
- это наша цель.
Запомните начало второго раздела, 98304
и размер сектора, 512
: эти значения понадобятся нам.
Теперь нужно переформатировать второй раздел, ведь сейчас он всё ещё 1.7 GiB
:
$ sudo fdisk /dev/loop0
Далее, войдя в диалог, вводим следующие команды:
d
2
n
p
2
98304
- Просто
Enter
, принимая умолчальное значение. N
w
Значение на шаге 6 - это тот самый сдвиг до начала второго раздела с Linux-системой. Теперь, вновь заглянув внутрь:
$ sudo fdisk -l /dev/loop0
Мы увидим, что размер увеличился:
...
Device Boot Start End Sectors Size Id Type
/dev/loop0p1 8192 96453 88262 43.1M c W95 FAT32 (LBA)
/dev/loop0p2 98304 7831551 7733248 3.7G 83 Linux
Обратите внимание, что сдвиг к началу второго раздела остался прежним, 98304
, мы лишь добавили секторов.
Для окончательного изменения размера раздела с ext4
-файловой системы выполняем:
$ sudo partprobe /dev/loop0
$ sudo losetup -d /dev/loop0
$ sudo losetup /dev/loop0 ./2018-04-18-raspbian-stretch-lite.img
$ sudo e2fsck -f /dev/loop0p2
$ sudo resize2fs /dev/loop0p2
$ sudo losetup -d /dev/loop0
В результате размер Linux-раздела будет изменён, и мы наконец-то можем смонтировать наш диск.
Монтируем Raspbian
Выполняем:
$ sudo mount ./2018-04-18-raspbian-stretch-lite.img -o loop,offset=$((512 * 98304)) /mnt/raspbian/
где 98304
- тот самый сдвиг до начала Raspbian-системы.
Готовим QEMU
Теперь необходимо разместить исполняемый файл qemu-arm-static
внутри Raspbian:
$ sudo cp $(which qemu-arm-static) /mnt/raspbian/usr/bin
Важно скопировать его именно в /usr/bin
: я, в первый раз, скопировал в /bin
- нет, не заработало.
Прыгаем внутрь
И теперь начинается волшебство. Прыгаем внутрь Raspbian-системы с помощью chroot
:
$ sudo chroot /mnt/raspbian/ qemu-arm-static /bin/bash
root@linux:/#
Готово. Обратите внимание на вывод uname -a
:
Linux linux 4.9.0-4-amd64 #1 SMP Debian 4.9.65-3+deb9u1 (2017-12-23) armv7l GNU/Linux
А вот вывод на моей основной системе:
Linux linux 4.9.0-4-amd64 #1 SMP Debian 4.9.65-3+deb9u1 (2017-12-23) x86_64 GNU/Linux
Видите различие архитектур? armv7l
против x86_64
. И теперь мы можем устанавливать
и запускать ARM-овские пакеты.
А диск точно расширился?
Проверим это:
root@linux:/# df -h /
/bin/df: Warning: cannot read table of mounted file systems: No such file or directory
Filesystem Size Used Avail Use% Mounted on
- 3.6G 949M 2.5G 28% /
Всё в порядке, нам доступно 2.5 GB, этого достаточно.
Git
Нам будет нужен код нашего проекта, поэтому устанавливаем git:
root@linux:/# sudo apt install git
В процессе установки вы увидите нечто такое:
...
Get:3 http://mirror.ossplanet.net/raspbian/raspbian stretch/main armhf git armhf 1:2.11.0-3+deb9u2 [3380 kB]
...
Видите armhf
? Это значит, что устанавливаемый git - это пакет для архитектуры armhf
, у нашего Raspberry именно
такая. Кстати, armhf
означает ARM hard float
, что свидетельствует о способности CPU производить вычисления с
плавающей точкой на аппаратном (hardware) уровне.
GHC и cabal-install
Выполняем:
root@linux:/# sudo apt install ghc cabal-install
В репозитории пакетов Debian Stretch используется версия ghc-8.0.1
. Эта же версия устанавливается и успешно
запускается на самом SBC (проверено!), а значит, собранное этим компилятором тут гарантированно заработает и там.
Сборка
После клонирования репозитория целевого проекта делаем обычные:
root@linux:/# cabal update
root@linux:/# cabal install
В моём проекте, в файле package.yaml
(а следовательно, и в .cabal
), я явно указал версии всех зависимостей, ведь
cabal
ничего не знает про Haskell LTS.
А почему не Stack?
Причины две:
stack
можно установить на ARM-платформу, однако, при выполненииstack setup
, он жрёт дисковое пространство как не в себя. Конечно, можно было бы расширить образ диска гигабайт до 5 или 6, но я не вижу большого смысла: компилятор8.0.1
прекрасно собирает мой код, собираемый на основной системе компилятором8.2.2
.- Когда я попробовал установить компилятор
8.2.2
на ARM, у меня получилось, однако собранная им программа так и не запустилась на SBC, выдав ошибкуIllegal instruction
. Это значит, что формат исполняемого файла не подошёл. Признаться, я не знаю, почему, но факт остаётся фактом.
В итоге я решил остаться на системном ghc-8.0.1
. Конечно, для единства сборки я прописал в stack.yaml
именно эту
версию компилятора, чтобы в процессе разработки пользоваться тем же, чем и в процессе целевой сборки под Raspberry.
Время сборки
Да, первая сборка, когда собираются все зависимости, получается долгой. Под словом “долгой” я имею в виду не один час. Конечно, на это время непосредственно влияет количество и “вес” Haskell-зависимостей, но быстрой сборки не ждите в любом случае.
Впрочем, утешает то, что это лишь в первый раз, в дальшейшем будет компилироваться лишь собственный код проекта. Ориентировочно, 2k строк моего кода компилируются чуть более 3-х минут. Вполне приемлемо.
Значит можно и CI?
А почему бы и нет? Последовательность шагов в нашем гипотетическом CI-скрипте такова:
- Скачиваем архив нашего подготовленного образа диска откуда-нибудь, куда мы предварительно его положили.
- Распаковываем архив.
- Монтируем образ диска, к той же
/mnt/raspbian
. - Переходим внутрь, с помощью
chroot
. - Вытаскиваем последние изменения нашего кода (ну или делаем чистый клон каждый раз).
- Собираем, прогоняем тесты, вот этот вот всё.
- Выходим из
chroot
-режима. - Копируем собранные исполняемые файлы в CI-артефакты, чтобы их потом можно было забрать.
- Отмонтируем образ диска.
Всё. После этого CI-артефакты можно скачивать на SBC и запускать.
Кэширование
Как известно, CI-сервисы умеют кэшировать данные между сборками, и мы можем воспользоваться этим. Тогда нам не придётся каждый раз скачивать один и тот же архив с образом диска. Вместо этого, на последнем шаге, мы снова сжимаем образ в архив, который, на основе нашего CI-конфига, будет кэшироваться. А это значит, что при следующей сборке наш архив будет восстановлен, и нам останется лишь распаковать его.
Это полезно ещё и потому, что, сделав некие изменения во время сборки №5 (например, установив дополнительные Haskell-зависимости), мы “вспомним” их перед началом сборки №6.
Но вы спросите, а как же размер? Да, мы втискиваемся впритык. Известно, что CI-сервисы ограничивают размер файла/каталога, подлежащего кэшированию. Например, у Travis CI это 1 GB. Размер архива образа диска, используемого в этой статье, получается разным, в зависимости от используемого сжатия:
.zip
- 1.2 GB,.tgz
- 1.2 GB,.bz2
- 1.1 GB,.7z
- 781 MB.
Как видите, p7zip
способен сжать образ до приемлемого для кэширования размера. Он, конечно, и сжимает дольше остальных архиваторов,
но результат важнее.
Выводы
Собирать Haskell-проекты для Raspberry-устройств можно, и это наш главный вывод!