Компилирование приложений в GNU/Linux #

Васильев Андрей Михайлович, 2023

Версии презентации

Минимальное приложение #

Рассмотрим нативное приложение под операционную систему, main.c:

#include <stdio.h>

int main() {
    printf("Hello, world!\n");
    return 0;
}

Скомпилируем данное приложение:

gcc -o app -O3 main.c

Удостоверимся, что приложение работает

$ ./app
Hello, world!

Сборка разных типов приложений #

  • Часть языков программирования требуют шага компиляции при работе: Си, C++, Rust, Go, Java, Kotlin, C#, Zig, Nim и т.д
  • Часть языков программирования требуют упаковки исходных кодов, интерпретируемые языки программирования: Python, JavaScript/Node
  • Современные приложения обычно состоят из нескольких компонентов, написанных на нескольких языках программирования, которые надо объединить

Сборка и установка приложений #

В общем процесс сборки приложения можно «втиснуть» в следующую схему:

  1. Получение исходных кодов приложения
  2. Выяснение и установка зависимостей для сборки приложения
  3. Выполнение сборки приложения (шаг может быть пропущен для интерпретируемых языков программирования)

Процесс установки собранного приложения «вписываются» в следующую схему:

  1. Установка зависимостей для запуска приложения
  2. Размещение скомпилированных файлов приложения по целевым путям
    • Исполняемые файлы в PATH-каталоги, например в /usr/local/bin
    • Файлы ресурсов приложения например в /usr/local/share

Обе задачи можно решить с помощью систем сборок

Системы сборок #

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

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

Система сборки Make #

Классический инструмент сборки — Make, созданный в 74 году

Основные концепции Make:

  • Результат работы системы сборки — файл
  • Результаты получаются из исходных файлов
  • Если время изменения исходных файлов позже времени изменения результата, значит нужно выполнять сборку
  • Если время изменения файла-результата позже времени изменения исходных файлов, значит сборку выполнять не нужно
  • Результаты могут зависеть от других результатов
flowchart LR source_1[Исходный файл № 1] source_2[Исходный файл № 2] source_3[Исходный файл № 3] intermediate_1[Промежуточный результат № 1] intermediate_2[Промежуточный результат № 2] final[Результат] source_1 --> intermediate_1 source_2 --> intermediate_1 source_3 --> intermediate_2 intermediate_1 --> final intermediate_2 --> final

Синтаксис Makefile #

Makefile — конфигурационный файл для приложения Мake

цель: зависимости ...
    команды по сборке
  • Цель — путь к файлу-результату, который должен получиться
  • Зависимости — пути к файлам исходных кодов для цели
  • Команды по сборке — SH-команды, которые создают цель. Должны быть отделены символом табуляции

Компиляция приложения #

Сформируем Makefile для компиляции приложения

app: main.c
    cc -o app main.c

Запустим сборку приложения с помощью Мake:

$ make app

Больше возможностей Make #

Рассмотрим следующий Makefile

all: app

app: main.o
     gcc -o app main

main.o: main.c
     gcc -c main.c

clean:
     rm hello.o hello.exe
  • Процесс компиляции приложения состоит из фаз создания объектных файлов с дальнейшей их линковкой
  • all и clean предоставляет из себя фальшивые цели, они не создают файлы
  • При запуске make без аргументов будет выполнена первая фальшивая цель
  • Данный Makefile автоматизирует как создание файлов, так и их удаление

Другие возможности Make #

  • Переменные позволяют сформировать список исходных кодов только один раз
  • Автоматические переменные позволяют упросить написание SH-правил для выполнения действий
  • Шаблонные правила позволяют описать правила по преобразованию файлов одних типов файлов в другие
  • Встроенные шаблонные правила позволяют писать ещё меньше правил

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

На настоящий момент на большинстве ОС используется реализация Make от проекта GNU, GNU Make

Сложности современного мира #

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

  • Как называется компилятор языка Си/C++
  • Как правильно передавать ему необходимость использования нужной версии стандарта языка и другие аргументы
  • Находится ли в системе нужная библиотека
  • Как правильно подключать заголовочные файлы библиотеки и библиотечные файлы

В рамках логической структуры Make реализовать работу данных задач удобным образом достаточно сложно

Эти проблемы были актуальны уже для приложений, разрабатывающихся в конце 80-х годов (множество UNIX-подобных систем)

Генераторы Makefile #

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

flowchart LR gen_config[[Конфигурационный\nфайл генератора]] generator(Генератор\nMakefile) makefile[[Makefile]] gen_config --> generator generator --> makefile

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

Можно выделить следующие генераторы: GNU Autotools, CMake, Meson, SCons, QMake

