Доброго времени суток, Хабр!


Сегодня я хочу поделиться опытом разработки под миникомпьютеры на 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

  1. ddb запускает контейнер, выполняет скрипт entry.sh
  2. entry.sh немного настраивает dub, чтобы тот внутри контейнера использовал папку для библиотек, которая будет располагаться в текущей директории, что позволит при повторном запуске сборки заново не выкачивать и не собирать используемые в проекте библиотеки
  3. entry.sh заканчивается тем, что передаёт управлние входной команде (make в нашем случае)
  4. make в свою очередь читает makefile
  5. в makefile хранятся все флаги для кросс-компиляции и директории для сборки, формируется строка вызова dub
  6. при вызове в dub в качестве компилятора передаётся скрипт ldc из текущей директоирии и выставляются переменные окружения
  7. в качестве зависимости сборки в makefile выставлены runtime библиотеки, которые, при их остутствии, собираются программой ldc-build-runtime
  8. переменные передаются в скрипт 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.


Лента новостей в вк

Only registered users can participate in poll. Log in, please.
Пробовали ли D под arm?
1.59%да, всё сделал, но по-другому1
1.59%да, всё сделал так же1
0%да, но ничего не вышло0
19.05%нет, но хочу12
77.78%нет49
63 users voted. 17 users abstained.