Доброго времени суток, Хабр!
Сегодня я хочу поделиться опытом разработки под миникомпьютеры на linux (RPI, BBB и другие) на языке программирования D. Под катом полная инструкция о том как сделать это без боли. Ну или почти… =)
Почему D?
Когда на работе встала задача написать систему мониторинга под ARM, даже будучи большим поклонником D, я сомневался стоит ли его брать в качестве основного инструмента. В целом я — не прихотливый человек, и на D уже давно, поэтому подумал, что стоит попробовать и… не всё так однозначно. С одной стороны, особых проблем (кроме одной не совсем понятной, которая ушла с приходом новой версии компилятора) не было, с другой, люди, которые занимаются разработкой под ARM, постоянно могут посчитать, что инструментарий не готов от слова совсем. Решать Вам.
Инструментарий
Могу посоветовать Visual Studio Code
с плагином D Programming Language
от тов. WebFreak (Jan Jurzitza). В настройках можно выставить настройку Beta Stream
, чтобы всегда иметь последнюю версию serve-d
. Плагин сам устанавливает необходимое ПО.
Общая структура проекта
В целом получилось достаточно заморочено (в сравнении с обычным проектом на D), но, как мне кажется, вполне гибко и удобно.
.
+-- arm-lib/
| +-- libcrypto.a
| +-- libssl.a
| L-- libz.a
+-- docker-ctx/
| +-- Dockerfile
| L-- entry.sh
+-- source
| L-- app.d
+-- .gitignore
+-- build-docker
+-- ddb
+-- dub.sdl
+-- ldc
L-- makefile
arm-lib
— библиотеки, необходимые для работы нашего приложения (собранные под arm)
docker-ctx
— контекст для сборки docker образа
entry.sh
— будет выполнять при каждом запуске контейнера некоторые действия, о которых позже
dub.sdl
— файл проекта на D, позволяет включить сторонние библиотеки и многое другое
build-docker
— скрипт сборки контейнера (по сути 1 строка, но всё же)
ddb
— docker D builder — скрипт запуска контейнера (так же одна строка, но на деле так удобней)
ldc
— скрипт, позволяющий вызвать ldc со всеми нужными параметрами
makefile
— содержит рецепты сборки для arm и x86 и дополнительные действия
source/app.d
— исходники проекта
Пара слов о arm-lib
.
Там лежат файлы, необходимые для работы vibe. Добавлять в репозитарий бинарные файлы — плохой тон. Но здесь для упрощения себе жизни легче сделать именно так. Можно добавить их внутрь контейнера, но тогда, чтобы полностью сформировать рецепт сборки контейнера, нужно будет хранить папку arm-lib
в dockert-ctx
. На вкус и цвет...
Общий алгоритм сборки
./ddb make
ddb
запускает контейнер, выполняет скриптentry.sh
entry.sh
немного настраиваетdub
, чтобы тот внутри контейнера использовал папку для библиотек, которая будет располагаться в текущей директории, что позволит при повторном запуске сборки заново не выкачивать и не собирать используемые в проекте библиотекиentry.sh
заканчивается тем, что передаёт управлние входной команде (make
в нашем случае)make
в свою очередь читаетmakefile
- в
makefile
хранятся все флаги для кросс-компиляции и директории для сборки, формируется строка вызоваdub
- при вызове в
dub
в качестве компилятора передаётся скриптldc
из текущей директоирии и выставляются переменные окружения - в качестве зависимости сборки в
makefile
выставлены runtime библиотеки, которые, при их остутствии, собираются программойldc-build-runtime
- переменные передаются в скрипт
ldc
и в параметрыdub.sdl
Содержание основных файлов
Dockerfile
Так как мы будем писать под RPI3, выбираем образ базовой системы debian:stretch-slim
, там gcc-arm-linux-gnueabihf
использует ту же версию glibc
что и официальный дистрибутив raspbian (была проблема с fedora, где мейнтейнер кросскомпилятора использовал слишком свежую версию glibc
).
FROM debian:stretch-slim
RUN apt-get update && apt-get install -y make cmake bash p7zip-full tar wget gpg xz-utils gcc-arm-linux-gnueabihf ca-certificates && apt-get autoremove -y && apt-get clean
ARG ldcver=1.11.0
RUN wget -O /root/ldc.tar.xz https://github.com/ldc-developers/ldc/releases/download/v$ldcver/ldc2-$ldcver-linux-x86_64.tar.xz && tar xf /root/ldc.tar.xz -C /root/ && rm /root/ldc.tar.xz
ENV PATH "/root/ldc2-$ldcver-linux-x86_64/bin:$PATH"
ADD entry.sh /entry.sh
RUN chmod +x /entry.sh
WORKDIR /workdir
ENTRYPOINT [ "/entry.sh" ]
Компилятор ldc
качается с github
, где собран на основе актуального llvm
.
entry.sh
#!/bin/bash
if [ ! -d ".dpack" ]; then
mkdir .dpack
fi
ln -s $(pwd)/.dpack /root/.dub
exec $@
Тут всё просто: если нет папки .dpack
, то создаём, используем .dpack
для создания символической ссылки на /root/.dub
.
Это позволит хранить скачанные dub
-ом пакеты в папке проекта.
build-docker, ddb, ldc
Это три простых однострочных файла. Два из них необязательны, но удобны, но написаны для linux (bash). Для windows придётся создать аналогичные файлы на местном скриптовом или просто запускать руками.
build-docker
запускает сборку контейнера (вызывается один раз, только для linux):
#!/bin/bash
docker build -t dcross docker-ctx
ddb
запускает контейнер для сборки и передаёт параметры (только для linux):
#!/bin/bash
docker run -v `pwd`:/workdir -t --rm dcross $@
Обратите внимание, что используется имя контейнера dcross
(само имя не принципиально, но оно должно совпадать в обоих файлах) и для проброса текущей директории в /workdir
(директория указана как WORKDIR
в Dockerfile
) используется команда pwd
(в win, кажется, нужно использовать %CD%
).
ldc
запускает ldc
, как ни странно, при этом используя переменные окружения (только linux, но запускается в контейнере, так что для сборки под win изменения не требует):
#!/bin/bash
$LDC $LDC_FLAGS $@
dub.sdl
Для примера он будет достаточно прост:
name "chw"
description "Cross Hello World"
license "MIT"
targetType "executable"
targetPath "$TP"
dependency "vibe-d" version="~>0.8.4"
dependency "vibe-d:tls" version="~>0.8.4"
subConfiguration "vibe-d:tls" "openssl-1.1"
targetPath
берётся из переменной окружения потому что dub
некоторые поля рецепта сборки не может специфицировать по платформе (например lflags "-L.libs" platform="arm"
будет добавлять флаг линковщику только при сборке под arm).
makefile
А вот тут самое интересное. По сути make
не используется для сборки как таковой, он вызывает для этого dub
, а уже сам dub
следит за тем что нужно пересобирать, а что нет. Но с помощью makefile
формируются все необходимые переменные окружения, выполняются дополнительные команды в более сложных случаях (сборка библиотек на С, запаковка файлов обновлений и т.д.).
Содержание makefile
объёмней остальных:
# По умолчанию собираем под arm
arch = arm
# target path -- директория, куда будут собираться бинарные файлы
TP = build-$(arch)
LDC_DFLAGS = -mtriple=armv7l-linux-gnueabihf -disable-inlining -mcpu=cortex-a8
# хитрый приём по замене пробелов точками с запятой
EMPTY :=
SPACE :=$(EMPTY) $(EMPTY)
LDC_BRT_DFLAGS = $(subst $(SPACE),;,$(LDC_DFLAGS))
ifeq ($(force), y)
# принудительно пересобираем все пакеты даже если собраны
# иногда необходимо, т.к. dub не отслеживает некоторые варианты изменений
FORCE = --force
else
FORCE =
endif
ifeq ($(release), y)
BUILD_TYPE = --build=release
else
BUILD_TYPE =
endif
DUB_FLAGS = build --parallel --compiler=./ldc $(FORCE) $(BUILD_TYPE)
$(info DUB_FLAGS: $(DUB_FLAGS))
# использовать путь в контейнере
LDC = ldc2
LDC_BRT = ldc-build-runtime
# директория с исходниками ldc, где будут собираться runtime библиотеки для ARM
LDC_RT_DIR = .ldc-rt
# использовать gcc здесь необходимо только для линковки
GCC = arm-linux-gnueabihf-gcc
ifeq ($(arch), x86)
LDC_FLAGS =
else ifeq ($(arch), arm)
LDC_FLAGS = $(LDC_DFLAGS) -L-L./$(LDC_RT_DIR)/lib -L-L./arm-lib -gcc=$(GCC)
else
$(error unknown arch)
endif
DUB = TP=$(TP) LDC=$(LDC) LDC_FLAGS="$(LDC_FLAGS)" dub $(DUB_FLAGS)
# перечисленные цели не являются файлами
.PHONY: all clean rtlibs stat
# цель по умолчанию
all: rtlibs
$(DUB)
DRT_LIBS=$(addprefix $(LDC_RT_DIR)/lib/, libdruntime-ldc.a libdruntime-ldc-debug.a libphobos2-ldc.a libphobos2-ldc-debug.a)
$(DRT_LIBS):
CC=$(GCC) $(LDC_BRT) -j8 --dFlags="$(LDC_BRT_DFLAGS)" --buildDir=$(LDC_RT_DIR) --targetSystem="Linux;UNIX" BUILD_SHARED_LIBS=OFF
# D runtime для ARM
rtlibs: $(DRT_LIBS)
# можно посчитать количество строк кода
stat:
find source -name '*.d' | xargs wc -l
clean:
rm -rf $(TP)
rm -rf .dub
$(LDC_BRT) --buildDir=$(LDC_RT_DIR) --resetOnly
Такой makefile
позволяет собирать проект как под arm, так и под x86 почти одной командой:
./ddb make
./ddb make arch=x86 # соберёт в контейнере под x86
make arch=x86 # соберёт на host системе при наличии ldc
Файлы для arm попадают в build-arm
, для x86 в build-x86
.
app.d
Ну и на закуску для полной картины код app.d
:
import vibe.core.core : runApplication;
import vibe.http.server;
void handleRequest(scope HTTPServerRequest req, scope HTTPServerResponse res)
{
if (req.path == "/")
res.writeBody("Hello, World!", "text/plain");
}
void main()
{
auto settings = new HTTPServerSettings;
settings.port = 8080;
settings.bindAddresses = ["::1", "0.0.0.0"];
auto l = listenHTTP(settings, &handleRequest);
scope (exit) l.stopListening();
runApplication();
}
Всем же сейчас нужен web =)
Заключение
В целом не так всё сложно, как кажется с первого взгляда, просто пока не готов универсальный подход. Лично я потратил много времени пытаясь обойтись без make
. С ним всё пошло как-то проще и вариативней.
Но нужно понимать, что D — не Go, в D принято использовать внешние библиотеки и нужно быть аккуратней с их версиями.
Самый простой способ добыть библиотеку под arm — это скопировать её с рабочего устройства.
Ссылки
Здесь исходный код примера. В этом репозитарии рускоязычным сообществом помаленьку собираем информацию, примеры, ссылки.
Здесь есть дополнительная информация, например о том как собрать для YoctoLinux.
Комментарии (13)
AlienJust
06.11.2018 23:55Добавлять в репозитарий бинарные файлы — плохой тон
Спорное утверждение.
maisvendoo
07.11.2018 00:00+3Спорное утверждение
Бесспорно дурной тон. Репозторий в данном контексте — удаленное хранилище, связанное с системой контроля версий, предназначенной прежде всего для контроля состояния исходных текстов и ресурсов преимущественно текстового формата, если иное не оговорено для конкретных текстовых файлов. Там не место всему тому, что является продуктом обработки исходных кодов -результатам препроцессинга, компиляции и прочему.
.gitignore и .gitattributes курим до просветления и понимания для чего предназначены VCSSiemargl
08.11.2018 09:43А что предлагается делать со сторонними библиотеками, не подлежащими пересборке в рамках проекта?
Пусть проект будет сопровождаться, ну скажем лет 10.
Для D это больной момент — синтаксис, фобос, да и ABI еще «плывут»deviator Автор
08.11.2018 11:18Не понял связи плавания ABI и сторонних библиотек. Библиотеки не на D какими были, такими и остаются, библиотеки на D собираются с проектом как зависимости dub.
immaculate
07.11.2018 05:00Бинарные файлы (точнее, файлы, получающиеся каким-либо компилированием любых исходников) добавляют неимоверное количество боли при работе с несколькими ветками. Это только один из всех возможных костылей.
bfDeveloper
07.11.2018 16:09Радует, что ldc стал нормально из коробки работать на arm. В прошлый раз я был вынужден использовать gdc, а он отстаёт по поддержке фич языка.
Siemargl
Отлично. Озвучьте теперь, плз, результирующие размеры бинарников и требования по памяти.
deviator Автор
Конкретно этот hello world весит 25Mb, требование по памяти при ab -ab -k -c 50 -n 50000 / 41268 7736 5360 (виртуальная, резидентная, разделяемая соответственно).
Проверял на rpi 3b+.
jacob1237
25 Мб наверное потому что собирали с dependency "vibe-d" version="~>0.8.4"?
Можно было бы подтянуть по-отдельности vibe:core и vibe:http, в итоге исходник весил бы около 3Мб после обработки через strip (3 Мб правда это не под ARM, а под x86-64, под ARM наверное даже меньше будет).
Чтобы народ не пугать, ведь в этих 25 Мб куча всяких ненужных вещей типа драйверы mongodb и т.д.
deviator Автор
Да, можно сделать бинарник меньше. 25мб это с полным vibe и debug-информацией. Не меняя сборочного рецепта
./ddb make release=y
выдаёт бинарник в 6.2мб, при использованииvibe-d:core
иvibe-d:http
размер можно уменьшить до 6мб, используяstrip
можно ещё парочку убрать.