User's Makefile

С точки зрения GNU Make, пользовательские Make-файлы ничем не отличаются от обычных Make-файлов. Функции и утилиты Системы Сборки лишь упрощают разработку, а соблюдение рекомендаций, касающихся структуры Make-файлов, позволяет автоматизировать работу по созданию новых и редактированию существующих Make-файлов проекта.

Component Targets

Структура пользовательских Make-файлов достаточно проста. Первое представление о том, какие основные секции должны быть представлены в любом Make-файле, мы дали во вводной статье, рассматривая пример "Hello, World". Рассмотрим теперь более подробно основные секции пользовательских Make-файлов на примере сборки пакета sox (universal sound converter, player, and recorder).

Основной целью сборки любого продукта, в общем случае, является программа, способная работать на конкретном целевом устройстве. Пользовательские Make-файлы создаются для того, чтобы собрать некую программу для работы на некотором устройстве. Один и тот же Make-файл может быть предназначен для сборки одной и той же программы для целой линейки устройств, имеющих разную архитектуру.

Поэтому система сборки оперирует понятиями toolchain и hardware. Toolchain, естественно, является средством, а hardware, в свою очередь, является целью. Слово цель в понимании системы сборки, фактически, представляет собой идентификатор целевого устройства и, согласно этому идентификатору, система сборки выполняет целый ряд действий. Кратко охарактеризовать эти действия можно следующим. Система сборки выбирает необходимые средства и настраивает окружение для того, чтобы исходный текст программы транслировать в объектный код, способный работать на целевом устройстве, имеющем соответствующий идентификатор.

Иными словами, пользователь создает Make-файл общего вида, назначает целевые устройства, а система сборки выбирает нужный toolchain и настраивает окружение таким образом, чтобы стандартные действия, описанные в пользовательском Make-файле, привели к созданию конечной программы.

Ключевой фразой предыдущего повествования является: "назначает целевые устройства", то есть определяет список идентификаторов целевых устройств, для которых и разрабатывался данный Make-файл.

Итак, в начале любого Make-файла должен быть определен список целевых устройств, например:

COMPONENT_TARGETS  = $(HARDWARE_PC32)
COMPONENT_TARGETS += $(HARDWARE_PC64)
COMPONENT_TARGETS += $(HARDWARE_CB1X)
COMPONENT_TARGETS += $(HARDWARE_CB3X)
COMPONENT_TARGETS += $(HARDWARE_FFRK3288)
COMPONENT_TARGETS += $(HARDWARE_VIP1830)
COMPONENT_TARGETS += $(HARDWARE_BEAGLE)
COMPONENT_TARGETS += $(HARDWARE_OMAP5UEVM)
COMPONENT_TARGETS += $(HARDWARE_B74)
COMPONENT_TARGETS += $(HARDWARE_CI20)

Поскольку конечной целью отдельного Make-файла является некий компонент создаваемой системы, данный список носит название COMPONENT_TARGETS.

Для того чтобы далее в тексте Make-файла, были доступны константы, определенные в системе сборки, необходимо включить файл build-system/constants.mk. При включении необходимо использовать относительный путь. Так, если наше дерево исходных текстов выглядит следующим образом:

   .
   ├── app
   │   └── sox
   │        └── 14.4.1
   │            └── Makefile
   │
   └── build-system

то директива include должна быть записана как

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

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

Requires

В разделе requires допускается определение списка SOURCE_REQUIRES и, при необходимости, списка REQUIRES. Данные списки определяют зависимости пакета, во-первых, от исходных текстов и, во-вторых, от других компонентов создаваемого продукта, но уже на стадии работы.

Раздел должен заканчиваться строкой, содержащей специальный идентификатор __END_OF_REQUIRES__, например,

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

вне зависимости от того, присутствует список REQUIRES в Make-файле, или нет.

Списки межпакетных зависимостей REQUIRES можно формировать с использованием условных директив GNU Make, в зависимости от целевого устройства или текущего toolchain-a, используя переменные системы сборки HARDWARE и TOOLCHAIN соответственно. Более того, если использован механизм FLAVOURS, в пользовательских Make-файлах доступна переменная FLAVOUR, обозначающая текущий вариант целевого устройства, и при составлении условных списков можно поступать, например, следующим образом.

COMPONENT_TARGETS  = $(HARDWARE_PC32)
COMPONENT_TARGETS += $(HARDWARE_PC64)

FLAVOURS = de fr

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

ifeq ($(FLAVOUR),de)
REQUIRES = base/examples/hello-dll^de
endif
ifeq ($(FLAVOUR),fr)
REQUIRES = base/examples/hello-dll^fr
endif
ifeq ($(FLAVOUR),)
REQUIRES = base/examples/hello-dll
endif

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

