Новости и статьи

Среда разработки встроенных систем или дистрибутивов на основе GNU/Linux. Здесь вы можете найти новости проекта, статьи и заметки, касающиеся разработки дистрибутива Radix cross Linux.

Система сборки (примеры)

25 ноября 2024 г.

Система сборки Radix cross Linux достаточно подробно описана на странице Overview.

Build System

Здесь мы рассмотим новые аспекты использования системы сборки на простых примерах "Здравствуй, Мир!" для создания независимых пользовательских приложений с применением набора инструментариев Radix cross Linux.

Предыдущие версии системы сборки не подразумевали использования компиляторов Clang и Rust как основных cross-компиляторов. Однако современные реалии привели нас к необходимости использования собственных инструментариев LLVM и RUST, но не в качестве основных (поясним это далее).

Дело в том, что множество современных пакетов прикладных программ, помимо GCC, используют компиляторы языка Rust и набор LLVM. Разработчиков привлекает тот момент, что для поддержки множества архитектур целевых устройств достаточно установить одно средство, вместо использования нескольких инструментариев (Toolchain), собранных из исходного кода GCC.

tags

Статья будет достаточно объемной, поскольку необходимо раскрыть множество вещей, без понимания которых пользователю будет трудно, в дальнейшем, перейти от простых примеров к более сложным проектам.

Без Rust уже нельзя, но...

Как правило, компилятор Rust используется совместно с компиляторами C/C++, например в проекте Mozilla Firefox или librsvg. При этом, toolchain‑ы Rust легко устанавливаются с помощью утилиты rustup и разработчик запросто может расширить линейку поддерживаемых им CPU, имея в распоряжении единожды установленный инструментарий cargo.

Однако здесь его подстерегает несколько проблем:

  • не всегда существуют необходимые toolchain‑ны в стандартном наборе Rust
  • отсутствует возможность изменить целевой триплет на предмет изменения имени целевой OS.

Вторая проблема решается легко, например, если в рамках сборки продукта на цель x86_64-radix-linux-gnu необходимо использовать еще и компилятор Rust, то в Make-файле проекта можно вручную задать существующий целевой триплет:

TARGET      = x86_64-radix-linux-gnu
rust-target = x86_64-unknown-linux-gnu

extra_configure_switches += RUST_TARGET=$(rust-target)
extra_configure_switches += CARGO='$(cargo-executable) +1.71.1-x86_64-unknown-linux-gnu --config $(cross_file)'

	@./configure                     \
	  --prefix=/usr                  \
	  --build=x86_64-slackware-linux \
	  --host=$(TARGET)  \
	  $(extra_configure_switches)

Данный пример показывает использование компилятора Rust для сборки librsvg, а именно при задании управлений процедуре конфигурирования исходного кода librsvg перед сборкой.

Здесь видно, что разработчику приходится вручную назначать имя целевого триплета для компилятора rustc из набора доступных целей toolchain‑а 1.71.1-x86_64-unknown-linux-gnu. Если же целей имеется множество, то и сложность Make-файла будет выше из-за необходимости применения директив ifeq, ifneq, else, endif.

Первая же проблема может быть решена только путем сборки собственного инструментария Rust, а это в свою очередь опять подразумевает наличие заранее собранного cross-компилятора GCC (с набором системных и runtime библиотек) или наличия набора LLVM, поддерживающего нужную архитектуру CPU.

Разумеется набор GCC, в данном случае, наиболее предпочтителен поскольку количество поддерживаемых архитектур у GCC гораздо больше, чем у LLVM. Кроме того, GCC позволяет осуществить более тонкую настройку на конкретный набор команд и особенностей выбранного процессора.

Инструменты LLVM и RUST

Причины, описанные выше, и привели к тому, что в систему сборки была добавлена возможность использования "сторонних" наборов LLVM и RUST. Слово "сторонних" неслучайно взято в кавычки.

С одной стороны, наборы LLVM и RUST собраны самостоятельно и поддерживают все необходимые нам архитектуры и целевые триплеты так, что никакая подмена имен с помощью директив условной компиляции не понадобится.

Но, с другой стороны, нам не нужны, ни LLVM, ни RUST в качестве основных компиляторов, которые выбираются автоматически для конкретного целевого устройства. То есть LLVM и RUST всегда будут находиться в стороне от главного принципа системы сборки, который характеризуется тремя словами:

TOOLCHAIN, HARDWARE, FLAVOUR

и реализованного способом, описанным в файле PRINCIPLE.md.

Одним словом, LLVM и RUST всегда будут оставаться "сторонними", если речь идет о сборке продукта в парадигме GNU.

Автозагрузка инструментов

Здесь и далее за трактовкой всех непонятных, на первый взгляд, терминов мы будем отсылать читателя к обзорной статье, посвященной системе сборки, а также к разделу User's Makefile.

Прежде чем рассматривать систему сборки и примеры, необходимо приготовить рабочее окружение. Разумеется на машине должны быть установлены стандартные (для текущего дистрибутива) средства разработки, такие как binutils, gcc, autoconf, automake.

Итак. Создадим каталог /opt/toolchains:

sudo mkdir -p /opt/toochains
sudo chown -R user:user /opt/toochains

Выберем каталог для работы, например, /home/user/development (здесь слово user подразумевает имя пользователя):

mkdir -p /home/user/development/
cd /home/user/development/

и загрузим исходный код системы сборки Radix cross Linux:

svn checkout svn://radix-linux.su/radix/build-system/branches/build-system-1.11.x build-system

Исходный код примеров можно снять с ветки radix-1.11, основного репозитория дистрибутива Radix cross Linux. Subversion, в отличие от Git, позволяет снять любой каталог проекта, избавляя нас от необходимости загружать весь проект целиком:

svn checkout svn://radix-linux.su/radix/system/branches/radix-1.11/doc doc

Перейдем в каталог ./build-system и приготовим систему сборки к работе:

cd build-system
make -j16

Здесь надо набраться терпения и подождать несколько минут до окончания сборки.

Далее можно перейти в каталог doc и собрать программу "Здравствуй, Мир!":

cd ../doc/examples/hello/
HARDWARE=build make

Если сборка не удалась, заходите в Telegram, нажимайте Leave a comment под любым постом и задавайте вопрос: Почему не собирается код?

После этого, можно приступать к рассмотрению системы сборки и принципов ее работы.

Cтроки 135 - 142 файла build-system/constants.mk дают определения фиктивных целевых устройств, то есть устройств, которые служат лишь для автоматического выбора toolchain‑ов работающих на машине разработчика.

Так, цели HARDWARE_NOARCH и HARDWARE_BUILD служат для загрузки исходных кодов и для сборки программ для нативной машины (машины, на которой ведется сборка программ), соответственно. Тут важно заметить то, что данные цели разрешены всегда и не фигурируют в списке доступных целевых устройств, и для них не нужны никакие специальные toolchain‑ы. Здесь используется нативный набор GCC, установленный на рабочей GNU/Linux машине.

Цели HARDWARE_RUST и HARDWARE_LLVM, также предназначены для сбоки программ на машине разработчика, но заданы лишь для того, чтобы во время подготовки системы сборки к работе, была возможна автоматическая загрузка toolchain‑ов LLVM и RUST в случае, когда данные toolchain‑ы не инсталлированы на машине разработчика заранее.

Поясним, о чем идет речь.

Все toolchain‑ы Radix cross Linux устанавливаются в каталог /opt/toolchains. Данный каталог должен быть создан заранее и права на чтение и запись в этот каталог должны быть предоставлены разработчику:

sudo mkdir -p /opt/toochains
sudo chown -R user:user /opt/toochains

Здесь имя и группа user, разумеется, должны быть заменены на те, которые необходимы на машине разработчика.

Если же вы начали сборку программ на какое-либо целевое устройство, не позаботившись заранее о каталоге /opt/toolchains, система сборки выдаст предупреждение и инструкцию по созданию каталога /opt/toolchains:

#
#
# Please create '/opt/toolchains' directory
# and give write permissions to 'user':
#
#    # sudo mkdir -p /opt/toolchains
#    # sudo chown -R user:user /opt/toolchains
#
#
# ERROR: /opt/toolchains: Permission denied. Stop.
#

Здесь, user – это имя того пользователя, который запустил процесс сборки (см. раздел Build System in Practice).

