December 15, 2018

Как код на D писать под ARM

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

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

Источник: habr.com