Данный пример, помимо всего сказанного, демонстрирует использование механизма FLAVOURS системы сборки, а именно то, что ссылка на вариант целевого устройства осуществляется с помошью символа '^'. Разумеется, здесь механизм FLAVOURS показан не на примере какого-либо варианта целевого устройства, а, для простоты, на примере создания различных локализаций одного и того же приложения. Однако это не умаляет сути и назначения механизма FLAVOURS.

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

Source Requires

Список SOURCE_REQUIRES, если он определен, должен содержать как минимум один каталог, например,

SOURCE_REQUIRES = sources/packages/m/sox

в котором осуществляется загрузка архива с исходными текстами собираемого пакета. Если данный список не определен, то разработчик Make-файла должен самостоятельно позаботиться о месте размещения исходных текстов. Как правило, если речь идет о сборке пакетов сторонних производителей, то список SOURCE_REQUIRES не бывает пустым.

Вообще при построении дистрибутивов актуальной является не только задача получения архивов исходных пакетов от сторонних производителей, но и задача создания и сохранения изменений к этим пакетам. При создании платформы Radix.Linux были приняты следующие правила:

  1. Каталог sources содержит Make-файлы, осуществляющие загрузку исходных пакетов и, при необходимости, создание изменений к ним.
  2. Приготовленные изменения никогда не хранятся в репозитории в виде patch-файлов. Вместо этого сохраняется способ приготовления необходимых изменений в соответствующем подкаталоге.
  3. Имя подкаталога, где происходит создание patch-файла, должно соответствовать шаблону create-*-patch.

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

Для того чтобы лучше разобраться в принципах работы Make-файлов каталога sources надо рассмотреть некоторые файлы репозитория platform. Например, Make-файл, расположенный в каталоге sources/packages/a/acl. А также файл PATCHES , где используется patch-файл, приготовленный в каталоге sources/packages/a/acl.

Runtime Requires

Как мы говорили ранее, вслед за списками зависимостей необходимо ввести специальный идентификатор, обозначающий завершение раздела Requires.

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

Этот идентификатор необходим для того, чтобы сократить время сбора зависимостей. Дело в том, что пользователь может задавать разные списки зависимостей для разных целевых устройств, пользуясь условными директивами GNU Make. Система сборки при этом не сможет получить список зависимостей простым разбором Make-файла, то есть список будет окончательно сформирован лишь на этапе работы утилиты Make. Поэтому для получения списков зависимостей система сборки "проигрывает" пользовательские Make-файлы в отладочном режиме GNU Make и, тем самым, предоставляет дополнительные возможности в создании Make-файлов, поддерживающих различные конфигурации создаваемой системы. Такое отладочное проигрывание Make-файлов занимает большое количество времени, но лишь в том случае, если файл проигрывается целиком. Поскольку проигрывание файлов целиком на этапе сбора зависимостей не нужно, используется специальный идентификатор __END_OF_REQUIRES__, дающий возможность сократить время. Экономия времени здесь настолько ощутима, что отказаться от нее просто невозможно. Например, при полном проигрывании Make-файлов сбор межпакетных зависимостей для системы, содержащей 700 пакетов и собираемой на 10 целевых устройств, составляет несколько часов (3 - 4 часа на Intel Core i7). Напротив, при частичном проигрывании Make-файлов такой сбор занимает не более трех – четырех секунд.

Такой механизм сбора зависимостей используется только для списков межпакетных зависимостей. Списки SOURCE_REQUIRES обрабарываюся простым разбором Make-файлов, поскольку один и тот же пакет собирается из одних и тех же исходных кодов, а части кода, зависимые от архитектуры целевого устройства, обрабатываются уже на уровне самого пакета и его собственной системы сборки.

Наверное, здесь будет уместным сказать несколько слов о зависимостях пакетов друг от друга. Дело в том, что процесс построения межпакетных зависимостей на данный момент не может быть формализован. Другими словами, нельзя создать формальный алгоритм автоматического построения дерева зависимостей. Поэтому система сборки предоставляет лишь механизм, с помощью которого на основании авторских списков зависимостей можно автоматически определять порядок сборки и инсталляции пакетов в систему и, кроме того, строить деревья, подобные тому, что показано во вводной статье.

Service Variables

Данная секция содержит определения переменных, необходимых для работы функций системы сборки, таких как UNPACK_SRC_ARCHIVE и APPLY_PATCHES. Кроме того, здесь определяются переменные, служащие в качестве целей с точки зрения утилиты Make, а также задается список patch-файлов, которые, при необходимости, накладываются на распакованный исходный архив.

