Доброго времени суток!
Сегодня я хочу поделиться опытом разработки под миникомпьютеры на 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
└── 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.
Автор: deviator