Вернемся к целям HARDWARE_RUST и HARDWARE_LLVM. Как мы говорили ранее, данные цели созданы для того, чтобы toolchain‑ы LLVM и RUST загружались с FTP-сервера и инсталлировались в каталог /opt/toolchains автоматически, если они не подготовлены заранее.

Откуда берутся инструменты

Рассмотрим описания toolchain‑ов в файле build-system/constants.mk (строки с 770 по 791).

Здесь видно, что в описание toolchain‑ов TOOLCHAIN_BUILD_RUST и TOOLCHAIN_BUILD_LLVM входит определение ..._TARBALL, которое и задает источник загрузки toolchain‑а.

Процесс привязки toolchain‑ов LLVM и RUST описаны в файлах build-system/3pp/app/llvm/18.1.8/Makefile и build-system/3pp/app/rust/1.82.0/Makefile, соответственно.

Эти файлы содержат определения

ENABLE_LLVM = true
ENABLE_RUST = true

именно для того, чтобы временно разрешить данные фиктивные целевые устройства, но лишь для обеспечения автоматической загрузки и инсталляции (этих "сторонних") toolchain‑ов так, как это делается для всех основных toolchain‑ов Radix cross Linux.

Ну и поскольку toolchain‑ы LLVM и RUST не являются основными, вызов сценариев их подключения также является нестандартным: строки 40 - 46 корневого Make-файла системы сборки.

Таким образом, после сборки всех основных средств, осуществляется подключение LLVM и RUST, а затем создается файл build-system/build-config.mk путем простого копирования шаблона.

Временный файл build-system/build-config.mk можно редактировать вручную или же с помощью скрипта build-system/configure-targets на предмет выбора доступных для сборки целевых устройств. Кроме того, разработчик может хранить у себя заранее подготовленные шаблоны build-config.mk, чтобы не редактировать его каждый раз, когда будет необходимо запретить или разрешить какие-либо целевые устройства.

Чтобы понять, как имено можно использовать toolchain‑ы LLVM и RUST, надо рассмотреть файл build-system/sbin/.config, где задаются определения:

LLVM_CONFIG        := /opt/toolchains/LLVM/18.1.8/bin/llvm-config
LLVM_PATH          := /opt/toolchains/LLVM/18.1.8/bin

RUST_TOOLCHAIN     := RcL-1.82.0-x86_64-unknown-linux-gnu
CARGO_HOME         := /home/user/development/build-system/usr/lib/cargo
RUSTUP_HOME        := /home/user/development/build-system/usr/lib/rustup
CARGO              := /home/user/development/build-system/usr/lib/cargo/bin/cargo
RUSTC              := /opt/toolchains/RUST/1.82.0/bin/rustc
RUSTDOC            := /opt/toolchains/RUST/1.82.0/bin/rustdoc
RUSTFMT            := /opt/toolchains/RUST/1.82.0/bin/rustfmt
CBINDGEN           := /opt/toolchains/RUST/1.82.0/bin/cbindgen
RUST_PATH          := /home/user/development/build-system/usr/lib/cargo/bin:/opt/toolchains/RUST/1.82.0/bin

которые можно использовать в User's Make-файлах, поскольку файл build-system/sbin/.config включается в build-system/core.mk системы сборки.

Разумеется, путь /home/user/development/ будет другим, в зависимости от того, где разработчих решил разместить систему сборки для работы. Важно подчеркнуть, что ни toolchain‑ы, ни сама система сборки не являются перемещаемыми.

Здравствуй, Мир!

Вот, наконец, мы подошли к непосредственным примерам использования системы сборки.

Здесь мы рассмотрим систему сборки именно как инструмент создания независимых приложений с использованием toolchain‑ов Radix cross Linux.

Начнем, естественно, с самого простого, а именно со сборки приложения "Здравствуй, Мир!" для машины разработчика. Сценарий сборки представлен в файле doc/examples/hello/Makefile:


COMPONENT_TARGETS  = $(HARDWARE_BUILD)
COMPONENT_TARGETS += $(HARDWARE_INTEL_PC64)

include ../../../build-system/constants.mk

# ======= __END_OF_REQUIRES__ =======

bin_srcs = main.c

SRCS = $(bin_srcs)

bin_objs = $(addprefix $(TARGET_BUILD_DIR)/,$(bin_srcs:.c=.o))
bin_target = $(TARGET_BUILD_DIR)/main