Рассмотрим эти переменные.

version            = 14.4.1
tar_bz2_archive    = $(SRC_PACKAGE_PATH)/packages/m/sox/sox-$(version).tar.bz2
SRC_ARCHIVE        = $(tar_bz2_archive)
SRC_DIR            = $(TARGET_BUILD_DIR)/sox-$(version)
src_dir_name       = sox-$(version)
src_done           = $(TARGET_BUILD_DIR)/.source_done

PATCHES = PATCHES

build_dir          = $(TARGET_BUILD_DIR)/build
build_target       = $(TARGET_BUILD_DIR)/.build_done
install_target     = $(TARGET_BUILD_DIR)/.install_done

Здесь переменные SRC_ARCHIVE, SRC_DIR и PATCHES являются ключевыми переменными системы сборки. Их значения фактически являются аргументами функций UNPACK_SRC_ARCHIVE и APPLY_PATCHES. Если исходный пакет не требует изменений, то переменную PATCHES можно опустить или оставить ее значение пустым.

Переменная PATCHES задает произвольное имя файла, содержащего список накладываемых изменений, то есть patch-файлов. Все patch-файлы в списке должны быть заданы относительно текущего каталога, а именно каталога, в котором расположен Make-файл. Поскольку пакет sox не требует наложения изменений, рассмотрим списки patch-файлов на примере пакета bzip2:

   .
   ├── app
   │   └── bzip2
   │       └── 1.0.6
   │           ├── Makefile
   │           └── PATCHES
   │
   └── build-system

Здесь файл PATCHES содержит строку

../../../sources/packages/a/bzip2/patches/bzip2-1.0.6-cross.patch -p0

определяющую относительный путь к patch-файлу и управление -p0 утилиты patch. Нетрудно видеть, что patch-файл был приготовлен в каталоге sources/packages/a/bzip2 и помещен в подкаталог patches для временного хранения.

Переменная SRC_ARCHIVE определяет путь к исходному архиву, который будет распакован в каталог $(SRC_DIR) посредством функции UNPACK_SRC_ARCHIVE системы сборки. Заметим здесь, что переменная SRC_DIR определена как

SRC_DIR = $(TARGET_BUILD_DIR)/sox-$(version)

то есть задает имя каталога уже распакованного архива, а не просто каталог $(TARGET_BUILD_DIR). Дело в том, что функция UNPACK_SRC_ARCHIVE распаковывает исходный архив в базовый каталог, то есть целевым каталогом для распаковки является по существу `dirname $(SRC_DIR)`. Сделано это для удобства и вот почему. Как правило, исходные архивы содержат всего лишь один каталог, имя которого задается формулой package-version и, учитывая это, удобно в качестве значения переменной SRC_DIR иметь имя каталога, непосредственно содержащего исходные файлы пакета. Это дает возможность использовать переменную SRC_DIR и как указатель на место распаковки архива, и как действительный путь к исходным файлам.

Забегая вперед скажем, что использование переменных SRC_ARCHIVE, SRC_DIR, PATCHES и функций UNPACK_SRC_ARCHIVE, APPLY_PATCHES делает приготовление исходных текстов для последующей сборки очень простым и абсолютно идентичным для всех пользовательских Make-файлов:

src_done = $(TARGET_BUILD_DIR)/.source_done

$(src_done): $(SRC_ARCHIVE) $(PATCHES_DEP)
       $(UNPACK_SRC_ARCHIVE)
       $(APPLY_PATCHES)
       @touch $@

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

$(SRC_DIR): $(SRC_ARCHIVE) $(PATCHES_DEP)
       $(UNPACK_SRC_ARCHIVE)
       $(APPLY_PATCHES)
       @touch $@

Использование каталога в качестве цели гарантированно приведет к ошибкам, особенно при параллельной сборке, потому что утилита Make не может корректно зафиксировать время создания каталога.

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

build_target     = $(TARGET_BUILD_DIR)/.build_done
install_target   = $(TARGET_BUILD_DIR)/.install_done

Далее в нашем примере они понадобятся для указания основных целей Make-файла.

Package Information

Для того чтобы автоматизировать приготовление целевого пакета, необходимо задать ряд переменных:

PKG_GROUP = app