Альтернативы #

  • Существуют другие инструменты сборки, например Ninja, Ant
  • Существуют интегрированные решения, выполняющие сборку приложений целиком, например Bazel, Gradle, build2 и т.п.

Работа Autotools #

  • Можно определить наличие системы сборки по файлам configure, configure.ac, aclocal.m4 и т.д.
  • Пользователю для сборки приложения достаточно:
    • Создать Makefile: ./configure (надо будет поставить все зависимости)
    • Запустить сборку: make (или быстрее с make -j)
    • Выполнить установку: su -c 'make install'
  • Разработчику предоставляются средства для указания:
    • Списка зависимостей приложения
    • Списка устанавливаемых файлов
    • Списка требований к компилятору
  • По умолчанию приложение будет установлено в каталог /usr/local, что можно изменить, указав префикс: ./configure --prefix ~/my-apps/
    • При установке в домашний каталог не надо повышать привилегии для установки приложения

Работа CMake #

flowchart TB cmake_config[CMakeLists.txt] cmake(cmake) makefile[Makefile] make(make) executable[application] cmake_config --> cmake cmake --> makefile makefile --> make make --> executable
  • Конфигурационный файл называется CMakeLists.txt
  • Для создания конфигурационного файла Make надо запустить cmake в каталоге вместе с CMakeLists.txt
  • Затем выполнить компиляцию и установку приложения: make && su -c 'make install'

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

mkdir build
cd build
cmake ../
cmake --build .
cmake --install . --prefix ~/my-apps

Установка зависимостей приложений #

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

checking for libpcre... no
configure: error: Package requirements (libpcre) were not met:

No package 'libpcre' found

Для компиляции приложения необходимо, чтобы в системе были установлены:

  • Бинарные файлы библиотек (необходимы и для дальнейшей работы)
  • Заголовочные файлы библиотек и подключаемые компилируемые компоненты

Заголовочные файлы поставляются в дистрибутивах отдельно от файлов библиотек

  • В AltLinux такие пакеты обычно имеют префикс lib и суффикс -devel
  • В Debian-подобных дистрибутивах имеют префикс lib и суффикс -dev

Альтернативные варианты ошибок #

Не найден заголовочный файл #

Можно воспользоваться поиском по содержимому пакетов с помощью apf или epm

Не найдена библиотека в pkg-config #

pkg-config — инструментарий для помощи при компиляции приложения

  • Позволяет разработчику-клиенту получить список аргументов к компилятору для подключения библиотеки
  • Позволяет разработчику библиотеки образом указать эти параметры
  • Конфигурационные файлы данного инструмента имеют расширение .pc
  • Большинство систем сборок имеют удобные средства интеграции с pkg-config
  • Можно легко поставить нужный пакет с использованием epm: epm install pkgconfig(history)

Окружение для сборки #

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

  • Приложение для генерации конфигурации системы сборки
  • Система сборки
  • Инструментарий для управления ключами сборки (например для подключения зависимостей pkg-config)
  • Дополнительные инструменты, используемые во время сборки приложения

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

Окружение для запуска #

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

Массовая установка приложений #

Установка приложений из исходных кодов интересна:

  • Разработчику приложения для проведения тестирования его работы в конкретном дистрибутиве
  • Пользователю, если приложение не доступно в репозитории дистрибутива
  • Пользователю, желающему получить более свежую версию ПО чем доступна в репозитории дистрибутива
  • Пользователю, желающему получить предыдущую версию ПО, недоступную из репозитория

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

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

Создание RPM-пакетов #

flowchart LR subgraph Пакет с исходными кодами sources[Исходные коды приложения] spec[Спецификация сборки] end rpmbuild(rpmbuild) rpm-file[RPM-архив приложения] sources --> rpmbuild spec --> rpmbuild rpmbuild --> rpm-file

Для выполнения сборки RPM-пакетов используется приложение rpmbuild

  • Устанавливает все необходимые зависимости для компиляции приложения
  • Собирает (компилирует) приложение в финальную форму
  • Формирует список зависимостей, которые необходимы для работы приложения
  • Собирает файлы и мета-информацию в один или несколько RPM-архивов

Локальная сборка RMP-пакета в ALT Linux #

Рассмотрим процесс создания пакета вручную, для этих целей используются

  • Gear — система для создания RPM-пакетов из git-репозитория
  • Hasher — инструмент для безопасной и воспроизводимой сборки пакетов
  • rpmbuild — низкоуровневый инструмент для выполнения сборки
  • rpmlint — инструмент базовой проверки ошибок при создании пакетов
  • patch — инструмент для внесения изменений в исходный код на основе файлов
  • rpmdevtools — набор инструментов, упрощающих сборку RPM-пакетов
  • buildreq — инструмент для поиска сборочных зависимостей
