Доброго времени суток, Хабр!
Сегодня я хочу поделиться опытом разработки под миникомпьютеры на 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
| └── libz.a
├── docker-ctx/
| ├── Dockerfile
| └── entry.sh
├── source
| └── app.d
├── .gitignore
├── build-docker
├── ddb
├── dub.sdl
├── ldc
└── makefilearm-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 makeddbзапускает контейнер, выполняет скриптentry.shentry.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-ctxddb запускает контейнер для сборки и передаёт параметры (только для 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.