SOX_PKG_NAME                = sox
SOX_PKG_VERSION             = 14.4.1
SOX_PKG_ARCH                = $(TOOLCHAIN)
SOX_PKG_DISTRO_NAME         = $(DISTRO_NAME)
SOX_PKG_DISTRO_VERSION      = $(DISTRO_VERSION)
SOX_PKG_GROUP               = $(PKG_GROUP)
###                          |---handy-ruler-------------------------------|
SOX_PKG_SHORT_DESCRIPTION   = universal sound sample translator
SOX_PKG_URL                 = $(BUG_URL)
SOX_PKG_LICENSE             = GPLv2
SOX_PKG_DESCRIPTION_FILE    = $(TARGET_BUILD_DIR)/$(SOX_PKG_NAME)-pkg-description
SOX_PKG_DESCRIPTION_FILE_IN = $(SOX_PKG_NAME)-pkg-description.in
SOX_PKG_INSTALL_SCRIPT      = $(SOX_PKG_NAME)-pkg-install.sh

Имена переменных имеют три компонента, разделенных символом подчеркивания ('_'). Начинаются имена с необязательного префикса, обозначающего имя пакета, далее идет обязательный компонент имени PKG, затем суффикс, который и представляет действительное назначение переменной. Данное соглашение мы рассмотрим позже, в разделе Collecting Requires.

Префикс имени может быть любым, то есть не требуется его уникальность в рамках всего проекта. Так, например, во всех Make-файлах каталога X11/X.org подобные переменные начинаются с префикса XORG, это удобно для автоматического генерирования шаблонов Make-файлов.

Важно отметить, что значения переменных

SOX_PKG_NAME                = sox
SOX_PKG_VERSION             = 14.4.1
SOX_PKG_SHORT_DESCRIPTION   = universal sound sample translator
SOX_PKG_LICENSE             = GPLv2

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

SOX_PKG_NAME                = $(pkg_name)
SOX_PKG_VERSION             = $(version)

категорически не допускаются. Связано это с тем, что во время сбора межпакетных зависимостей, а также во время построения деревьев межпакетных зависимостей функции системы сборки читают Make-файлы напрямую для получения значений переменных

*_PKG_NAME
*_PKG_VERSION
*_PKG_SHORT_DESCRIPTION
*_PKG_LICENSE

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

Отдельного рассмотрения требуют переменные, задающие длинное описание пакета и скрипт инсталляции, назначение которых мы рассматривали в разделе Package Tools.

У пользователя может возникнуть необходимость предварительного редактирования этих файлов перед тем, как они будут расположены в корневом каталоге целевого пакета. Поэтому лучше всего располагать шаблоны данных файлов, например, следующим образом.

   app
   └── sox
       └── 14.4.1
           ├── Makefile
           ├── PATCHES
           ├── sox-pkg-description.in
           └── sox-pkg-install.sh

Здесь файл sox-pkg-description.in является шаблоном, о чем свидетельствует суффикс .in, а файл sox-pkg-install.sh всего лишь требует копирования, разумеется ему долен быть присвоен атрибут исполняемого файла.

В Make-файле соотвествующие переменные заданы как

SOX_PKG_DESCRIPTION_FILE    = $(TARGET_BUILD_DIR)/$(SOX_PKG_NAME)-pkg-description
SOX_PKG_DESCRIPTION_FILE_IN = $(SOX_PKG_NAME)-pkg-description.in
SOX_PKG_INSTALL_SCRIPT      = $(SOX_PKG_NAME)-pkg-install.sh

И далее для приготовления целевого пакета используются следующие процедуры:

$(SOX_PKG_DESCRIPTION_FILE): $(SOX_PKG_DESCRIPTION_FILE_IN)
       @cat $< | $(SED) -e "s/@VERSION@/$(version)/g" > $@

$(pkg_archive):   .   .   .
       @cp $(SOX_PKG_DESCRIPTION_FILE) $(SOX_PKG)/.DESCRIPTION
       @cp $(SOX_PKG_INSTALL_SCRIPT) $(SOX_PKG)/.INSTALL

Отсюда видно, что в шаблон длинного описания пакета подставляется актуальная версия пакета, и лишь затем происходит копирование файла в корневой каталог целевого пакета.

Main Targets Definition

В разделе Package Tools мы описали процедуру создания пакетов, где говорили о необходимости создания временного каталога для размещения файлов пакета. При создании пакета с помощью Make-файла имя этого временного каталога лучше всего определить с помошью соответствующей переменной:

SOX_PKG = $(CURDIR)/$(TARGET_BUILD_DIR)/$(SOX_PKG_NAME)-package

Нам также необходимо задать базовое имя результирующего пакета:

pkg_basename = $(SOX_PKG_NAME)-$(SOX_PKG_VERSION)-$(SOX_PKG_ARCH)-$(SOX_PKG_DISTRO_NAME)-$(SOX_PKG_DISTRO_VERSION)