apt-get update
apt-get install gcc rpm-build rpmlint make python gear hasher patch \
    rpmdevtools

Дистрибутивы отличаются друг от друга выбираемыми инструментами и особенностями их работы

Рабочее пространство для сборки RPM-пакетов #

Чтобы создать дерево каталогов, которое является рабочей областью сборки RPM-пакетов, используйте приложение rpmdev-setuptree, или же создайте каталоги вручную, используя приложение mkdir:

$ rpmdev-setuptree
$ tree ~/rpmbuild/
/home/user/rpmbuild/
|-- BUILD
|-- RPMS
|-- SOURCES
|-- SPECS
 -- SRPMS
  • BUILD: Содержит все файлы, которые появляются при сборке пакета.
  • RPMS: Здесь формируются собранные RPM-пакеты (.rpm) в подкаталогах для разных архитектур, например, в подкаталогахx86_64 и noarch.
  • SOURCES: Здесь находятся архивы исходного кода и патчи. Утилита rpmbuild ищет их здесь.
  • SPECS: Здесь хранятся spec-файлы.
  • SRPMS: Здесь находятся пакеты с исходниками (.src.rpm).

Настройка общих параметров сборки #

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

  • Местоположение каталогов для сборки
  • Указание ключа для подписывания пакетов
  • Информация о том, кто выполнял сборку пакета
%_topdir        %homedir/RPM
#%_tmppath      %homedir/tmp

# %packager     Joe Hacker <joe@email.address>
# %_gpg_name    joe@email.address

Данный файл содержит определения RPM-макросов. Макросы начинаются со знака процента, затем идёт название макроса, а его значение записывается через пустую строку

Файлы, начинающиеся со знака # являются комментариями

  • Необходимо заполнить значение макроса %packager
  • Удалить определение макроса %__arch_install_post, если присутствует

Source RPM, .src.rpm файлы #

.src.rpm-файлы являются пакетами с исходными кодами приложения, из которых получаются RPM-пакеты после сборки

Рассмотрим src.rpm-пакет текстового редактора nano:

$ rpm -qlp nano-7.2-alt1.1.src.rpm
nano-7.2-build.patch
nano-7.2.tar
nano.spec
  • nano.spec — файл спецификации по сборке приложения
  • nano-7.2.tar — исходные коды приложения, полученные от разработчика
  • nano-7.2-build.path — набор изменений в исходные коды от мейнтейнера

Данные файлы можно получить

Сборка из Source RPM #

  1. Установите инструменты сборки
  2. Настройте окружение для выполнения сборки
  3. Получите .src.rpm-файл для сборки
  4. Распакуйте файл в сборочный каталог rpm -i FILE.src.rpm (выполнять с правами обычного пользователя)
    $ tree ~/RPM
    ~/RPM
    ├── SOURCES
    │   └── nano
    │       ├── nano-7.2-build.patch
    │       └── nano-7.2.tar
    └── SPECS
        └── nano.spec
  5. Выполните сборку приложения rpmbuild -ba RPM/SPECS/nano.spec
  • Сборка может не пройти ввиду отсутствующих зависимостей
  • Собранные RPM-пакеты будут находится в ~/RPM/RPMS/

Spec-файлы #

Spec-файл можно рассматривать как “инструкцию”, которую утилита rpmbuild использует для фактической сборки RPM-пакет. Он сообщает системе сборки, что делать, определяя инструкции в серии разделов.

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

Пример Spec-файла можно посмотреть в AltLinux Wiki

Пункты преамбулы #

  • Name — Базовое имя пакета, которое должно совпадать с именем Spec-файла.
  • Version — Версия upstream-кода.
  • Release — Релиз пакета используется для указания номера сборки пакета при данной версии upstream-кода. Как правило, установите начальное alt1 и увеличивайте его с каждым новым выпуском пакета, например: alt1, alt2, alt3 и т.д.
  • Summary — Краткое, в одну строку, описание пакета.
  • License — Лицензия на собираемое программное обеспечение.
  • Source0 — Путь или URL-адрес к сжатому архиву исходного кода (не исправленный, исправления обрабатываются в другом месте). При необходимости можно добавить дополнительные исходные директивы, каждый раз увеличивая их количество, например: Source1, Source2, Source3 и так далее.
  • Patch0 — Название первого патча, который при необходимости будет применен к исходному коду. При необходимости можно добавить дополнительные директивы PatchX, увеличивая их количество каждый раз, например: Patch1, Patch2, Patch3 и так далее.
  • BuildRequires — Разделённый запятыми или пробелами список пакетов, необходимых для сборки программы. Может быть несколько записей BuildRequires, каждая в отдельной строке в SPEC файле.
  • Requires — Разделённый запятыми или пробелами список пакетов, необходимых программному обеспечению для запуска после установки. Это его зависимости Может быть несколько записей Requires, каждая в отдельной строке в Spec-файле.

