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


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

  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.


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

Комментарии (13)


  1. Siemargl
    06.11.2018 22:52

    Отлично. Озвучьте теперь, плз, результирующие размеры бинарников и требования по памяти.


    1. deviator Автор
      06.11.2018 23:58

      Конкретно этот hello world весит 25Mb, требование по памяти при ab -ab -k -c 50 -n 50000 / 41268 7736 5360 (виртуальная, резидентная, разделяемая соответственно).
      Проверял на rpi 3b+.


      1. jacob1237
        07.11.2018 22:40

        25 Мб наверное потому что собирали с dependency "vibe-d" version="~>0.8.4"?


        Можно было бы подтянуть по-отдельности vibe:core и vibe:http, в итоге исходник весил бы около 3Мб после обработки через strip (3 Мб правда это не под ARM, а под x86-64, под ARM наверное даже меньше будет).


        Чтобы народ не пугать, ведь в этих 25 Мб куча всяких ненужных вещей типа драйверы mongodb и т.д.


        1. deviator Автор
          08.11.2018 02:26

          Да, можно сделать бинарник меньше. 25мб это с полным vibe и debug-информацией. Не меняя сборочного рецепта ./ddb make release=y выдаёт бинарник в 6.2мб, при использовании vibe-d:core и vibe-d:http размер можно уменьшить до 6мб, используя strip можно ещё парочку убрать.


  1. AlienJust
    06.11.2018 23:55

    Добавлять в репозитарий бинарные файлы — плохой тон


    Спорное утверждение.


    1. maisvendoo
      07.11.2018 00:00
      +3

      Спорное утверждение

      Бесспорно дурной тон. Репозторий в данном контексте — удаленное хранилище, связанное с системой контроля версий, предназначенной прежде всего для контроля состояния исходных текстов и ресурсов преимущественно текстового формата, если иное не оговорено для конкретных текстовых файлов. Там не место всему тому, что является продуктом обработки исходных кодов -результатам препроцессинга, компиляции и прочему.

      .gitignore и .gitattributes курим до просветления и понимания для чего предназначены VCS


      1. Siemargl
        08.11.2018 09:43

        А что предлагается делать со сторонними библиотеками, не подлежащими пересборке в рамках проекта?

        Пусть проект будет сопровождаться, ну скажем лет 10.
        Для D это больной момент — синтаксис, фобос, да и ABI еще «плывут»


        1. deviator Автор
          08.11.2018 11:18

          Не понял связи плавания ABI и сторонних библиотек. Библиотеки не на D какими были, такими и остаются, библиотеки на D собираются с проектом как зависимости dub.


    1. immaculate
      07.11.2018 05:00

      Бинарные файлы (точнее, файлы, получающиеся каким-либо компилированием любых исходников) добавляют неимоверное количество боли при работе с несколькими ветками. Это только один из всех возможных костылей.


  1. deviator Автор
    06.11.2018 23:58

    [deleted]


  1. nckma
    07.11.2018 12:16

    Всегда удевляют неудачные названия языков программирования вроде C, D, Go.
    Гуглить нужный вопрос по языку очень трудно ибо поисковик не всегда понимает контекст таких коротких слов.


    1. deviator Автор
      07.11.2018 12:53

      Разработчики призывают использовать dlang в поисковых запросах.


  1. bfDeveloper
    07.11.2018 16:09

    Радует, что ldc стал нормально из коробки работать на arm. В прошлый раз я был вынужден использовать gdc, а он отстаёт по поддержке фич языка.