Утилита make-package создает три файла: архив пакета, файл, содержащий sha-сумму, и файл, содержащий текстовое описание пакета. Для удобства задания основных целей система сборки предоставляет несколько функций, с помошью которых можно легко создать списки целевых файлов:

pkg_archive     = $(TARGET_BUILD_DIR)/$(PKG_GROUP)/$(pkg_basename).$(pkg_arch_suffix)
pkg_signature   = $(call sign-name,$(pkg_archive))
pkg_description = $(call desc-name,$(pkg_archive))
products        = $(call pkg-files,$(pkg_archive))

Мы не будем здесь подробно описывать работу данных функций. Они определены в файле constants.mk и рассмотрены во вводной статье. Пользователь может самостоятельно разобраться в их назначении. Главным приемуществом использования данных функций, в отличие от ручного определения списков, является то, что если возникнет необходимость изменить расширения имен файлов, создаваемых утилитой make-package, не понадобится изменять все пользовательские Make-файлы проекта, а достаточно будет изменить функции в файле constants.mk.

Итак, мы подошли к определению основных целей, с точки зрения системы сборки, а именно:

BUILD_TARGETS     = $(build_target)
BUILD_TARGETS    += $(install_target)

PRODUCT_TARGETS   = $(products)

ROOTFS_TARGETS    = $(pkg_archive)

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

Во вводной статье мы уже рассматривали списки основных целей. Здесь же нам остается разобрать приготовление трех целей Make-файла, которые вошли в списки целей системы сборки, а именно цели: $(build_target), $(install_target) и $(pkg_archive).

Main Targets Building

Первой целью является сборка исходного пакета и в общем случае, когда пакет построен на основе autotools, достаточно проста:

$(build_target): $(src_done)
       @mkdir -p $(build_dir)
       @cd $(build_dir) && \
          $(BUILD_ENVIRONMENT) ../$(src_dir_name)/configure \
          --prefix=/usr               \
          --build=$(BUILD)            \
          --host=$(TARGET)            \
          $(extra_configure_switches)
       @cd $(build_dir) && $(BUILD_ENVIRONMENT) $(MAKE)
       @touch $@

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

Installation

При выполнении цели $(install_target) основной задачей является заполнение временного каталога целевого пакета $(SOX_PKG). Для этого мы используем переменную DESTDIR:

env_sysroot = DESTDIR=$(SOX_PKG)

$(install_target): $(build_target)
       @mkdir -p $(SOX_PKG)
       @cd $(build_dir) && $(BUILD_ENVIRONMENT) $(MAKE) install $(env_sysroot)

Для исходных пакетов, использующих autotools, это стандартный способ указания целевого каталога для инсталляции.

В принципе, если мы инсталлируем пакет на рабочую машину, стандартная процедура make install достаточна и не требует более никаких действий с нашей стороны. Однако в нашем случае это далеко не все.

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

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

Система Radix.Linux принимает следующие соглашения по документации, которая должна входить в поставку. Во-первых, лицензии, сопровождающие пакет, должны быть разположены в каталоге /usr/doc/$(pkgname)-$(version) целевой файловой системы. Во-вторых, вся документация, не относящаяся к стандартной, должна располагаться в каталоге /usr/share/doc/$(pkgname)-$(version). В-третьих, стандартная документация должна быть инсталлирована согласно FHS. Кроме того, man-pages и info-файлы должны быть архивированы. При создании пакета sox, для обеспечения этих требований, мы выполнили следующие действия:

$(install_target): $(build_target)
       @mkdir -p $(SOX_PKG)
       @cd $(build_dir) && $(BUILD_ENVIRONMENT) $(MAKE) install $(env_sysroot)
       # ======= Install Documentation =======
       @if [ -d $(SOX_PKG)/usr/share/man ] ; then \
         ( cd $(SOX_PKG)/usr/share/man ; \
           for manpagedir in `find . -type d -name "man*"` ; do \
             ( cd $$manpagedir ; \
               for eachpage in `find . -type l -maxdepth 1` ; do \
                 ln -s `readlink $$eachpage`.gz $$eachpage.gz ; \
                 rm $$eachpage ; \
               done ; \
               gzip -9 *.?  ; \
             ) \
           done \
         ) \
        fi
       @mkdir -p $(SOX_PKG)/usr/doc/$(src_dir_name)
       @cp -a $(SRC_DIR)/AUTHORS $(SRC_DIR)/COPYING \
              $(SOX_PKG)/usr/doc/$(src_dir_name)
       @mkdir -p $(SOX_PKG)/usr/share/doc/$(src_dir_name)
       @( cd $(SRC_DIR) ; \
          cp -a AUTHORS COPYING INSTALL LICENSE* NEWS README \
                $(SOX_PKG)/usr/share/doc/$(src_dir_name) \
        )
       @rm -f $(SOX_PKG)/usr/share/doc/$(src_dir_name)/FLAC.tag
       @( cd $(SRC_DIR) ; \
          if [ -r ChangeLog ] ; then \
            DOCSDIR=`echo $(SOX_PKG)/usr/share/doc/$(src_dir_name)` ; \
            cat ChangeLog | head -n 1000 > $$DOCSDIR/ChangeLog ; \
            touch -r ChangeLog $$DOCSDIR/ChangeLog ; \
          fi \
        )