info_target = $(TARGET_BUILD_DIR)/.info-done

BUILD_TARGETS = $(bin_target) $(info_target)

#
# The user may reject the sysroot usage. For this the user have to declare
# the USE_TARGET_DEST_DIR_SYSROOT variable with value 'no':
#
#   USE_TARGET_DEST_DIR_SYSROOT = no
#
USE_TARGET_DEST_DIR_SYSROOT = no


include ../../../build-system/core.mk


$(bin_target): $(bin_objs)
	$(LINK)
########################################################
# Also Directly using $(CC) and $(LINKER) is available:
# ====================================================
#	$(CC) $(CFLAGS) -c -o $(TARGET_BUILD_DIR)/main.o main.c
#	$(LINKER) $(ARCH_FLAGS) $(LDFLAGS) -o $(TARGET_BUILD_DIR)/main $(TARGET_BUILD_DIR)/main.o
	@touch $@

$(info_target): $(bin_target)
	@echo "==================================="
	@echo "======= Environment:        ======="
	@echo "==================================="
	@echo "======= CFLAGS     = '$(CFLAGS)'"
	@echo "======= LDFLAGS    = '$(LDFLAGS)'"
	@echo "======= ARCH_FLAGS = '$(ARCH_FLAGS)'"
	@echo ""
	@echo "#"
	@echo "# Please find the `basename $(bin_target)` executable in the $(TARGET_BUILD_DIR)/ directory."
	@echo "#"
	@echo ""
	@touch $@

Для сборки проекта необходимо перейти в каталог doc/examples/hello/

cd doc/examples/hello/

и выполнить следующую команду:

HARDWARE=build make

Если же не задавать целевое устройство HARDWARE=build, то исполняемый файл main будет собран дважды: сначала для рабочей машины, а затем для Generic Intel x86_64 машины с использованием toolchain‑а /opt/toolchains/x86_64-PC-linux-glibc/1.11.3/.

Результаты сборки следует искать в каталогах

.$(TOOLCHAIN)/$(HARDWARE)/

как описано в разделе Hello, World.

Для того чтобы было проще понять, как именно собран файл main, следует обратиться к основному файлу системы сборки build-system/core.mk, где в секции Generic Rules (строки от 1843 до 1880) описаны правила: $(LINK), $(LINK_C), $(LINK_SO), $(LINK_A).

Разберем подробнее наш сборочный сценарий doc/examples/hello/Makefile.

Ключевое слово BUILD_TARGETS (строка 17) определяет цели сборки, а именно $(bin_target) и $(info_target):

BUILD_TARGETS = $(bin_target) $(info_target)

Естественно, основной целью является исполняемый файл main, размещеный в каталоге:

.$(TOOLCHAIN)/$(HARDWARE)/

Данный каталог определен ключевой переменной системы сборки TARGET_BUILD_DIR:

bin_target = $(TARGET_BUILD_DIR)/main

Цель $(info_target) добавлена лишь для того, чтобы показать флаги, которые использовались при сборке и дать комментарий о том, где искать результаты.

Здесь следует пояснить два момента: назначение определения USE_TARGET_DEST_DIR_SYSROOT=no и откуда взялись управления для компилятора GCC.

Сборка системы Radix cross Linux может осуществляться в двух режимах:

Первый определяется тем, что используется системная библиотека GNU Libc, которая уже присутствует в toolchain‑е и именно она переносится в rootfs наряду с runtime библиотеками языков, компиляторы которых входят в состав toolchain‑а.

Такой режим предназначен для сборки минималистичных систем, не включающих в свой состав средств разработки и предназначенных именно для малых, встраиваемых систем.

Для того, чтобы сообщить системе сборки факт того, что sys-root находится в составе toolchain‑a и мы не собираем собственные системные библиотеки, служит значение 'no' переменной USE_TARGET_DEST_DIR_SYSROOT.

Второй режим сборки, характеризуется тем, что система собирается "с нуля", то есть всё, начиная с runtime библиотек языков и GNU Libc, собирается последовательно и именно эти результирующие пакеты будут инсталлированы в корневую файловую систему образа Radix cross Linux. В этом режиме переменная USE_TARGET_DEST_DIR_SYSROOT приравнивается к значению USE_TARGET_DEST_DIR_SYSROOT=yes.

