Хабрахабр

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

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

Под катом полная инструкция о том как сделать это без боли. Сегодня я хочу поделиться опытом разработки под миникомпьютеры на linux (RPI, BBB и другие) на языке программирования D. Ну или почти… =)

Почему D?

В целом я — не прихотливый человек, и на D уже давно, поэтому подумал, что стоит попробовать и… не всё так однозначно. Когда на работе встала задача написать систему мониторинга под ARM, даже будучи большим поклонником D, я сомневался стоит ли его брать в качестве основного инструмента. Решать Вам. С одной стороны, особых проблем (кроме одной не совсем понятной, которая ушла с приходом новой версии компилятора) не было, с другой, люди, которые занимаются разработкой под ARM, постоянно могут посчитать, что инструментарий не готов от слова совсем.

Инструментарий

WebFreak (Jan Jurzitza). Могу посоветовать Visual Studio Code с плагином D Programming Language от тов. Плагин сам устанавливает необходимое ПО. В настройках можно выставить настройку 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.

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»