Поскольку при сборке пакета был использован кросс-компилятор, настроенный на каталог TARGET_DEST_DIR в качестве системного, инсталлированные скрипты компоновщика (файлы с расширением .la) содержат неправильные для целевой системы пути поиска библиотек. Исправить эту ситуацию – необходимо.

В нашем случае достаточно простого удаления лишних каталогов из полных имен файлов:

       # ======= remove target path from target libtool *.la files =======
       @( cd $(SOX_PKG)/usr/lib$(LIBSUFFIX) ; \
          sed -i "s,$(TARGET_DEST_DIR),,g" libsox.la \
        )

Теперь в каталоге $(SOX_PKG) мы имеем образ пакета, который целиком и полностью пригоден для работы в создаваемой операционной среде.

Далее, необходимо позаботиться о кросс-окружении, а именно, об инсталляции содержимого пакета в каталог $(TARGET_DEST_DIR). Система сборки предусматривает функцию install-into-devenv, единственным аргументом которой является исходный каталог. Поэтому данная функция проста в использовании:

       # ======= Install the same to $(TARGET_DEST_DIR) =======
       $(call install-into-devenv, $(SOX_PKG))

Мы настоятельно рекомендуем использовать функцию install-into-devenv, а не осуществлять простое копирование файлов, как показано на следующем листинге.

       # ======= Install the same to $(TARGET_DEST_DIR) =======
       @mkdir -p $(TARGET_DEST_DIR)
       @cd $(SOX_PKG) && cp -rf * $(TARGET_DEST_DIR)

В отличие от простого копирования, функция install-into-devenv не только копирует файлы, но и ведет список инсталлированных файлов, сохраняя его в файле $(TARGET_BUILD_DIR)/.dist. Файл $(TARGET_BUILD_DIR)/.dist используется при выполнении команды:

$ make dist_clean

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

Итак, воспользовавшись функцией install-into-devenv, мы расширили возможности нашего кросс-окружения, однако опять, но уже в кросс-окружении, мы получили некорректные скрипты компоновщика (файлы с расширением .la) и конфигурационные файлы утилиты pkg-config (файлы с расширением .pc). Если не позаботиться об изменении этих файлов, то при сборке следующих пакетов, зависящих от данного, мы получим множество ошибок. Причем поиск ошибок в условиях, когда в системе несколько сотен пакетов, будет весьма затруднителен. Устранить возможные проблемы нам, как всегда, поможет редактор sed.

       # ======= tune libtool *.la search path to the target destination for development =======
       @( cd $(TARGET_DEST_DIR)/usr/lib$(LIBSUFFIX) ; \
          sed -i "s,/usr,$(TARGET_DEST_DIR)/usr,g" libsox.la ; \
          sed -i "s,L/lib,L$(TARGET_DEST_DIR)/lib,g" libsox.la \
        )
       # ======= tune pkg-config *.pc search path to the target destination for development =======
       @( cd $(TARGET_DEST_DIR)/usr/lib$(LIBSUFFIX)/pkgconfig ; \
          sed -i "s,/usr,$(TARGET_DEST_DIR)/usr,g" sox.pc \
        )

В завершение процесса настройки, можно удалить ненужную информацию из объектных файлов целевого пакета:

       # ======= Strip binaries =======
       @( cd $(SOX_PKG) ; \
          find . | xargs file \
                 | grep "executable" \
                 | grep ELF \
                 | cut -f 1 -d ':' \
                 | xargs $(STRIP) --strip-unneeded 2> /dev/null ; \
          find . | xargs file \
                 | grep "shared object" \
                 | grep ELF \
                 | cut -f 1 -d ':' \
                 | xargs $(STRIP) --strip-unneeded 2> /dev/null ; \
          find . | xargs file \
                 | grep "current ar archive" \
                 | cut -f 1 -d ':' \
                 | xargs $(STRIP) -g 2> /dev/null \
        )
       @touch $@

Package Creation