Разумеется, в нашем случае, когда идет соборка простой утилиты "Здравствуй, Мир!", в отсутствии собранной корневой файловой системы, мы должны назначить USE_TARGET_DEST_DIR_SYSROOT=no.

Теперь об управлениях компилятором.

Определения каждого из основных toolchain‑ов в файле build-system/constants.mk (строки с 795 по 1420) содержат определения ..._ARCH_FLAGS, кроме того, общие флаги задаются в файле build-system/target-setup.mk.

Таким образом, после выполнения сценария doc/examples/hello/Makefile, для HARDWARE=build, можно видеть только CFLAGS общего назначения:

======= CFLAGS     = '-I. -g -O3   '
======= LDFLAGS    = ''
======= ARCH_FLAGS = ''

Если же мы проведем сборку для Generic Intel x86_64 машины:

HARDWARE=intel-pc64 make

то увидим флаги:

======= CFLAGS     = '-I. -g -O3 -march=x86-64 -D__X86_64_GLIBC__=1 -D__HARDWARE__=0002 -DINTEL_PC64=0002 ...'
======= ARCH_FLAGS = '-march=x86-64'

которые говорят об оптимизации кода для архитектуры x86_64, а также предоставляют возможность условной компиляции, в зависимости от текущего целевого устройства. Более подробную информацию о декларациях для условной сборки можно получить в разделе Hardware IDs обзорной статьи.

Разумеется, пользователь может добавить собственные управления в свой сценарии сборки, например, следующим образом:

CFLAGS += -fomit-frame-pointer

Использование RUST

Мы приготовили два сценария сборки: первый показывает использование компилятора rustc непосредственно, а второй рассказывает о том, как можно использовать утилиту cargo с разными rust-toolchain‑ами.

RUSTC

Рассмотрим сценарий doc/examples/hello-rust/Makefile:


COMPONENT_TARGETS  = $(HARDWARE_INTEL_PC64)
COMPONENT_TARGETS += $(HARDWARE_BAIKAL_M1)

include ../../../build-system/constants.mk

# ======= __END_OF_REQUIRES__ =======

bin_src = main.rs

bin_target = $(TARGET_BUILD_DIR)/main

info_target = $(TARGET_BUILD_DIR)/.info-done

BUILD_TARGETS = $(bin_target) $(info_target)

#
# The user may reject the sysroot usage. For this the user have to declare
# the USE_TARGET_DEST_DIR_SYSROOT variable with value 'no':
#
#   USE_TARGET_DEST_DIR_SYSROOT = no
#
USE_TARGET_DEST_DIR_SYSROOT = no


include ../../../build-system/core.mk


ARCH = $(shell echo $(TARGET) | cut -f1 -d'-')

$(bin_target): $(bin_src)
	@echo "======= Build the `basename $(bin_target)` executable: ======="
	@$(RUSTC) --target $(TARGET) -C linker=$(CROSS_PREFIX)gcc $(bin_src) -o $(bin_target)
	@touch $@

$(info_target): $(bin_target)
	@echo "==================================="
	@echo "======= Environment:        ======="
	@echo "==================================="
	@echo "======= RUST_TOOLCHAIN = '$(RUST_TOOLCHAIN)'"
	@echo "======= CARGO_HOME     = '$(CARGO_HOME)'"
	@echo "======= RUSTUP_HOME    = '$(RUSTUP_HOME)'"
	@echo "======= RUSTC          = '$(RUSTC)'"
	@echo "======= RUSTDOC        = '$(RUSTDOC)'"
	@echo "======= RUSTFMT        = '$(RUSTFMT)'"
	@echo "======= CBINDGEN       = '$(CBINDGEN)'"
	@echo "======= RUST_PATH      = '$(RUST_PATH)'"
	@echo ""
	@echo "#"
	@echo "# Please find the `basename $(bin_target)` executable in the $(TARGET_BUILD_DIR)/ directory."
	@echo "#"
	@echo "# Run the `basename $(bin_target)` executable:"
	@echo ""
	@$(BUILDSYSTEM)/usr/bin/qemu-$(ARCH) \
	   $(TOOLCHAIN_PATH)/$(TARGET)/sys-root/$(shell $(PATCHELF) --print-interpreter $(bin_target) | sed 's,^\/,,') \
	   --library-path $(TOOLCHAIN_PATH)/$(TARGET)/lib:$(TOOLCHAIN_PATH)/$(TARGET)/sys-root/lib:$(TOOLCHAIN_PATH)/$(TARGET)/sys-root/usr/lib \
	   $(bin_target)
	@echo ""
	@touch $@