Составляющие основной части #

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

  • %description — Полное описание программного обеспечения, входящего в комплект поставки RPM. Это описание может занимать несколько строк.
  • %prep — Команда или серия команд для подготовки программного обеспечения к сборке, например, распаковка архива из Source0. Эта директива может содержать сценарий оболочки (shell скрипт).
  • %build — Команда или серия команд для фактической сборки программного обеспечения в машинный код (для скомпилированных языков) или байт-код (для некоторых интерпретируемых языков).
  • %install — Раздел, который во время сборки пакета эмулирует конечные пути установки файлов в систему. Команда или серия команд для копирования требуемых артефактов сборки из %builddir (где происходит сборка) в %buildroot каталог (который содержит структуру каталогов с файлами, подлежащими сборке). Обычно это означает копирование файлов из ~/rpmbuild/BUILD в ~/rpmbuild/BUILDROOT и создание необходимых каталогов ~/rpmbuild/BUILDROOT. Это выполняется только при создании пакета, а не при установке пакета конечным пользователем.
  • %check — Команда или серия команд для тестирования программного обеспечения. Обычно включает в себя такие вещи, как запуск модульных тестов.
  • %files — Список файлов, которые будут установлены в системе конечного пользователя.
  • %changelog — Запись изменений, произошедших с пакетом между различными Version или Release сборками.

RPM-макросы #

Макросы RPM — это прямые текстовые подстановки, которые происходят путем замены определенных выражений и условий на соответствующий текст во время процесса сборки пакета. Имена макросов начинаются с символа %

  • Обеспечивают нужную функциональность
  • Облегчает задачи мейнтенеров пакетов, уменьшая дублирование
  • Делает Spec-файлы более понятными (с какого-то момента)

Просмотреть список доступных макросов и их значения можно, выполнив команду:

rpm --showrc

Получить значение, раскрываемое макросом можно, использовав команду rpm --eval <имя_макроса>.

$ rpm --eval %_sysconfdir
/etc

Вопросы массовой сборки приложений #

  • Откуда появляется пакет с исходными кодами?
  • Кто формирует спецификацию?
  • Что делать, когда выходит новая версия приложения?
  • Что делать, когда выходит новая версия библиотеки, от которой зависит приложение?
  • Как точно удостоверится, что все зависимости для сборки точно указаны в зависимостях?
  • Как выполнить сборку приложения под все поддерживаемые архитектуры?
  • Как организовать публикацию RPM-пакетов в репозиторий?

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

Инструменты Alt Linux #

girar, gigrar-builder — инструменты для автоматизированной сборки пакетов Sisyphus

  • Управление git-репозиториями (создание, удаление, обновление, перемещение, копирование, поиск по имени репозитория)
  • Управление системой оповещений об изменениях в git-репозиториях
  • Управление правами доступа к сборке пакетов в Sisyphus и другие бранчи
  • Управление сборкой пакетов в Sisyphus и другие бранчи из gear-репозиториев и SRPM-пакетов (сборка пакетов реализована системой girar_builder)
  • Управление аккаунтами пользователей git.alt

Принципиальная архитектура girar #

flowchart TB subgraph Хостинг Git-репозиториев repo_one["Репозиторий пакета"] repo_two["Репзоиторий пакета"] end control(Управляющий сервер\ngirar) subgraph builder_arm[Сборочный сервер\nARM] gear_one[gear] hasher_one[hasher] end repository_arm[[Репозиторий ARM]] subgraph builder_x86_64[Сборочный сервер\nx86_64] gear_two[gear] hasher_two[hasher] end repository_x86_64[[Репозиторий x86_64]] control -.-> builder_arm control -.-> builder_x86_64 repo_one --> builder_arm repo_two --> builder_arm builder_arm --> repository_arm repo_one --> builder_x86_64 repo_two --> builder_x86_64 builder_x86_64 --> repository_x86_64

Процесс обновления пакета #

stateDiagram-v2 state "Добавление исходных кодов разработчика" as adding state "Создание спецификации\nи локальная сборка пакета" as spec state "Доработка спецификации сборки пакета" as local_build state "Публикация пакета в публичном репозитории" as publish state "Сборка на все целевые платформы" as farm_build [*] --> adding adding --> spec spec --> local_build local_build --> spec local_build --> publish publish --> farm_build farm_build --> spec farm_build --> [*]