Заключительной целью является создание пакета. Данная процедура проста и, практически, одинакова для всех пакетов.

$(pkg_signature)   : $(pkg_archive) ;
$(pkg_description) : $(pkg_archive) ;

$(pkg_archive): $(install_target) $(SOX_PKG_DESCRIPTION_FILE) $(SOX_PKG_INSTALL_SCRIPT)
       @cp $(SOX_PKG_DESCRIPTION_FILE) $(SOX_PKG)/.DESCRIPTION
       @cp $(SOX_PKG_INSTALL_SCRIPT) $(SOX_PKG)/.INSTALL
       @$(BUILD_PKG_REQUIRES) $(SOX_PKG)/.REQUIRES
       @echo "pkgname=$(SOX_PKG_NAME)"                            >  $(SOX_PKG)/.PKGINFO ; \
        echo "pkgver=$(SOX_PKG_VERSION)"                          >> $(SOX_PKG)/.PKGINFO ; \
        echo "arch=$(SOX_PKG_ARCH)"                               >> $(SOX_PKG)/.PKGINFO ; \
        echo "distroname=$(SOX_PKG_DISTRO_NAME)"                  >> $(SOX_PKG)/.PKGINFO ; \
        echo "distrover=$(SOX_PKG_DISTRO_VERSION)"                >> $(SOX_PKG)/.PKGINFO ; \
        echo "group=$(SOX_PKG_GROUP)"                             >> $(SOX_PKG)/.PKGINFO ; \
        echo "short_description=\"$(SOX_PKG_SHORT_DESCRIPTION)\"" >> $(SOX_PKG)/.PKGINFO ; \
        echo "url=$(SOX_PKG_URL)"                                 >> $(SOX_PKG)/.PKGINFO ; \
        echo "license=$(SOX_PKG_LICENSE)"                         >> $(SOX_PKG)/.PKGINFO
       @$(FAKEROOT) sh -c "cd $(SOX_PKG) && chown -R root:root . && $(MAKE_PACKAGE) --linkadd yes .."

Как видно из предыдущего листинга, мы просто создаем необходимые файлы и вызываем утилиту make-package.

Collecting Requires

При создании пакета для сбора межпакетных зависимостей мы использовали функцию системы сборки BUILD_PKG_REQUIRES. Вообще, в файле $(BUILDSYSTEM)/core.mk предусмотрено четыре переменных для вызова скрипта системы сборки $(BUILDSYSTEM)/build_pkg_requires с различными параметрами.

    BUILD_PKG_REQUIRES = $(BUILDSYSTEM)/build_pkg_requires $(REQUIRES)
BUILD_ALL_PKG_REQUIRES = $(BUILDSYSTEM)/build_pkg_requires --pkg-type=all $(REQUIRES)
BUILD_BIN_PKG_REQUIRES = $(BUILDSYSTEM)/build_pkg_requires --pkg-type=bin $(REQUIRES)
BUILD_DEV_PKG_REQUIRES = $(BUILDSYSTEM)/build_pkg_requires --pkg-type=dev $(REQUIRES)

Данные функции используются для построения файла .REQUIRES по списку $(REQUIRES), определенному в начале Make-файла.

Вызов скрипта $(BUILDSYSTEM)/build_pkg_requires без аргументов аналогичен вызову с аргументом --pkg-type=all, при этом осуществляется сбор зависимостей от пакетов любого типа. Последние два вызова осуществляют поиск пакетов типа bin и dev, согласно значению управления --pkg-type.

Здесь типы пакетов имеют нетрадиционное значение.

Обычно дистрибьюторы делят пакеты на бинарные и "пакеты для разработки" путем разделения пакета на две части. Первая, называемая бинарным пакетом, содержит исполняемые файлы и библиотеки, необходимые для работы в системе. Вторая, называемая "пакетом для разработки", содержит заголовочные и конфигурационные файлы, необходимые для создания других приложений, использующих объектные файлы бинарного пакета.

Платформа Radix.Linux строится на других принципах и не делит пакеты на части. Подход Radix.Linux состоит в следующем.

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

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

Если создается операционная среда для встроенных систем, то автор дистрибутива вправе решать, в каком объеме осуществлять поставку того или иного приложения. Например, после инсталляции пакета во временный каталог перед созданием целевого архива автор дистрибутива может удалить из него все заголовочные файлы, все скрипты компоновщика, все конфигурационные файлы утилиты pkg-config и тем самым уменьшить размер пакета. Кроме того, автор может вообще удалить некоторые объектные файлы, которые по его мнению не нужны на целевой машине.

В обоих случаях создаваемый пакет с точки зрения системы сборки является обычным пакетом.

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