Команда сборки приложения "Здравствуй, Мир!" (main.rs) проста:

	@$(RUSTC) --target $(TARGET) -C linker=$(CROSS_PREFIX)gcc $(bin_src) -o $(bin_target)

Основным здесь является то, что компоновка исполняемого файла main происходит с помощью компилятора gcc, выбор которого осуществляется автоматически системой сборки исходя из текущего целевого устройства (HARDWARE_INTEL_PC64 или HARDWARE_BAIKAL_M1) списка COMPONENT_TARGETS, заданного вначале сценария doc/examples/hello-rust/Makefile.

Это обстоятельство отсылает нас к предыдущим рассуждениям о том, что вспомогательные toolchain‑ы LLVM и RUST не являются основными в парадигме GNU системы сборки Radix cross Linux. Это вполне естественно, так как Rust не имеет собственного компоновщика и может использовать либо gcc/ld, либо lld (из набора LLVM).

Разумеется выбор между gcc и lld для нас очевиден. Но пользователи, которые собирают собственные проекты с помошью нашей системы сборки могут с тем же успехом выбрать lld.

По поводу цели сборки $(info_target) надо сказать следующее.

Помимо перечисления значений переменных, определенных в файле build-system/sbin/.config, и напоминания о том, где искать результат сборки (main), здесь выполняется запуск приложения main с помощью Qemu.

Да, система сборки включает в свой состав Qemu, и для запуска приложения надо только выбрать интерпретатор и задать путь поиска системных библиотек.

Поскольку мы используем sys-root из состава toolchain‑а, наш выбор очевиден (строка 53):

	@$(BUILDSYSTEM)/usr/bin/qemu-$(ARCH) \
	   $(TOOLCHAIN_PATH)/$(TARGET)/sys-root/$(shell $(PATCHELF) --print-interpreter $(bin_target) | sed 's,^\/,,') \
	   --library-path $(TOOLCHAIN_PATH)/$(TARGET)/lib:$(TOOLCHAIN_PATH)/$(TARGET)/sys-root/lib:$(TOOLCHAIN_PATH)/$(TARGET)/sys-root/usr/lib \
	   $(bin_target)

Переменные TOOLCHAIN_PATH и TARGET задаются системой сборки в зависимости от текущего целевого устройства, а путь к интерпретатору мы получаем с помощью утилиты patchelf, которая входит в состав некоторых основных toolchain‑ов Radix cross Linux.

Имя суффикса утилиты qemu-$(ARCH) легко получить, взяв первый компонент целевого триплета:

ARCH = $(shell echo $(TARGET) | cut -f1 -d'-')

CARGO

Рассмотрим каталог проекта doc/examples/hello-cargo:

hello-cargo/
├── Makefile
└── package/
    ├── Cargo.toml
    └── src/
        └── main.rs

Здесь, помимо сценария сборки (Makefile), присутствует каталог package, в котором, кроме исходного файла на языке Rust (main.rs), существует файл проекта Cargo.toml. Мы представили здесь полный каталог проекта для простоты. Обычный сценарий сборки больших пакетов подразумевает: загрузку исходного проекта в виде архива, его распаковку в каталог $(TARGET_BUILD_DIR) и, уже затем, сборку результирующего кода. Здесь мы просто переместим исходные файлы проекта в каталог $(TARGET_BUILD_DIR) для последующей сборки.

Данные действия описаны простой командой cp (строка 39 сценария doc/examples/hello-cargo/Makefile).

Файл doc/examples/hello-cargo/Makefile построен по обычным правилам, но цель $(bin_target) достигается более сложным путем:


COMPONENT_TARGETS  = $(HARDWARE_INTEL_PC64)
COMPONENT_TARGETS += $(HARDWARE_BAIKAL_M1)

include ../../../build-system/constants.mk

# ======= __END_OF_REQUIRES__ =======