Например, для сборки GNU Libc необходимы runtime-библиотеки компилятора GCC. Однако библиотеки компилятора для работы целевой системы могут быть получены двумя способами. Первый способ подразумевает простое копирование runtime-библиотек из toolchain-а, который используется во время сборки системы, а второй способ заключается в сборке самого компилятора как самостоятельного пакета для целевой системы.

Пакет, созданный первым способом, необходим для сборки GNU Libc и, следовательно, должен быть инсталлирован в кросс-окружение до начала сборки GNU Libc. Однако вопрос об инсталляции данного пакета на целевую машину зависит от того, собираемся ли мы инсталлировать пакет gcc целиком. В любом случае, мы не хотим чтобы пакет GNU Libc зависел ни от пакета gcc-runtime, ни от пакета gcc. Ведь в противном случае для того чтобы обеспечить нормальную работу системного инсталлятора, который будет обязан соблюдать межпакетные зависимости, нам придется ставить пакет GNU Libc в зависимость от пакета gcc-runtime. Кроме того, мы будем обязаны разделить пакет gcc на два пакета: gcc-runtime и gcc-without-runtime, что само по себе противоестественно. Ну и, что более всего неприятно, в результате мы получим два совершенно одинаковых по именам, но разных по сути пакета gcc-runtime.

Не лучше ли просто знать, что пакет gcc-runtime получен простым копированием библиотек из toolchain-а, а пакет gcc содержит полный набор компиляторов коллекции GNU? Более того, так будет проще принимать решения на любом этапе создания, инсталлирования и использования дистрибутива.

Система сборки предоставляет возможность разрешения подобных коллизий. Рассмотрим это на примере все тех же пакетов gcc и gcc-runtime.

Пакету pkgtool присвоим тип bin, по сути он таковым и является. Для этого в файле base/pkgtool/Makefile имя переменной, задающей имя пакета, мы определили как

BIN_PKG_NAME = pkgtool

То есть средний компонент имени равен BIN_PKG.

Пакетам gcc-runtime и gcc присвоим тип dev:

GCC_RT_DEV_PKG_NAME = gcc-runtime

и

GCC_DEV_PKG_NAME = gcc

(см. dev/gcc-runtime/4.9.2/Makefile и dev/gcc/4.9.2/Makefile, соответственно).

Тип пакета GNU Libc здесь не имеет значения.

В файле libs/glibc/2.21/Makefile определим список зависимостей следующим образом:

REQUIRES   = dev/gcc-runtime/4.9.2
REQUIRES  += base/pkgtool

И воспользуемся функцией BUILD_BIN_PKG_REQUIRES для создания файла .REQUIRES:

$(pkg_archive):   . . .
              .
              .
              .
       @$(BUILD_BIN_PKG_REQUIRES) $(GLIBC_PKG)/.REQUIRES
              .
              .

Тогда в файл .REQUIRES будет записана лишь одна строчка:

pkgtool=1.0.6

то есть пакет GNU Libc, с точки зрения системного инсталлятора, зависит лишь от одного пакета pkgtool, который должен быть инсталлирован в систему до начала инсталляции пакета GNU Libc. Однако во время сборки все зависимости будут отработаны корректно, и runtime-библиотеки компилятора GCC попадут в кросс-окружение до начала компляции GNU Libc.

Resume

Итак, мы рассмотрели структуру типового Make-файла, использующего систему сборки, а также основные функции, которые позволяют автоматизировать рутинные операции. Разумеется, не все исходные пакеты, с которыми может столкнуться пользователь системы сборки, можно предусмотреть и описать. Однако система Radix.Linux уже насчитывает сотни пакетов, и пользователь всегда может найти интересующие его примеры в репозитории платформы.

В заключение необходимо сказать, что при создании больших проектов, содержащих тысячи Make-файлов, мы рекомендуем строго придерживаться одних и тех же шаблонов и алгоритмов для всех Make-файлов проекта. Особенно в условиях, когда коллектив разработчиков достаточно велик. Соблюдение простых и общепринятых правил позволит в дальнейшем избежать огромного количества ручной работы по изменению, в случае необходимости, Make-файлов, так как если все Make-файлы имеют одинаковую структуру, их редактирование можно автоматизировать с помощью sed и awk. Надо понимать, что создание пакетов программ – скорее не творческий, а весьма рутинный процесс, требующий повышенного внимания. Только скурпулезное выполнение простых требований может сократить время поиска ошибок в будущем. Неплохо также иметь CM-инженера с крепкими нервами, основным лозунгом которого станут, напрмер, слова: "программист имеет право на ошибку, но только на собственной ветке репозитория".