bin_src     = package/src/main.rs
cargo_toml  = package/Cargo.toml

build_dir   = $(TARGET_BUILD_DIR)/build
bin_target  = $(TARGET_BUILD_DIR)/main
info_target = $(TARGET_BUILD_DIR)/.info-done

cross_file  = $(CURDIR)/$(TARGET_BUILD_DIR)/$(TARGET)-config.toml

BUILD_TARGETS = $(bin_target) $(info_target)

#
# The user may reject the sysroot usage. For this the user have to declare
# the USE_TARGET_DEST_DIR_SYSROOT variable with value 'no':
#
#   USE_TARGET_DEST_DIR_SYSROOT = no
#
USE_TARGET_DEST_DIR_SYSROOT = no


include ../../../build-system/core.mk


ARCH = $(shell echo $(TARGET) | cut -f1 -d'-')

cargo-flags      = +$(RUST_TOOLCHAIN) --config $(cross_file)
cargo-target-dir = $(CURDIR)/$(TARGET_BUILD_DIR)/target
rust-target      = $(TARGET)

$(bin_target): $(cargo_toml) $(bin_src)
	@mkdir -p $(build_dir)/
	@cp -a package/* $(build_dir)/
	@echo "======= Create cross file: ======="
	@echo ''                                                  >  $(cross_file)
	@echo '[target.$(rust-target)]'                           >> $(cross_file)
	@echo 'rustflags = ['                                     >> $(cross_file)
	@echo '  "-C", "linker=$(CROSS_PREFIX)gcc",'              >> $(cross_file)
	@echo '  "--cap-lints", "allow",'                         >> $(cross_file)
	@echo ']'                                                 >> $(cross_file)
	#
	# RUSTFLAGS='-C linker=$(CROSS_PREFIX)gcc --cap-lints allow'
	#
	@echo "======= Build the `basename $(bin_target)` executable: ======="
	@cd $(build_dir) && CARGO_HOME=$(CARGO_HOME) \
	                    RUSTUP_HOME=$(RUSTUP_HOME) \
	                    CARGO_BUILD_RUSTC=$(RUSTC) \
	                    CARGO_BUILD_TARGET='$(rust-target)' \
	                    CARGO_TARGET_DIR='$(cargo-target-dir)' \
	                    $(CARGO) $(cargo-flags) build --verbose --release --bin main
	@cp $(TARGET_BUILD_DIR)/target/$(TARGET)/release/main $(TARGET_BUILD_DIR)/
	@touch $@

$(info_target): $(bin_target)
	@echo "==================================="
	@echo "======= Environment:        ======="
	@echo "==================================="
	@echo "======= RUST_TOOLCHAIN = '$(RUST_TOOLCHAIN)'"
	@echo "======= CARGO_HOME     = '$(CARGO_HOME)'"
	@echo "======= RUSTUP_HOME    = '$(RUSTUP_HOME)'"
	@echo "======= RUSTC          = '$(RUSTC)'"
	@echo "======= RUSTDOC        = '$(RUSTDOC)'"
	@echo "======= RUSTFMT        = '$(RUSTFMT)'"
	@echo "======= CBINDGEN       = '$(CBINDGEN)'"
	@echo "======= RUST_PATH      = '$(RUST_PATH)'"
	@echo ""
	@echo "#"
	@echo "# Please find the `basename $(bin_target)` executable in the $(TARGET_BUILD_DIR)/ directory."
	@echo "#"
	@echo "# Run the `basename $(bin_target)` executable:"
	@echo ""
	@$(BUILDSYSTEM)/usr/bin/qemu-$(ARCH) \
	   $(TOOLCHAIN_PATH)/$(TARGET)/sys-root/$(shell $(PATCHELF) --print-interpreter $(bin_target) | sed 's,^\/,,') \
	   --library-path $(TOOLCHAIN_PATH)/$(TARGET)/lib:$(TOOLCHAIN_PATH)/$(TARGET)/sys-root/lib:$(TOOLCHAIN_PATH)/$(TARGET)/sys-root/usr/lib \
	   $(bin_target)
	@echo ""
	@touch $@

После копирования исходного проекта в каталог build_dir = $(TARGET_BUILD_DIR)/build, мы создаем файл описания управлений компилятором rustc для текущего целевого устройства. Данный файл мы создаем динамически, так как имеем два целевых устройства, для которых предназначен один и тот же сценарий сборки doc/examples/hello-cargo/Makefile. Например, для платы TF307 на базе процессора Baikal-M1000, файл управлений

cross_file  = $(CURDIR)/$(TARGET_BUILD_DIR)/$(TARGET)-config.toml

будет создан в каталоге .m1000-glibc/baikal-m1/ и будет иметь имя aarch64-m1000-linux-gnu-config.toml, в соответствие целевому триплету TARGET=aarch64-m1000-linux-gnu.

После выполнения команды:

	@cd $(build_dir) && CARGO_HOME=$(CARGO_HOME) \
	                    RUSTUP_HOME=$(RUSTUP_HOME) \
	                    CARGO_BUILD_RUSTC=$(RUSTC) \
	                    CARGO_BUILD_TARGET='$(rust-target)' \
	                    CARGO_TARGET_DIR='$(cargo-target-dir)' \
	                    $(CARGO) $(cargo-flags) build --verbose --release --bin main

результирующий исполняемый файл main окажется в каталоге:

$(TARGET_BUILD_DIR)/target/$(TARGET)/release/main

но мы перенесем его прямо в каталог $(TARGET_BUILD_DIR)/ для того, чтобы не бродить длинными путями при выполнении цели $(info_target).

Команда вызова утилиты cargo составлена нами исходя из стандартной документации и здесь нужно лишь обратить внимание на переменные CARGO_HOME, RUSTUP_HOME, RUSTC и CARGO. Ранее мы говорили о том, что все эти переменные определены в файле build-system/sbin/.config во время подключения rust-toolchain‑а к системе сборки.

Поскольку утилита cargo может использовать разные toolchain‑ы, в строке 33 нашего сценария сборки:

cargo-flags = +$(RUST_TOOLCHAIN) --config $(cross_file)

мы задействовали еще одну переменную RUST_TOOLCHAIN, значением которой является имя нашего toolchain‑а RUST. Это имя мы выбрали во время выполнения команды:

	@CARGO_HOME=$(CARGO_HOME) RUSTUP_HOME=$(RUSTUP_HOME) PATH=$(CARGO_HOME)/bin:$PATH \
	  rustup toolchain link RcL-$(version)-x86_64-unknown-linux-gnu $(TOOLCHAIN_PATH)

когда готовили систему сборки к работе (строки 57,58 файла build-system/3pp/app/rust/1.82.0/Makefile)

Переменная RUST_TOOLCHAIN, также определена в файле build-system/sbin/.config.

Значения всех переменных, декларированных системой сборки, выводятся на терминал при выполнении цели $(info_target) нашего сценария сборки doc/examples/hello-cargo/Makefile.

LLVM

Сценарии doc/examples/hello-llvm-gcc/Makefile и doc/examples/hello-llvm-lld/Makefile читатели могут рассмотреть самостоятельно. Разница между ними лишь в используемом компоновщике исполняемого файла main утилиты "Здравствуй, Мир!".

Следует обратить внимание на то, что в файле build-system/sbin/.config, для набора LLVM, заданы лишь две переменные:

LLVM_CONFIG        := /opt/toolchains/LLVM/18.1.8/bin/llvm-config
LLVM_PATH          := /opt/toolchains/LLVM/18.1.8/bin

Первая указавает на утилиту llvm-config. С ее помошью можно получить все необходимые данные о компиляторах clang/clang++ и управлениях для сборки программ.

Вторая переменная LLVM_PATH задана на случай, если пользователь решит переопределить системную переменную PATH, добавив к ее значению путь поиска clang/clang++:

PATH := $(LLVM_PATH):$(PATH)

в собственных Make-файлах.

На этом, наверное, стоит закончить данное повествование. Если у читателя возникнут дополнительные вопросы, мы всегда будем на связи в Telegram. Кроме того, на сайте Radix cross Linux есть форма обратной связи, посредством которой можно в любой момент обратиться за поддержкой.


В следующей статье мы постараемся разобрать пример использования системы сборки Radix cross Linux для создания ПО микроконтроллеров, например, таких как Longan Nano на базе микроконтроллера GD32VF103 от компании GigaDevice.