Анатомия spec-файла: Создание RPM-спеков с нуля

Глубокое погружение в структуру spec-файла для сборки RPM-пакетов. Вы освоите синтаксис, работу с макросами и детальное описание всех этапов сборки от %prep до %files.

1. Структура spec-файла: Основные разделы и базовый синтаксис

Структура spec-файла: Основные разделы и базовый синтаксис

В предыдущем модуле мы разобрали архитектуру RPM и поняли, как пакетный менеджер взаимодействует с системой. Теперь пришло время спуститься на уровень разработчика и научиться создавать пакеты. Сердцем любого RPM-пакета является spec-файл (spec file от слова specification). Это текстовый документ, который содержит пошаговую инструкцию: откуда взять исходный код, как его подготовить, как скомпилировать и какие файлы в итоге нужно упаковать в готовый архив.

Если RPM-пакет — это готовое блюдо, то исходный код — это ингредиенты, а spec-файл — это подробный кулинарный рецепт. В этой статье мы разберём анатомию этого «рецепта», изучим базовый синтаксис и научимся писать спеки, соответствующие стандартам Rosa Linux.

Общая архитектура spec-файла

Spec-файл имеет строгую, исторически сложившуюся структуру. Он читается сверху вниз и делится на две логические части:

  • Преамбула (Preamble) — метаданные пакета (название, версия, лицензия, зависимости). Эта часть описывает что мы собираем.
  • Секции сборки (Build sections) — набор скриптов и директив, описывающих как мы это собираем. Каждая секция начинается со знака процента (например, %prep, %build).
  • !Жизненный цикл исходного кода внутри spec-файла

    Давайте рассмотрим минимальный рабочий пример spec-файла для вымышленной утилиты hello-world, а затем разберём каждую его строку под микроскопом.

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

    Преамбула: Паспорт вашего пакета

    Преамбула не имеет специального открывающего тега. Она начинается с первой строки файла и содержит пары «Ключ: Значение». Это паспортные данные пакета, которые пользователь увидит при выполнении команды rpm -qi имя_пакета.

    Основные теги метаданных

  • Name — базовое имя пакета. Оно должно состоять из строчных букв, цифр и дефисов. Избегайте нижних подчеркиваний.
  • Version — версия программы (апстрим-версия), точно совпадающая с версией разработчика исходного кода. В версии запрещено использовать дефисы, так как RPM использует дефис для отделения версии от релиза.
  • Release — номер сборки (релиза) пакета для текущей версии. Если вы изменили spec-файл, но версия программы осталась прежней (например, добавили патч или исправили зависимость), вы увеличиваете Release (1, 2, 3...). При обновлении Version значение Release сбрасывается обратно в 1.
  • Summary — краткое описание пакета. Строго в одну строку, без точки на конце. Это то, что пользователи видят при поиске пакетов.
  • License — лицензия, под которой распространяется код. В Rosa Linux принято использовать короткие идентификаторы стандарта SPDX (например, GPL-2.0-or-later, MIT, Apache-2.0).
  • URL — ссылка на домашнюю страницу проекта.
  • Исходники и патчи

  • Source0 — имя архива с исходным кодом. Обычно это tar-архив (.tar.gz, .tar.xz). Цифра 0 означает, что это первый (и часто единственный) исходник. Если программе нужны дополнительные файлы (иконки, конфигурации по умолчанию), их добавляют как Source1, Source2 и так далее.
  • Patch0 — файл с изменениями (патч), который нужно применить к исходному коду перед сборкой. Патчи используются для исправления ошибок или адаптации программы под специфику Rosa Linux без изменения оригинального архива Source0.
  • Зависимости: BuildRequires против Requires

    Это один из самых важных аспектов, в котором новички часто допускают ошибки.

  • BuildRequires — пакеты, необходимые только для сборки программы. Например: компиляторы (gcc, clang), системы сборки (cmake, meson), заголовочные файлы библиотек (pkgconfig(gtk+-3.0), openssl-devel). Эти пакеты устанавливаются в сборочной среде (на серверах ABF) и не тянутся на компьютер конечного пользователя.
  • Requires — пакеты, необходимые для работы уже установленной программы. Например, если ваша утилита написана на Python, ей нужен интерпретатор Python для запуска.
  • > Важное правило: Современный RPM очень умён. Он автоматически анализирует скомпилированные бинарные файлы и сам прописывает зависимости от системных библиотек (например, libc.so.6). Поэтому в Requires нужно указывать только неочевидные зависимости (скрипты, внешние утилиты, специфические шрифты), которые RPM не может определить автоматически.

    Макросы: Переменные мира RPM

    Вы наверняка заметили в примере конструкции вида %{name} или %{_bindir}. Это макросы — встроенные переменные RPM.

    Макросы решают три критически важные задачи:

  • Избавление от дублирования. Если вы назвали пакет hello-world в теге Name, вам не нужно писать hello-world-1.0.tar.gz в Source0. Вы пишете %{name}-%{version}.tar.gz. При изменении версии вам нужно будет обновить только одну строку в преамбуле.
  • Кросс-платформенность и стандартизация. В разных дистрибутивах пути к системным папкам могут отличаться. Например, 64-битные библиотеки в одних системах лежат в /usr/lib, а в Rosa Linux — в /usr/lib64.
  • Управление флагами компилятора. Макросы передают правильные параметры оптимизации и безопасности при сборке.
  • Базовые макросы путей

    Никогда не используйте жестко заданные пути (хардкод) в spec-файлах. Всегда используйте макросы:

    | Жесткий путь (Неправильно) | Макрос (Правильно) | Назначение | | :--- | :--- | :--- | | /usr/bin | %{_bindir} | Исполняемые файлы (бинарники) | | /usr/lib64 или /usr/lib | %{_libdir} | Системные библиотеки | | /etc | %{_sysconfdir} | Конфигурационные файлы | | /usr/share/doc | %{_docdir} | Документация | | /usr/share/man | %{_mandir} | Руководства (man pages) |

    Секция %prep: Подготовка плацдарма

    Секция %prep (от preparation) — это первый этап реальной работы. Здесь исходный код распаковывается из архива Source0 и подготавливается к компиляции. На этом этапе применяются патчи.

    Исторически для распаковки использовался макрос %setup -q. Однако в современных спеках Rosa Linux стандартом де-факто стал макрос %autosetup.

    Что делает %autosetup -p1 под капотом?

  • Удаляет старую директорию с исходниками (если она осталась от предыдущей неудачной сборки).
  • Распаковывает архив Source0.
  • Переходит в созданную директорию.
  • Автоматически находит все файлы Patch0, Patch1 и т.д. в преамбуле и применяет их к исходному коду с параметром -p1 (отбрасывая первый уровень вложенности путей в патче, что является стандартом для git-патчей).
  • Секция %build: Превращение кода в программу

    В секции %build происходит магия компиляции. Здесь исходный код превращается в исполняемые бинарные файлы. Содержимое этой секции напрямую зависит от того, какую систему сборки использует разработчик программы (Autotools, CMake, Meson, Ninja, чистый Makefile).

    Для классических программ на C/C++, использующих Autotools (скрипт configure), секция выглядит так:

    Почему мы используем макрос %configure, а не пишем ./configure напрямую? Макрос %configure автоматически подставляет десятки параметров, специфичных для Rosa Linux. Он указывает правильные пути (--prefix=/usr, --libdir=/usr/lib64), а также передает системные флаги компилятора (CFLAGS, CXXFLAGS, LDFLAGS), которые обеспечивают оптимизацию под архитектуру процессора и включают защиту от уязвимостей (например, ASLR и Stack Smashing Protection).

    Макрос %make_build заменяет обычный вызов make. Его главное преимущество — он автоматически определяет количество ядер процессора на сборочном сервере и запускает параллельную сборку (эквивалент make -j8), что ускоряет процесс в разы.

    Секция %install: Сборочный корень (Buildroot)

    Секция %install — самая сложная для понимания концепция среди новичков.

    Когда программа скомпилирована, её нужно «установить». Но если мы выполним стандартную команду make install, программа попытается установиться прямо в системные директории сборочного сервера (в /usr/bin, /etc и т.д.). Это приведет к хаосу: файлы пакета смешаются с файлами системы, и мы не сможем собрать их в RPM-архив. Более того, у процесса сборки обычно нет root-прав для записи в /usr.

    Для решения этой проблемы используется сборочный корень (buildroot). Это временная, изолированная директория (песочница), которая эмулирует корневую файловую систему /.

    В секции %install мы устанавливаем программу не в реальную систему, а в этот сборочный корень.

    Макрос %make_install эквивалентен команде make install DESTDIR=%{buildroot}. Программа думает, что устанавливается в /usr/bin/hello-world, но физически файл копируется во временную папку, например: /home/builder/rpmbuild/BUILDROOT/hello-world-1.0-1.x86_64/usr/bin/hello-world.

    > Важно: Секция %install не устанавливает пакет на ваш компьютер! Она лишь формирует структуру папок и файлов внутри временной директории, из которой RPM-утилита затем запакует архив.

    Секция %files: Опись имущества

    После того как файлы помещены в buildroot, RPM должен знать, какие именно файлы нужно включить в итоговый пакет. Этим занимается секция %files.

    Если файл есть в buildroot, но не указан в %files, сборка завершится с ошибкой (ошибка unpackaged files found). Это механизм защиты: вы не должны случайно забыть упаковать нужный файл или упаковать лишний мусор.

    Специальные директивы в %files

  • %license — помечает файл как лицензионное соглашение. RPM установит его в специальную системную директорию для лицензий.
  • %doc — помечает файл как документацию (README, CHANGELOG).
  • %config(noreplace) — критически важный макрос для конфигурационных файлов (обычно в /etc). Флаг noreplace говорит пакетному менеджеру: «Если пользователь изменил этот файл на своем компьютере, при обновлении пакета не перезаписывай его новым дефолтным файлом». Вместо этого новый файл будет сохранен с расширением .rpmnew.
  • Управление директориями

    Существует огромная разница между указанием директории и указанием файлов внутри неё.

    Если вы напишете: %{_datadir}/hello-world/ RPM упакует саму директорию hello-world и абсолютно все файлы и поддиректории внутри неё. Это удобно для папок, которые целиком принадлежат вашей программе.

    Но если вы напишете: %{_bindir} Это катастрофическая ошибка! Вы прикажете RPM стать владельцем всей системной папки /usr/bin и всех программ в ней. Пакет будет конфликтовать с базовой системой.

    Если вам нужно создать пустую директорию, принадлежащую пакету, используйте директиву %dir: %dir %{_sysconfdir}/hello-world

    Секция %changelog: История изменений

    В самом конце spec-файла находится секция %changelog. Это журнал изменений пакета (не путать с changelog самой программы). Здесь мейнтейнеры записывают, кто, когда и зачем изменил spec-файл.

    Формат записи строго регламентирован:

    Например: * Mon Oct 23 2023 Ivan Ivanov <ivan@example.com> - 1.0-2 - Добавлен патч для исправления падения при запуске

    Типичные ошибки новичков

  • Сборка от root. Никогда не собирайте пакеты от имени суперпользователя. Ошибка в spec-файле (например, случайный rm -rf / в секции %install вместо %install_root) может уничтожить вашу систему. Сборка всегда должна происходить от обычного пользователя.
  • Загрузка файлов из сети во время сборки. Сборочные серверы (включая ABF от Rosa Linux) изолированы от интернета в целях безопасности и воспроизводимости. Команды вроде wget или npm install (без предварительно подготовленного кэша) в секции %build завершатся ошибкой. Все исходники должны быть скачаны заранее и указаны в Source.
  • Игнорирование rpmlint. После сборки пакета всегда проверяйте его утилитой rpmlint. Она укажет на хардкод путей, неправильные права доступа и отсутствующие макросы.
  • В следующей статье мы подробно погрузимся в управление зависимостями: научимся разрешать конфликты библиотек, использовать виртуальные пакеты и правильно связывать компоненты системы между собой.

    10. Секция %install: Установка файлов в buildroot с помощью %make_install

    Секция %build успешно завершила свою работу. Исходный код скомпилирован, объектные файлы слинкованы, и в сборочной директории лежат готовые бинарные файлы, библиотеки и сгенерированная документация. Однако сейчас они представляют собой просто хаотичный набор файлов в рабочей песочнице.

    Чтобы пакетный менеджер смог упаковать эти файлы в аккуратный RPM-архив, их необходимо разложить по строгим полочкам системной иерархии (FHS). Именно за этот этап отвечает секция %install.

    Важно сразу зафиксировать главное правило, которое новички часто понимают неверно: секция %install не устанавливает программу в вашу операционную систему. Она эмулирует процесс установки, копируя файлы во временную изолированную директорию — сборочный корень.

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

    Анатомия процесса установки: Концепция DESTDIR

    В классической парадигме Autotools или обычных Makefile, разработчик апстрима пишет инструкции для установки программы. Обычно это цель install внутри Makefile, которая выглядит примерно так:

    Если обычный пользователь скачает исходники, скомпилирует их и выполнит команду sudo make install, эти файлы скопируются прямо в корневую файловую систему его компьютера.

    Но при сборке RPM-пакета мы работаем от имени обычного пользователя (без прав root) и в изолированной среде. Если сборочный скрипт попытается выполнить такую команду, он получит ошибку Permission denied, так как у него нет прав на запись в системный /usr/bin.

    Чтобы решить эту проблему, стандарт GNU предписывает разработчикам использовать специальную переменную DESTDIR (Destination Directory — целевая директория). Правильно написанный Makefile должен выглядеть так:

    Переменная DESTDIR по умолчанию пуста. Поэтому при ручной установке (make install) файлы летят в корень системы. Но мейнтейнер пакета может передать в эту переменную путь к сборочному корню!

    !Схема перенаправления путей: как переменная DESTDIR превращает системный путь во временный путь внутри сборочной среды.

    Когда мы вызываем установку с указанием DESTDIR, происходит магия конкатенации путей: Путь $(DESTDIR) + /usr/bin/app превращается в /home/builder/rpmbuild/BUILDROOT/app-1.0-1.x86_64/usr/bin/app.

    Файлы физически копируются в безопасную временную папку, но внутри этой папки воссоздается точная копия системного дерева каталогов.

    Макрос %make_install: Стандарт индустрии

    Вам не нужно вручную прописывать длинные пути и вызывать make. Для этого существует стандартный макрос %make_install.

    В секции %install базового spec-файла это выглядит максимально лаконично:

    Под капотом этот макрос разворачивается в команду: make install DESTDIR=%{buildroot}

    Он автоматически берет путь к текущему сборочному корню и передает его утилите make. Это работает в 95% случаев для проектов на C/C++, использующих классические системы сборки.

    Что делать, если апстрим не знает про DESTDIR?

    Иногда мейнтейнеры сталкиваются со старыми или небрежно написанными проектами, где разработчик проигнорировал стандарт GNU и не добавил поддержку DESTDIR. Вместо этого файлы устанавливаются относительно переменной PREFIX.

    Если %make_install не срабатывает (файлы пытаются установиться в реальную систему и сборка падает с ошибкой прав доступа), применяется классический обходной маневр — переопределение префикса на лету:

    Этот метод заставляет make думать, что системный /usr находится внутри нашего сборочного корня. Это более хрупкий подход, но он спасает при работе с легаси-кодом.

    Современные альтернативы: %cmake_install и %meson_install

    Как мы выяснили ранее, современные системы сборки используют концепцию Out-of-Source Build. Артефакты компиляции лежат не в папке с исходниками, а в отдельной директории (например, redhat-linux-build).

    Обычный %make_install в корне исходников ничего не найдет. Поэтому для проектов на CMake и Meson используются их собственные макросы установки:

    Эти макросы невероятно умны. Они автоматически переходят в правильную директорию сборки, находят сгенерированные скрипты установки и корректно применяют перенаправление в %{buildroot}. Более того, %cmake_install на этом этапе часто выполняет автоматическое удаление вшитых путей (RPATH), о которых мы говорили на этапе компиляции.

    Ручная установка: Утилита install

    Автоматизация — это прекрасно, но Makefile апстрима редко бывает идеальным. Часто разработчик забывает включить в скрипт установки важные файлы: конфигурации по умолчанию, иконки для рабочего стола, man-страницы или systemd-юниты.

    В таких случаях мейнтейнеру приходится копировать файлы в сборочный корень вручную. И здесь кроется главная ловушка для новичков: никогда не используйте команду cp (copy) в секции %install.

    Для ручного размещения файлов в Linux существует специализированная утилита install.

    Почему install, а не cp?

    Команда cp просто дублирует данные. Утилита install делает три вещи одновременно:

  • Копирует файл.
  • Устанавливает правильные права доступа (по умолчанию 0755 для исполняемых файлов).
  • Выполняет операцию как атомарную (сначала создает временный файл, а затем мгновенно переименовывает его, что исключает повреждение данных при параллельных процессах).
  • Рассмотрим пример. Нам нужно вручную установить конфигурационный файл app.conf в директорию /etc/app/.

    Правильный синтаксис в spec-файле:

    Разберем флаги утилиты install: * -m 0644 — устанавливает права доступа (чтение и запись для владельца, только чтение для остальных). Конфигурационные файлы не должны быть исполняемыми. * -p — включает сохранение временных меток (Timestamp Preservation).

    Важность сохранения временных меток

    Флаг -p (preserve) критически важен для качества пакета. Когда вы скачиваете исходный архив, каждый файл в нем имеет дату создания (например, 15 мая 2021 года).

    Если вы скопируете файл без флага -p, операционная система установит ему текущее время (время сборки пакета). Почему это плохо?

    Многие системы резервного копирования и утилиты синхронизации (например, rsync) ориентируются на время изменения файла. Если при обновлении пакета содержимое конфигурационного файла не изменилось, но изменилась его дата (потому что пакет пересобрали), система бэкапа решит, что файл новый, и скопирует его заново. Это тратит ресурсы. Сохранение оригинальных таймстемпов апстрима — признак профессионального пакетирования.

    Флаг -D: Установка с созданием пути

    В примере выше мы сначала создали директорию через mkdir -p, а затем скопировали туда файл. Утилита install умеет делать это в одну команду с помощью флага -D:

    Это самый элегантный и предпочтительный способ ручной установки одиночных файлов в spec-файлах Rosa Linux.

    Очистка сборочного корня: Удаление мусора

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

    Если эти файлы останутся в %{buildroot}, пакетный менеджер либо упакует их (раздувая размер пакета), либо выдаст ошибку на следующем этапе, если найдет файлы, не описанные в манифесте.

    Проблема .la файлов (Libtool archives)

    Самый частый гость в списке на удаление — файлы с расширением .la.

    Libtool archive (.la файлы) — это текстовые файлы, создаваемые утилитой GNU Libtool. В конце 90-х и начале 2000-х годов они были необходимы для кросс-платформенной линковки динамических библиотек на экзотических UNIX-системах (где не было нормальной поддержки общих библиотек).

    В современных дистрибутивах Linux (включая Rosa Linux) динамический компоновщик ld.so и формат ELF прекрасно справляются с зависимостями библиотек самостоятельно. Файлы .la стали рудиментом. Более того, они вредны: они содержат жестко зашитые пути к другим библиотекам, что может сломать сборку зависимых пакетов при обновлении системы.

    Поэтому золотое правило пакетирования: файлы .la должны быть безжалостно удалены в секции %install.

    Статические библиотеки (.a)

    По умолчанию в Linux используются динамические библиотеки (.so). Они загружаются в память один раз и используются множеством программ. Статические библиотеки (.a) вшиваются прямо в исполняемый файл на этапе компиляции, раздувая его размер и делая невозможным обновление библиотеки без перекомпиляции самой программы.

    Политика дистрибутивов запрещает использование статических библиотек без крайней необходимости. Если make install установил файлы .a в %{buildroot}%{_libdir}, у мейнтейнера есть два пути:

  • Если они точно не нужны разработчикам, использующим вашу библиотеку — удалить их:
  • rm -f %{buildroot}%{_libdir}/*.a
  • Если они специфичны и могут понадобиться для статической сборки других проектов — вынести их в отдельный подпакет name-static (создание подпакетов мы разберем в будущих статьях).
  • Ненужная документация

    Часто апстрим устанавливает файлы INSTALL, README или LICENSE прямо в /usr/share/doc/app/. В RPM-пакетировании это считается дучным тоном, так как для документации существует специальная директива %doc (которая применяется на следующем этапе).

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

    Работа с символическими ссылками

    Иногда программе требуются символические ссылки (symlinks) для обратной совместимости. Например, бинарный файл называется python3.10, но пользователи привыкли вызывать просто python3.

    Создание симлинков в секции %install требует особой осторожности. Ссылка должна указывать на путь внутри целевой системы, а не внутри сборочного сервера!

    Неправильно:

    Если сделать так, симлинк будет указывать на /home/builder/rpmbuild/BUILDROOT/.... На компьютере пользователя эта ссылка будет битой.

    Правильно:

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

    Локализация: Макрос %find_lang

    Если программа поддерживает несколько языков, она устанавливает файлы переводов (обычно с расширением .mo) в директорию %{_datadir}/locale/.

    Перечислять сотни файлов переводов для каждого языка вручную — безумие. Для автоматизации этого процесса в самом конце секции %install вызывается специальный макрос %find_lang.

    Этот макрос сканирует сборочный корень, находит все файлы локализации, относящиеся к вашей программе, и записывает их пути в специальный текстовый файл %{name}.lang. На следующем этапе мы просто передадим этот файл пакетному менеджеру, и он сам включит все переводы в итоговый RPM.

    Итог этапа %install

    К моменту завершения секции %install директория %{buildroot} должна представлять собой идеальный слепок того, что появится на жестком диске пользователя после команды dnf install ваш-пакет.

    Все файлы лежат в правильных папках (/usr/bin, /etc, /usr/lib64), права доступа настроены, мусор удален, а временные метки сохранены.

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

    11. Секция %files: Базовые принципы включения файлов в пакет

    Секция %install завершила свою работу: все скомпилированные бинарники, библиотеки и конфигурационные файлы аккуратно разложены внутри изолированного сборочного корня. Однако пакетный менеджер RPM работает по принципу строгого декларирования. Тот факт, что файл физически находится во временной директории, не означает, что он автоматически попадет в итоговый .rpm архив.

    За инвентаризацию и упаковку отвечает секция %files. Это манифест пакета — таможенная декларация, в которой мейнтейнер обязан перечислить каждый файл и каждую директорию, пересекающую границу между сборочной средой и операционной системой пользователя.

    Принцип строгой декларации и ошибка распакованных файлов

    Сборка RPM-пакета — это процесс с нулевой терпимостью к неучтенным данным. Когда утилита rpmbuild доходит до этапа формирования архива, она сравнивает два списка:

  • Фактическое содержимое сборочного корня.
  • Список путей, указанных в секции %files.
  • Если в сборочном корне обнаруживается файл, который не упомянут в манифесте, сборка немедленно прерывается с классической ошибкой, с которой сталкивается каждый начинающий мейнтейнер:

    Эта защита создана намеренно. Она гарантирует, что в пакет не попадет случайный мусор, отладочные символы или файлы, которые система сборки апстрима установила по ошибке. Если файл нужен — задекларируйте его. Если не нужен — удалите его на этапе %install.

    Базовый синтаксис и абсолютные пути

    Главное правило секции %files, которое часто нарушают новички: пути в манифесте указываются относительно целевой системы, а не сборочного корня.

    Вам не нужно (и категорически нельзя) писать макрос %{buildroot} в этой секции. Пакетный менеджер сам знает, где лежат файлы во время сборки. Вы описываете то, как структура должна выглядеть на компьютере конечного пользователя.

    Правильный пример базовой секции:

    Использование стандартных макросов путей (%{_bindir}, %{_sysconfdir}) здесь так же обязательно, как и на предыдущих этапах. Это обеспечивает кросс-платформенность и правильную работу концепции Multilib.

    Глоббинг в RPM: Использование подстановочных знаков

    Перечислять сотни файлов по одному нецелесообразно. RPM поддерживает глоббинг — использование подстановочных символов (wildcards), таких как * (любое количество символов) и ? (один символ).

    Несмотря на удобство, глоббинг требует предельной осторожности. Слишком широкая маска — это бомба замедленного действия.

    Рассмотрим частую ошибку:

    Почему так делать нельзя?

  • Захват отладочных файлов: В процессе компиляции могут генерироваться файлы с отладочными символами (debuginfo). Широкая маска затянет их в основной пакет, многократно увеличив его размер.
  • Конфликты подпакетов: Сложные программы часто делятся на основной пакет, пакет для разработчиков (-devel) и серверную часть (-server). Если в основном пакете указать %{_bindir}/*, он украдет бинарные файлы, предназначенные для других подпакетов, что приведет к конфликтам при установке.
  • Золотое правило глоббинга: маска должна быть ровно такой ширины, чтобы захватить нужные файлы, и ни символом больше. Идеальный вариант — указывать префикс программы: %{_bindir}/%{name}*.

    Владение директориями: Самая сложная концепция %files

    Понимание того, как RPM обрабатывает директории — это граница, отделяющая новичка от профессионала. В RPM существует фундаментальная разница между указанием пути к директории и использованием директивы %dir.

    Рекурсивный захват (Без %dir)

    Если вы просто напишете путь к директории в секции %files, RPM сделает пакет владельцем этой директории и рекурсивно упакует абсолютно все файлы и поддиректории внутри нее.

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

    Однако этот подход становится фатальным, если применить его к системным директориям. Если вы напишете в манифесте %{_bindir}, ваш пакет заявит права на всю системную папку с бинарниками и попытается упаковать все программы, которые оказались в сборочном корне.

    Директива %dir (Только оболочка)

    Чтобы пакет стал владельцем самой директории, но не трогал ее содержимое, используется макрос %dir.

    !Разница между обычным указанием пути и использованием директивы %dir в RPM-пакетировании.

    Проблема осиротевших директорий

    Зачем вообще заявлять права на директорию? Почему нельзя просто перечислить файлы?

    Осиротевшие директории (Unowned directories) — это папки, которые были созданы при установке пакета (потому что в них лежал файл), но ни один пакет в системе не отмечен как их владелец.

    Представьте, что ваш пакет устанавливает файл /etc/myapp/conf.d/custom.conf. Вы написали в %files только путь к файлу. Когда пользователь установит пакет, RPM создаст папки myapp и conf.d, чтобы положить туда файл. Но когда пользователь удалит пакет (команда dnf remove), RPM удалит только custom.conf. Пустые папки /etc/myapp/conf.d/ навсегда останутся в системе, засоряя файловую систему, потому что RPM не знает, имеет ли он право их удалять.

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

    Исключение: Вы не должны заявлять права на общие системные директории, которые принадлежат базовой системе (например, /usr/bin, /etc, /usr/share). Ваш пакет — гость в этих папках.

    Управление правами доступа: Макросы %attr и %defattr

    По умолчанию RPM сохраняет те права доступа (permissions) и тех владельцев (owner/group), которые были установлены файлам в сборочном корне на этапе %install.

    Однако иногда требуется принудительно изменить права для конкретного файла прямо в манифесте. Для этого используется макрос %attr.

    Синтаксис: %attr(режим, пользователь, группа) путь_к_файлу

    Если какой-то параметр менять не нужно, вместо него ставится дефис (-). Например, %attr(-, nginx, -) изменит только пользователя, сохранив оригинальные права и группу.

    Наследие прошлого: %defattr

    В старых spec-файлах вы почти всегда встретите строку %defattr(-,root,root,-) сразу после объявления %files.

    Исторически RPM мог случайно захватить права пользователя, от имени которого шла сборка (например, builder). Макрос %defattr принудительно сбрасывал владельца всех нижеперечисленных файлов на root. В современных версиях RPM (включая ту, что используется в Rosa Linux) это поведение исправлено на уровне системы, и файлы автоматически получают владельца root. Использование %defattr сегодня считается избыточным, но знать о нем необходимо для чтения чужого кода.

    Конфигурационные файлы: Директива %config

    Конфигурационные файлы — это особая категория данных. Если бинарный файл пользователь никогда не меняет, то конфиги созданы именно для ручной правки.

    Если вы просто укажете путь к конфигу в %files, то при следующем обновлении пакета (dnf upgrade) RPM безжалостно перезапишет измененный пользователем файл новой версией от разработчика. Настройки будут потеряны, а сервер может перестать работать.

    Чтобы защитить пользовательские данные, применяется директива %config.

    %config(noreplace)

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

    Как это работает при обновлении пакета:

  • RPM видит, что файл на диске был изменен пользователем.
  • RPM не трогает пользовательский файл.
  • Новый конфиг из обновленного пакета сохраняется рядом с расширением .rpmnew (например, myapp.conf.rpmnew).
  • Пользователь получает предупреждение в консоли и может вручную перенести новые параметры из .rpmnew в свой рабочий конфиг.
  • Просто %config

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

    Поведение при обновлении:

  • RPM видит изменения пользователя.
  • RPM перезаписывает файл новой версией из пакета.
  • Старый пользовательский файл сохраняется как резервная копия с расширением .rpmsave (например, myapp.rpmsave).
  • Документация и лицензии: %doc и %license

    В предыдущей статье мы упоминали, что файлы README, CHANGELOG и LICENSE нужно удалять из сборочного корня, если апстрим установил их туда по умолчанию. Причина кроется в макросах %doc и %license.

    Эти директивы обладают уникальной механикой: они ищут файлы не в сборочном корне (BUILDROOT), а в директории с исходным кодом (BUILD).

    Когда RPM встречает эти макросы, он выполняет скрытую автоматизацию:

  • Создает в итоговом пакете системную директорию для документации (обычно /usr/share/doc/%{name}/).
  • Копирует указанные файлы прямо из папки с исходниками в эту новую директорию.
  • Назначает правильные права доступа.
  • Директива %license работает аналогично %doc, но в современных дистрибутивах она может складывать файлы лицензий в отдельную специализированную директорию (например, /usr/share/licenses/%{name}/), что упрощает юридический аудит системы.

    Интеграция локализаций: Флаг -f

    Если программа поддерживает множество языков, перечислять каждый .mo файл в манифесте — неблагодарный труд.

    Напомним, что в конце секции %install мы использовали макрос %find_lang %{name}, который автоматически нашел все переводы и записал их пути в текстовый файл %{name}.lang.

    Чтобы включить этот сгенерированный список в манифест, секция %files вызывается с флагом -f:

    RPM прочитает файл %{name}.lang, добавит все пути из него к списку файлов, указанных ниже, и упакует их в единый архив.

    Исключение файлов: Директива %exclude

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

    Несмотря на наличие этого инструмента, в сообществе мейнтейнеров Rosa Linux считается хорошим тоном физически удалять ненужные файлы в секции %install с помощью команды rm -rf, а не скрывать их через %exclude. Физическое удаление делает процесс сборки более прозрачным и снижает риск того, что мусорные файлы случайно попадут в другой подпакет.

    Секция %files — это финальный аккорд в создании базового RPM-пакета. Правильно составленный манифест гарантирует, что программа чисто установится в систему, не повредит пользовательские настройки при обновлении и не оставит после себя мусора при удалении.

    12. Секция %files: Маркировка документации, лицензий и конфигурационных файлов

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

    Простое перечисление путей в манифесте делает файлы «бесправными» резидентами файловой системы. Чтобы наделить их особым поведением при установке, обновлении и удалении, применяются семантические маркеры. Это специальные директивы, которые превращают слепой архив в умную базу данных.

    Семантическая разметка документации: Глубокое погружение в %doc

    На первый взгляд, макрос %doc кажется банальным инструментом для копирования файлов README или CHANGELOG. Однако под капотом скрывается сложная логика, зависящая от того, какой путь вы передаете макросу: абсолютный или относительный.

    Относительные пути: Магия автоматического копирования

    Когда вы указываете путь без ведущего слеша (например, %doc docs/manual.pdf), RPM включает режим автоматизации. В этом случае пакетный менеджер ищет файл не в сборочном корне, а в директории с исходным кодом.

    Процесс выглядит следующим образом:

  • RPM автоматически создает системную директорию для документации пакета. В Rosa Linux это обычно %{_datadir}/doc/%{name}-%{version}/.
  • Указанные файлы копируются из исходников в эту новую директорию.
  • Файлам назначаются безопасные права доступа (обычно 0644), а владельцем становится root.
  • Директория и скопированные файлы неявно добавляются в манифест пакета.
  • Этот механизм избавляет мейнтейнера от необходимости вручную создавать папки и копировать текстовые файлы в секции %install.

    Абсолютные пути: Маркировка существующих файлов

    Если программа использует сложную систему сборки (например, Sphinx или Doxygen), она может самостоятельно установить сгенерированные HTML-страницы или man-страницы в сборочный корень на этапе %install.

    В этом случае автоматическое копирование не нужно. Файлы уже лежат там, где должны. Но их все равно необходимо пометить как документацию. Для этого макросу %doc передается абсолютный путь (начинающийся со слеша или системного макроса).

    Зачем нужна маркировка документации?

    Главная причина строгого использования %doc — поддержка флага --nodocs.

    В современных инфраструктурах, особенно при сборке минималистичных Docker-контейнеров или образов для встраиваемых систем, каждый мегабайт имеет значение. Системные администраторы часто устанавливают пакеты командой dnf install --setopt=tsflags=nodocs myapp.

    Если вы просто скопируете manual.pdf в /usr/share/doc/ без использования макроса %doc, пакетный менеджер не будет знать, что это документация. Файл будет принудительно установлен даже в минималистичной среде, расходуя дисковое пространство. Маркировка дает пользователю свободу выбора: устанавливать тяжелые мануалы или проигнорировать их.

    Юридический слой: Почему %license отделили от %doc

    Исторически файлы с текстом лицензий (например, COPYING, LICENSE.txt) помечались макросом %doc. Однако с массовым распространением контейнеризации и флага --nodocs возникла критическая юридическая проблема.

    Многие открытые лицензии (включая GNU GPL и MIT) содержат строгое условие: текст лицензии обязан распространяться вместе с бинарными файлами программы. Когда администраторы использовали установку без документации, файлы лицензий удалялись вместе с инструкциями. Это автоматически превращало легальное использование свободного ПО в нарушение авторских прав.

    Для решения этой коллизии был введен макрос %license.

    Механика его работы идентична относительному вызову %doc: он берет файл из исходников и помещает его в систему. Разница заключается в двух аспектах:

  • Файлы помещаются в выделенную директорию %{_datadir}/licenses/%{name}/, что упрощает автоматизированный аудит лицензионной чистоты системы.
  • Файлы, отмеченные как %license, игнорируют флаг --nodocs. Они будут установлены в систему при любых обстоятельствах, гарантируя соблюдение юридических требований.
  • В сообществе Rosa Linux отсутствие макроса %license (если апстрим предоставляет файл лицензии) является блокирующей ошибкой при ревью пакета.

    Анатомия конфигурационных файлов: Трехстороннее сравнение

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

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

    Когда вы помечаете файл директивой %config(noreplace), RPM начинает отслеживать его состояние с помощью криптографических хэшей (обычно SHA256). В момент обновления пакета RPM анализирует три значения:

  • Хэш файла из старого RPM-пакета (каким файл был задуман разработчиком в прошлой версии).
  • Хэш файла на диске (текущее состояние, возможно, измененное пользователем).
  • Хэш файла из нового RPM-пакета (каким файл задуман разработчиком сейчас).
  • !Схема принятия решений пакетным менеджером RPM при обновлении конфигурационного файла с директивой noreplace.

    На основе этих трех хэшей встроенный конечный автомат RPM принимает решение по одному из сценариев:

    Сценарий 1: Никто ничего не менял

  • Старый RPM = На диске (пользователь не трогал конфиг).
  • Старый RPM Новый RPM (разработчик обновил конфиг).
  • Действие: RPM спокойно перезаписывает файл на диске новой версией. Пользовательских данных нет, терять нечего.
  • Сценарий 2: Пользователь изменил, разработчик — нет

  • Старый RPM На диске (пользователь внес правки).
  • Старый RPM = Новый RPM (в новой версии пакета конфиг остался прежним).
  • Действие: RPM оставляет файл на диске нетронутым. Обновлять нечего, настройки пользователя в безопасности.
  • Сценарий 3: Конфликт (Изменили оба)

  • Старый RPM На диске (пользователь внес правки).
  • Старый RPM Новый RPM (разработчик тоже обновил конфиг).
  • Действие: Это самая опасная ситуация. RPM оставляет файл пользователя на диске нетронутым (программа продолжит работать со старыми настройками). При этом новый файл от разработчика сохраняется рядом с расширением .rpmnew (например, nginx.conf.rpmnew).
  • В консоли администратор увидит предупреждение: warning: /etc/nginx/nginx.conf created as /etc/nginx/nginx.conf.rpmnew. После этого администратор должен вручную открыть оба файла и перенести новые директивы из .rpmnew в свой рабочий конфиг.

    Когда использовать обычный %config?

    Директива %config (без noreplace) работает иначе при конфликте (Сценарий 3). Она отдает приоритет разработчику: файл на диске перезаписывается новой версией, а пользовательские настройки сохраняются в резервную копию с расширением .rpmsave.

    Этот жесткий подход применяется редко, но он необходим для файлов, структура которых кардинально меняется между версиями. Например, если формат конфигурации демона изменился с INI на YAML, старый пользовательский конфиг гарантированно приведет к сбою при запуске новой версии службы. В таком случае лучше подсунуть рабочий дефолтный конфиг, сохранив старый в .rpmsave для справки.

    Призраки в системе: Макрос %ghost

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

    Если демон myapp при старте создает файл /var/log/myapp.log, пакетный менеджер ничего о нем не знает. При удалении пакета командой dnf remove myapp этот лог-файл останется в системе, превращаясь в цифровой мусор.

    Чтобы заявить права на файл, который будет создан в будущем, используется макрос %ghost (призрак).

    Важное правило: Файл, помеченный как %ghost, обязан присутствовать в сборочном корне на этапе %install, даже если он абсолютно пустой. Иначе rpmbuild выдаст ошибку «файл не найден».

    Обычно это решается созданием пустого файла с помощью команды touch:

    При сборке пакета RPM увидит файл, считает его права доступа (или применит те, что указаны в %attr), запишет путь в базу данных, но не включит само содержимое файла в итоговый .rpm архив.

    Когда пользователь установит пакет, файла /var/log/myapp.log на диске не появится. Но как только программа его создаст, RPM будет знать: «Этот файл принадлежит пакету myapp». При деинсталляции пакета лог-файл будет корректно удален вместе с программой.

    Комбинация макросов: %ghost и %config

    Макросы можно комбинировать для создания сложной логики. Классический пример — файл /etc/resolv.conf (настройки DNS), который генерируется сетевыми менеджерами динамически.

    Эта комбинация говорит пакетному менеджеру: «Файла нет в архиве, он появится позже (%ghost). Но когда он появится, считай его конфигурационным. Если при обновлении пакета выяснится, что пользователь вручную прописал туда свои DNS-серверы, не смей его удалять или перезаписывать (%config(noreplace))».

    Проверка правильности разметки

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

    Чтобы увидеть список всех конфигурационных файлов в собранном пакете: rpm -qp --config myapp-1.0-1.x86_64.rpm

    Чтобы проверить, какие файлы отмечены как документация и лицензии: rpm -qp --docfiles myapp-1.0-1.x86_64.rpm

    Если в выдаче этих команд не хватает файлов, которые вы планировали разметить, значит, в секции %files допущена синтаксическая ошибка или неверно указан путь.

    Семантическая разметка — это язык общения между мейнтейнером и системным администратором. Правильное использование %doc, %license, %config и %ghost гарантирует, что пакет будет вести себя предсказуемо в любых инфраструктурах: от тяжелых десктопов до минималистичных облачных контейнеров, уважая при этом как юридические нормы, так и личные данные пользователей.

    13. Секция %files: Правильное владение директориями и макрос %dir

    Упаковка файлов в RPM-архив — это не просто архивация, а заключение строгого контракта между вашим программным обеспечением и операционной системой. Когда пакет устанавливается, он заявляет операционной системе: «Я беру на себя ответственность за эти объекты». И если с обычными файлами (бинарниками, конфигурациями) всё интуитивно понятно, то директории часто становятся камнем преткновения для начинающих мейнтейнеров.

    Файловая система Linux напоминает огромный многоквартирный дом, где разные программы делят общие коридоры, но имеют свои изолированные комнаты. Неправильное управление правами на эти «помещения» приводит к конфликтам при обновлениях, мусору после удаления программ и серьезным уязвимостям безопасности.

    Механика учета директорий в RPM

    Чтобы понять, как пакетный менеджер работает с папками, нужно заглянуть под капот его базы данных. RPM не просто распаковывает архив. Для каждого объекта из манифеста он создает запись в своей внутренней SQLite-базе.

    Когда дело касается директорий, RPM использует механизм счетчика ссылок (Reference Counting). Это целое число, которое показывает, сколько установленных в данный момент пакетов заявили свои права на конкретную папку.

    Процесс выглядит так:

  • Вы устанавливаете Пакет А, который заявляет права на /opt/myapp. RPM создает папку и устанавливает счетчик для /opt/myapp равным 1.
  • Вы устанавливаете Пакет Б (например, плагин), который тоже заявляет права на /opt/myapp. Счетчик увеличивается до 2.
  • Вы удаляете Пакет А. RPM видит, что папка принадлежала этому пакету, и уменьшает счетчик на 1. Поскольку счетчик равен 1 (а не 0), папка остается на диске — она все еще нужна Пакету Б.
  • Вы удаляете Пакет Б. Счетчик падает до 0. Пакетный менеджер вызывает системную функцию rmdir(), чтобы уничтожить директорию.
  • !Интерактивная симуляция счетчика ссылок при установке и удалении пакетов

    Однако системный вызов rmdir() имеет встроенную защиту: он может удалить только абсолютно пустую директорию. Если внутри остался хотя бы один файл (например, лог-файл, который программа создала сама, без макроса %ghost), удаление прервется. В консоли администратор увидит классическое предупреждение: warning: /opt/myapp saved as /opt/myapp.rpmsave или rmdir: Directory not empty.

    Это оставляет в системе осиротевшие файлы (Orphaned files) — цифровой мусор, который больше не контролируется ни одним пакетом.

    Рекурсивный захват: Оружие массового поражения

    Самая частая ошибка при написании манифеста — бездумное указание путей к директориям.

    Если вы напишете в spec-файле просто путь к папке:

    RPM интерпретирует это как рекурсивный захват (Recursive capture). Пакетный менеджер автоматически включит в манифест саму директорию myapp, а также абсолютно все файлы, поддиректории и файлы в поддиректориях, которые находились там на этапе сборки в сборочном корне.

    В случае с изолированной папкой приложения (как /usr/share/myapp/) это удобная фича. Вам не нужно перечислять сотни иконок и скриптов вручную. Вы захватываете всю папку целиком.

    Но эта фича превращается в катастрофу, если применить ее к системным путям. Представьте, что вы написали:

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

    Базовый пакет filesystem и системные пути

    Кто же на самом деле владеет корневыми директориями вроде /usr, /etc, /usr/bin или /var/log?

    В Rosa Linux (как и в большинстве RPM-дистрибутивов) существует специальный базовый пакет filesystem. Это фундаментальный пакет, который устанавливается самым первым при развертывании ОС. Его единственная задача — создать правильную иерархию FHS и закрепить за собой владение базовыми системными директориями с правильными правами доступа (обычно root:root и 0755).

    Золотое правило пакетирования: Никогда не заявляйте права на директории, принадлежащие пакету filesystem.

    Если ваша программа устанавливает исполняемый файл в /usr/bin/myapp, в манифесте вы должны указать только сам файл:

    Проверить, кому принадлежит любая папка в вашей системе, можно консольной командой: rpm -qf /usr/bin

    Макрос %dir: Хирургическая точность

    Что делать, если вам нужно заявить права на саму директорию, но вы категорически не хотите захватывать файлы внутри нее? Для этого существует макрос %dir.

    Директива %dir говорит пакетному менеджеру: «Я владею только стенами этой комнаты. Мебель внутри меня не касается».

    !Разница между рекурсивным захватом и макросом %dir

    Сценарий применения: Архитектура плагинов

    Представьте, что вы собираете ядро веб-сервера mywebserver. Он поддерживает плагины, которые сторонние разработчики могут устанавливать в папку /usr/lib64/mywebserver/plugins/.

    Если в пакете ядра вы напишете:

    Вы совершите логическую ошибку. Ядро рекурсивно захватит папку. Когда пользователь установит сторонний плагин, а затем решит удалить ядро веб-сервера, RPM попытается удалить папку plugins/ вместе со всеми сторонними плагинами внутри (или выдаст ошибку, если плагины принадлежат другим пакетам).

    Правильный подход для пакета ядра:

    А в пакете стороннего плагина mywebserver-plugin-auth манифест будет выглядеть так:

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

    Совместное владение (Co-ownership)

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

    Классический пример — скрипты автодополнения для командной оболочки (bash-completion). Многие программы кладут свои скрипты в /usr/share/bash-completion/completions/. Если базовый пакет bash-completion не установлен, этой директории в системе нет.

    Если ваш пакет просто положит туда файл, возникнет неявное сиротство (Implicit Orphanhood). Это ситуация, когда файл устанавливается в директорию, которой нет в системе, и ни один пакет не берет на себя ответственность за создание и удаление этой директории. Утилита rpmlint при проверке такого пакета выдаст строгую ошибку: directory not owned by any package.

    Решение — совместное владение. Каждый пакет, использующий эту общую директорию, должен заявить на нее права с помощью %dir.

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

    Опасность конфликта атрибутов

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

    Если Пакет А говорит: %dir %attr(0755, root, root) /opt/shared

    А Пакет Б требует: %dir %attr(0775, admin, admin) /opt/shared

    Возникает конфликт атрибутов (Attribute Conflict). При попытке установить Пакет Б поверх Пакета А, пакетный менеджер обнаружит, что требования к безопасности взаимоисключающие, и прервет транзакцию с ошибкой. В экосистеме Rosa Linux за единообразием прав на общие директории строго следят мейнтейнеры репозитория.

    Управление правами: Макрос %attr

    По умолчанию, когда файлы и директории копируются в сборочный корень на этапе %install, они сохраняют те права, которые были назначены им утилитой install или скриптами сборки (обычно 0755 для папок и бинарников, 0644 для текстовых файлов).

    Однако иногда апстрим (разработчик оригинального кода) допускает ошибки в сборочных скриптах, назначая небезопасные права (например, 0777 — полный доступ для всех). Исправлять это патчами в исходном коде долго и не всегда целесообразно.

    Для принудительного переопределения прав прямо в манифесте используется макрос %attr. Синтаксис макроса требует трех аргументов: (режим, пользователь, группа).

    Важно помнить: если вы назначаете владельцем директории специфического системного пользователя (в примере выше — myapp), этот пользователь обязан существовать в системе до момента распаковки файлов. Это значит, что создание пользователя должно происходить в секции %pre (скрипт, выполняемый до установки файлов), о чем мы подробно поговорим в будущих статьях курса.

    Практический алгоритм: Как описывать директории

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

  • Это стандартная системная папка FHS? (например, /usr/bin, /etc, /usr/share/man).
  • Действие:* Ничего не пишем про папку. Указываем только файлы внутри нее.
  • Это уникальная папка только для вашего приложения? (например, /usr/share/myapp/, внутри которой лежат только ваши иконки и переводы).
  • Действие:* Указываем путь к папке без макросов. Используем рекурсивный захват.
  • Это папка вашего приложения, куда другие пакеты будут класть файлы? (например, папка для плагинов).
  • Действие:* Используем %dir /путь/к/папке. Сами плагины (если они идут в этом же пакете) перечисляем отдельно.
  • Это общая папка для определенного класса программ? (например, /usr/lib/systemd/system/ или /etc/bash_completion.d/).
  • Действие:* Проверяем, владеет ли ей базовый пакет (через rpm -qf). Если да — см. пункт 1. Если нет — используем совместное владение через %dir.

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

    14. Скриптлеты: Базовые сценарии при установке и удалении пакета

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

    Пакетный менеджер не умеет угадывать эти потребности. Чтобы выполнить системные команды в процессе изменения состояния пакета, используются скриптлеты (Scriptlets) — встроенные в spec-файл сценарии командной оболочки (shell scripts), которые запускаются строго в определенные моменты жизненного цикла пакета.

    Четыре столпа жизненного цикла

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

    Всего существует четыре базовых скриптлета:

  • %pre (Pre-installation) — выполняется до того, как пакетный менеджер начнет распаковывать файлы из архива на диск.
  • %post (Post-installation) — выполняется сразу после того, как все файлы пакета были успешно скопированы в файловую систему.
  • %preun (Pre-uninstall) — выполняется до того, как пакетный менеджер начнет удалять файлы пакета с диска.
  • %postun (Post-uninstall) — выполняется после того, как файлы были физически удалены из системы.
  • Внутри этих секций пишется обычный код для интерпретатора /bin/sh (по умолчанию).

    > Скриптлеты всегда выполняются с правами суперпользователя (root), независимо от того, какие права назначены самим файлам пакета.

    Сценарий 1: Создание системного пользователя

    Рассмотрим классическую задачу: вашему веб-серверу или базе данных требуется собственный системный пользователь для безопасной работы (например, пользователь nginx или postgres).

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

    Следовательно, пользователь должен быть создан до распаковки файлов. Идеальное место для этого — секция %pre.

    Обратите внимание на конструкцию || (логическое ИЛИ) и финальный exit 0. Это важнейшее правило написания скриптлетов: скриптлет никогда не должен завершаться с ошибкой (ненулевым кодом возврата).

    Если скриптлет %pre вернет ошибку (например, команда useradd упадет, потому что пользователь уже существует, а вы это не проверили), пакетный менеджер мгновенно прервет установку пакета. Система останется в консистентном, но незавершенном состоянии. Принудительный exit 0 в конце гарантирует, что даже если промежуточные некритичные команды выдали предупреждения, общая установка продолжится.

    Сценарий 2: Обновление кэша библиотек

    Если ваш пакет устанавливает новые динамические библиотеки (.so файлы) в стандартные системные пути (например, /usr/lib64), операционная система не узнает об их существовании автоматически. Чтобы программы могли найти новые библиотеки, необходимо обновить кэш линкера с помощью утилиты ldconfig.

    Файлы библиотек уже должны находиться на диске, чтобы утилита смогла их проиндексировать. Поэтому эта операция выполняется в секции %post.

    Точно так же, когда пакет удаляется, библиотеки исчезают с диска. Кэш снова становится неактуальным (в нем остаются «мертвые» ссылки). Поэтому кэш нужно обновить еще раз, уже в секции %postun.

    Анатомия обновления: Пересечение реальностей

    Установка и удаление — простые линейные процессы. Настоящая сложность скриптлетов раскрывается во время обновления пакета.

    Транзакция обновления (Upgrade Transaction) в RPM — это не перезапись файлов поверх старых. Это последовательная установка новой версии пакета с последующим удалением старой версии в рамках единого непрерывного процесса.

    !Схема выполнения скриптлетов при обновлении пакета

    Порядок выполнения скриптлетов при обновлении пакета myapp с версии 1.0 на версию 2.0 выглядит так:

  • Выполняется %pre пакета версии 2.0.
  • Распаковываются файлы версии 2.0 (перезаписывая совпадающие файлы версии 1.0).
  • Выполняется %post пакета версии 2.0.
  • Выполняется %preun пакета версии 1.0.
  • Удаляются файлы версии 1.0 (только те, которых больше нет в версии 2.0).
  • Выполняется %postun пакета версии 1.0.
  • Здесь кроется опасная логическая ловушка. Представьте, что в пакете версии 1.0 вы написали скриптлет %postun, который удаляет системного пользователя myapp (ведь пакет удаляется, зачем оставлять мусор?).

    Что произойдет при обновлении до версии 2.0?

  • Шаг 1: %pre (v2.0) убедится, что пользователь myapp существует.
  • Шаг 2-3: Файлы установятся, сервис запустится.
  • Шаг 6: Выполнится %postun от старой версии 1.0 и... удалит пользователя myapp!
  • В результате новая версия пакета установлена, но ее системный пользователь уничтожен старым скриптлетом. Сервис сломан.

    Аргумент 1.

    Аргумент 1 для разных скриптлетов:

    | Скриптлет | При чистой установке | При обновлении (для новой версии) | При обновлении (для старой версии) | При полном удалении | | :--- | :--- | :--- | :--- | :--- | | %pre | 1 | 2 | - | - | | %post | 1 | 2 | - | - | | %preun | - | - | 1 | 0 | | %postun | - | - | 1 | 0 |

    Разберем механику на примере обновления с версии 1.0 на 2.0:

  • Когда запускается %pre версии 2.0, в системе уже есть версия 1.0. Версия 2.0 готовится к установке. Итого будет 2 пакета. 1 равен 1.
  • Если бы мы просто удаляли пакет (без обновления), то после удаления не осталось бы ни одной версии. 1
  • Вернемся к проблеме с удалением пользователя. Чтобы старый пакет не ломал систему при обновлении, логику удаления нужно обернуть в условный оператор, проверяющий 1 -eq 0 ]; then userdel myapp || : fi spec %post if [ 1 и учетом chroot — сложная задача. Чтобы избавить мейнтейнеров от рутины и ошибок, в экосистеме RPM были созданы специализированные макросы для типовых задач.

    Управление Systemd-сервисами

    Если ваш пакет содержит юнит-файл для systemd (например, myapp.service), вам нужно:

  • После установки: сообщить systemd о новом файле (daemon-reload) и применить системные пресеты (включить автозапуск, если это разрешено политикой дистрибутива).
  • Перед удалением: остановить сервис и отключить автозапуск.
  • После обновления: перезапустить сервис, чтобы он подхватил новый бинарный файл.
  • Вместо написания десятков строк shell-кода с проверками 1. Не выполняйте деструктивные действия (удаление данных, остановку сервисов), не убедившись, что это полное удаление пакета, а не промежуточный этап обновления.

  • Минимизируйте зависимости. Чем больше внешних утилит требует ваш скриптлет, тем выше шанс сломать установку в нестандартных окружениях (контейнеры, chroot).
  • Не взаимодействуйте с пользователем. Скриптлеты выполняются в фоновом режиме. Команды вроде read -p "Введите пароль:"` навсегда подвесят транзакцию пакетного менеджера, так как у скриптлета нет доступа к интерактивному терминалу.
  • Понимание того, как и в каком порядке выполняются скриптлеты, позволяет создавать пакеты, которые не только корректно устанавливают файлы, но и бесшовно интегрируются в экосистему операционной системы, не оставляя мусора и не ломая существующие сервисы при обновлениях.

    15. Секция %changelog: Стандарты ведения истории изменений пакета

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

    Если преамбула описывает текущее состояние пакета, а секции %prep, %build и %install — механику его создания, то %changelog отвечает на вопрос «почему пакет стал таким, какой он есть сейчас?». Этот раздел критически важен для командной работы, аудита безопасности и отладки регрессий.

    Анатомия записи в журнале

    Журнал изменений состоит из последовательности блоков. Каждый блок описывает одну ревизию пакета и строго разделен на заголовок (метаданные транзакции) и тело (список изменений).

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

    Строгий синтаксис заголовка

    Заголовок записи всегда начинается с символа звездочки *, за которым следует дата, имя автора, контактный email и идентификатор версии-релиза.

    Формат даты не терпит вольностей. Пакетный менеджер использует жесткий парсер, ожидающий дату в формате POSIX (локаль C / en_US).

    Структура даты: День_недели Месяц День Год.

  • День недели: ровно 3 буквы на английском (Mon, Tue, Wed, Thu, Fri, Sat, Sun). Использование русских сокращений (Пн, Вт) приведет к ошибке сборки.
  • Месяц: ровно 3 буквы на английском (Jan, Feb, Mar и т.д.).
  • День: одно или два числа (от 1 до 31).
  • Год: четыре цифры (например, 2023).
  • После даты указывается имя мейнтейнера и его email в угловых скобках < >. Завершает строку связка Version-Release, к которой относятся описанные изменения.

    > Если вы ошибетесь в дне недели (например, напишете Mon для даты, которая в реальности выпадает на вторник), утилита сборки rpmbuild выдаст предупреждение bad date in %changelog, а строгие системы контроля качества (например, в Rosa Linux) могут отклонить такой пакет.

    Семантика тела записи: Что писать, а что нет

    Тело записи представляет собой маркированный список. Традиционно каждый пункт начинается с дефиса - или звездочки *.

    Главное правило ведения журнала: %changelog описывает изменения в пакетировании, а не изменения в самой программе.

    Мейнтейнеры-новички часто совершают ошибку, копируя в spec-файл длинные списки новых функций из официального релиза разработчика (апстрима). Этого делать не нужно. Пользователь может прочитать о новых функциях программы на сайте разработчика. Задача RPM-журнала — задокументировать действия упаковщика.

    Обязательные элементы для фиксации

  • Обновление версии апстрима. Достаточно одной строки: - Update to 2.1.0.
  • Изменение зависимостей. Если вы добавили или удалили пакеты в тегах Requires или BuildRequires, это нужно указать: - Add BuildRequires: cmake for new build system.
  • Работа с патчами. Любое добавление, удаление или обновление патча должно быть обосновано: - Add 0001-fix-build-on-aarch64.patch.
  • Изменения в макросах и путях. Если вы изменили логику установки файлов или права доступа: - Fix unowned directory issue in /var/lib/myapp.
  • Документирование уязвимостей (CVE)

    Особое внимание уделяется исправлениям безопасности. Если вы накладываете патч, закрывающий известную уязвимость, вы обязаны указать её идентификатор CVE (Common Vulnerabilities and Exposures) в тексте чейнджлога.

    Пример правильной записи: - Backport upstream patch to fix buffer overflow (CVE-2023-45678)

    Зачем это нужно? Инфраструктура дистрибутива (включая платформу сборки ABF в Rosa Linux) автоматически сканирует секции %changelog всех пакетов. Если сканер видит упоминание CVE, он связывает этот пакет с базой данных безопасности и автоматически генерирует уведомление для пользователей (Security Advisory) о том, что обновление закрывает критическую дыру.

    Сравнение плохих и хороших записей

    | Плохая запись (неинформативно) | Хорошая запись (профессионально) | | :--- | :--- | | - Fixed bugs | - Add patch to fix crash on startup with Wayland | | - Updated dependencies | - Drop obsolete Requires: python2 | | - New version | - Update to 3.4.1 | | - Added cool feature X (копирование апстрима) | - Enable feature X by adding BuildRequires: libX-devel |

    Автоматизация рутины: утилита rpmdev-bumpspec

    Ручное написание заголовков с высчитыванием правильного дня недели и инкрементированием номера релиза — утомительный процесс, ведущий к опечаткам. Для автоматизации этой задачи в экосистеме RPM существует утилита rpmdev-bumpspec (входит в пакет rpmdevtools).

    Эта консольная команда автоматически:

  • Читает текущий spec-файл.
  • Увеличивает значение тега Release на единицу (например, с 1 на 2).
  • Генерирует правильный заголовок в секции %changelog с текущей датой, вашим именем и email (берет из настроек системы или переменных окружения).
  • Вставляет переданный вами текст комментария.
  • Пример использования в терминале:

    После выполнения этой команды spec-файл будет обновлен, и вам останется только закоммитить изменения.

    Эволюция чейнджлогов: Git и сборочные платформы

    В современных дистрибутивах, таких как Rosa Linux, процесс разработки пакетов тесно интегрирован с системами контроля версий (Git).

    В классическом подходе мейнтейнер делает двойную работу: сначала пишет комментарий в %changelog внутри spec-файла, а затем пишет похожее сообщение для коммита в Git (например, git commit -m "Update to 2.0").

    Чтобы избежать дублирования, современные сборочные платформы (включая ABF) умеют автогенерировать секцию %changelog прямо из истории Git-коммитов в момент сборки RPM-пакета.

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

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

    2. Заполнение преамбулы: Обязательные метаданные пакета

    Заполнение преамбулы: Обязательные метаданные пакета

    Любой RPM-пакет начинается с паспорта — набора метаданных, которые описывают его сущность. Когда системный администратор вводит в терминале команду dnf info htop или ищет программу в графическом центре приложений, он видит именно ту информацию, которую мейнтейнер заложил в преамбулу spec-файла.

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

    В этой статье мы детально разберём каждый обязательный тег преамбулы, изучим стандарты Rosa Linux и научимся элегантно обходить ограничения пакетного менеджера.

    !Связь между кодом spec-файла и выводом пакетного менеджера

    Базовая идентификация: NVR (Name, Version, Release)

    Триада NVR — это фундамент пакета. Комбинация этих трёх полей формирует уникальное имя файла итогового RPM-архива (например, htop-3.2.2-1.res2023.1.x86_64.rpm).

    Name (Имя пакета)

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

    Правила хорошего тона и стандарты Rosa Linux:

  • Избегайте заглавных букв (пишем network-manager, а не NetworkManager).
  • Не используйте нижнее подчёркивание _ (оно допустимо технически, но исторически в RPM для разделения слов используется дефис -).
  • Если вы упаковываете библиотеку для языка программирования, используйте стандартные префиксы: python3-requests, rubygem-rails, golang-github-user-repo.
  • Version (Версия)

    Тег Version должен строго соответствовать версии, которую присвоил разработчик исходного кода (апстрим). Однако здесь кроется главный подводный камень RPM.

    > В теге Version категорически запрещено использовать дефис -.

    RPM использует дефис как разделитель между именем, версией и релизом. Если апстрим выпустил версию 1.0-beta, попытка записать её как Version: 1.0-beta приведёт к фатальной ошибке парсера при сборке.

    Как решать эту проблему? RPM предлагает два специальных символа для управления сортировкой версий: тильду ~ и карет ^.

    Использование тильды (~) для предрелизов: Символ тильды имеет отрицательный вес при сравнении версий. Это идеальный инструмент для альфа, бета и релиз-кандидатов (RC). Если вы запишете Version: 1.0~beta, пакетный менеджер будет считать, что эта версия младше, чем 1.0.

    Эволюция версий пакета будет выглядеть так (от старой к новой):

  • 1.0~alpha
  • 1.0~beta
  • 1.0~rc1
  • 1.0 (Финальный релиз)
  • Использование карета (^) для пост-релизов: Символ ^ работает наоборот — он имеет положительный вес, но меньший, чем обычная точка или цифра. Он используется, когда апстрим выпустил патч или снапшот после релиза, но не изменил основную версию. Например, 1.0^20231025git будет считаться старше, чем 1.0, но младше, чем 1.1.

    Release (Релиз сборки)

    Тег Release показывает, сколько раз пакет данной версии был собран для дистрибутива.

    Если вы взяли исходники версии 2.0 и собрали пакет, релиз будет 1. Если на следующий день вы обнаружили, что забыли добавить конфигурационный файл, вы вносите изменения в spec-файл и увеличиваете релиз до 2. Версия программы при этом не меняется.

    В Rosa Linux (и большинстве современных RPM-дистрибутивов) к номеру релиза обязательно добавляется макрос дистрибутива %{?dist}.

    Макрос %{?dist} автоматически разворачивается сборочной системой ABF в идентификатор конкретной платформы. Например, при сборке для Rosa Fresh R12 он превратится в .res12.1, и итоговый релиз станет 1.res12.1. Это позволяет использовать один и тот же spec-файл для сборки под разные версии операционной системы, избегая конфликтов в репозиториях.

    Тяжёлая артиллерия: Тег Epoch

    Иногда разработчики исходного кода совершают ошибки. Представьте ситуацию: программа годами выпускалась с версиями 2021.1, 2022.3, 2023.1. А затем автор решил перейти на семантическое версионирование и выпустил версию 3.0.0.

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

    Для решения таких катастроф существует тег Epoch (Эпоха). Это число (по умолчанию 0), которое имеет абсолютный, наивысший приоритет при сравнении NVR.

    Пакет с Epoch: 1 и Version: 1.0 всегда будет считаться новее, чем пакет с Epoch: 0 и Version: 999.9.

    > Использование Epoch — это крайняя мера. Единожды добавив Epoch в пакет, вы уже никогда не сможете его убрать (иначе пакет откатится назад). Применяйте его только тогда, когда апстрим непоправимо сломал схему версионирования.

    Описательные метаданные

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

    Summary (Краткое описание)

    Summary — это визитная карточка пакета.

  • Длина не должна превышать 80 символов.
  • Текст не должен заканчиваться точкой.
  • Описание должно быть объективным (не «Лучший в мире текстовый редактор», а «Консольный текстовый редактор с подсветкой синтаксиса»).
  • License (Лицензия)

    Rosa Linux строго следит за соблюдением авторских прав. В теге License запрещено писать произвольный текст. Необходимо использовать стандартизированные короткие идентификаторы SPDX (Software Package Data Exchange).

    Примеры правильных идентификаторов:

  • GPL-2.0-only
  • GPL-3.0-or-later
  • MIT
  • Apache-2.0
  • Если пакет содержит код под разными лицензиями, используются логические операторы and (и) и or (или):

    | Ситуация | Синтаксис SPDX | Пояснение | | :--- | :--- | :--- | | Разные файлы под разными лицензиями | MIT and GPL-2.0-only | Пользователь обязан соблюдать обе лицензии одновременно. | | Двойное лицензирование (на выбор) | GPL-2.0-only or CDDL-1.0 | Пользователь может выбрать, под какой лицензией использовать код. |

    URL

    Тег URL должен указывать на домашнюю страницу проекта, где пользователь может найти документацию и информацию о разработчике. Если сайта нет, указывается ссылка на репозиторий (GitHub, GitLab).

    Управление исходным кодом: Source и Patch

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

    Source (Исходники)

    Тег Source0 (или просто Source) указывает на главный архив с исходным кодом.

    Лучшая практика — указывать полный URL до архива, а не просто имя файла. Даже если сборочный сервер ABF изолирован от интернета и использует заранее загруженный кэш, полный URL позволяет другим разработчикам и утилитам (например, spectool) автоматически скачать нужный архив для локальной проверки.

    Если пакет требует дополнительных файлов, которые не входят в апстрим-архив (например, специфичный для Rosa Linux конфигурационный файл или systemd-сервис), они добавляются как Source1, Source2 и так далее. Эти файлы должны лежать рядом со spec-файлом в репозитории пакета.

    Patch (Патчи)

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

  • Исправления критических уязвимостей (CVE), если апстрим ещё не выпустил новую версию.
  • Адаптации путей под стандарты файловой системы Rosa Linux.
  • Отключения нестабильного функционала.
  • Имена патчей принято начинать с порядкового номера, чтобы в директории они сортировались в порядке применения: Patch0: 0001-fix-buffer-overflow.patch.

    Архитектура пакета: BuildArch и ExclusiveArch

    По умолчанию RPM собирает пакет под ту архитектуру процессора, на которой запущена сборка (например, x86_64 или aarch64). Это логично для программ на C/C++, Rust или Go, которые компилируются в машинный код.

    Однако множество пакетов не содержат бинарного кода. Это могут быть:

  • Скрипты на Python, Bash, Ruby.
  • Шрифты, иконки, обои рабочего стола.
  • Текстовая документация.
  • Для таких пакетов необходимо указать тег BuildArch: noarch (No Architecture).

    Что это даёт?

  • Экономия ресурсов. Сборочной системе ABF не придётся собирать этот пакет 4 раза для разных архитектур. Он будет собран один раз, и полученный файл my-python-script-1.0-1.noarch.rpm будет доступен для всех платформ.
  • Экономия места. В репозитории дистрибутива будет лежать только один файл, а не дубликаты для каждой архитектуры.
  • Если же программа принципиально работает только на одной архитектуре (например, утилита для настройки специфичного x86-процессора), используется тег ExclusiveArch: x86_64. На других платформах сборка даже не будет запускаться.

    Директива %description

    В отличие от тегов преамбулы, которые пишутся в формате «Ключ: Значение», описание пакета оформляется в виде директивы %description.

    Здесь вы можете подробно рассказать, что делает программа, перечислить её ключевые функции и особенности.

    Правила оформления %description:

  • Текст может состоять из нескольких абзацев (разделяются пустой строкой).
  • Строки должны быть принудительно перенесены так, чтобы их длина не превышала 80 символов. Это необходимо для корректного отображения в консольных утилитах.
  • Не используйте Markdown или HTML — пакетные менеджеры выводят этот текст как есть (plain text).
  • Синтаксис зависимостей в преамбуле

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

    Теги Requires (зависимости времени выполнения) и BuildRequires (зависимости времени сборки) поддерживают указание конкретных версий с помощью математических операторов: =, <, >, <=, >=.

    Современный RPM также поддерживает Rich Dependencies (Богатые зависимости) — возможность использовать логические операторы. Это крайне полезно, когда пакет может работать с разными реализациями одной технологии.

    В этом случае пакетный менеджер при установке проверит: если в системе уже есть mysql-server, он будет удовлетворён. Если нет ни того, ни другого, он по умолчанию установит первый вариант из скобок (mariadb-server).

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

    3. Исходные коды и патчи в преамбуле: Теги Source и Patch

    Исходные коды и патчи в преамбуле: Теги Source и Patch

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

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

    Принцип неизменности исходного кода (Pristine Sources)

    Прежде чем переходить к синтаксису, необходимо усвоить главное философское правило RPM-пакетирования — принцип неизменности исходного кода (Pristine Sources).

    Мейнтейнер дистрибутива (например, Rosa Linux) получает исходный код от автора программы (апстрима) обычно в виде архива — тарбола (от расширения .tar.gz или .tar.xz).

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

    Почему это правило так важно:

  • Аудит и безопасность. Любой пользователь или другой разработчик должен иметь возможность скачать оригинальный архив с официального сайта апстрима, сравнить его контрольную сумму с той, что используется в дистрибутиве, и убедиться, что в код не внедрены скрытые закладки.
  • Прозрачность изменений. Если вы изменили код внутри архива, никто не узнает, что именно вы поменяли. Изменения должны быть выделены в отдельные файлы (патчи), которые легко прочитать.
  • Упрощение обновлений. Когда выйдет новая версия программы, вам не придётся заново вспоминать и вручную вносить свои изменения в новый архив. Вы просто скачаете новый оригинальный тарбол и примените к нему старые патчи (если они всё ещё актуальны).
  • Представьте, что вы купили запчасть для автомобиля. Вы не вскрываете заводскую упаковку на складе, чтобы подпилить деталь напильником. Вы храните её в оригинальной коробке, а модифицируете только в момент установки в мастерской. В RPM склад — это преамбула, а мастерская — секция подготовки.

    Тег Source: Опись базовых ингредиентов

    Тег Source (или Source0) указывает на главный архив с исходным кодом программы. Если пакет требует дополнительных файлов, которые не поставляются разработчиком, используются теги Source1, Source2 и так далее.

    Полные URL против локальных файлов

    Технически RPM позволяет указать в качестве источника просто имя файла:

    Однако современный стандарт пакетирования требует указывать полный URL до файла на сервере разработчика:

    Указание полного пути решает сразу несколько задач:

  • Прослеживаемость. Сразу понятно, откуда взят код. Не нужно искать официальный сайт проекта.
  • Автоматизация. Существуют утилиты, такие как spectool, которые умеют парсить spec-файл, находить все теги Source с полными URL и автоматически скачивать нужные архивы в сборочную директорию одной командой.
  • > Важный нюанс: при сборке пакета на серверах Rosa Linux (в платформе ABF) сборочные контейнеры изолированы от интернета. Сборочная система не скачивает файл по этому URL во время компиляции. Она использует URL только как метаданные, а сам файл берёт из специального хранилища — Lookaside cache (кэш сторонних файлов), куда мейнтейнер загружает архив заранее.

    Использование макросов в URL

    Чтобы не переписывать URL каждый раз при обновлении версии пакета, ссылки конструируются с использованием макросов %{name} и %{version}.

    При парсинге этого файла RPM автоматически подставит значения, и ссылка превратится в https://github.com/htop-dev/htop/releases/download/3.2.2/htop-3.2.2.tar.xz.

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

    Дополнительные исходные файлы (Source1, Source2...)

    Часто апстрим предоставляет только код самой программы, но для интеграции в дистрибутив требуются специфичные файлы. Например:

  • Файл конфигурации по умолчанию, оптимизированный для Rosa Linux.
  • Файл сервиса для системы инициализации (systemd).
  • Иконка для меню приложений, если автор её не нарисовал.
  • Эти файлы создаются мейнтейнером, кладутся рядом со spec-файлом и объявляются в преамбуле:

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

    Тег Patch: Хирургическое вмешательство в код

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

    Патч — это текстовый файл, содержащий разницу (diff) между оригинальным файлом и изменённым. Он показывает, какие строки нужно удалить, а какие добавить.

    Зачем нужны патчи в дистрибутиве?

    Идеальный пакет собирается вообще без патчей. Но в реальности они необходимы для решения нескольких типовых задач:

  • Бэкпортирование (Backporting) исправлений безопасности. В программе найдена критическая уязвимость. Разработчик исправил её в ветке разрабатываемой версии 2.0, но пользователи дистрибутива сидят на стабильной версии 1.5. Мейнтейнер берёт исправление из новой версии и оформляет его в виде патча для старой.
  • Адаптация путей. Программа жёстко ожидает найти свои данные в /usr/local/share, а стандарты дистрибутива требуют использовать /usr/share.
  • Исправление ошибок компиляции. Новая версия компилятора GCC стала строже относиться к синтаксису C++, и старый код перестал собираться. Мейнтейнер пишет патч, исправляющий синтаксис.
  • Синтаксис и правила именования

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

    Стандарты оформления патчей:

  • Говорящее название. Имя файла должно чётко отражать суть изменений. Название patch1.patch считается грубым нарушением.
  • Порядковый номер. Имя принято начинать с четырехзначного числа (0001-, 0002-). Это гарантирует, что при просмотре директории с файлами пакета они будут отсортированы в том же порядке, в котором применяются.
  • Формат Git. Лучшие патчи генерируются командой git format-patch. Они содержат внутри себя метаданные: кто автор исправления, дату и подробное текстовое описание причины изменений.
  • !Схема потока данных: оригинальный архив и патчи из преамбулы передаются в секцию подготовки, где объединяются в готовый к компиляции код.

    Связь преамбулы с секцией подготовки

    Важно понимать границу ответственности. Теги Source и Patch в преамбуле — это исключительно декларация намерений. Написав Patch0: my-fix.patch, вы лишь сообщаете пакетному менеджеру: «У меня есть такой файл, учти его».

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

    Если в преамбуле объявлено 10 патчей, этот макрос последовательно применит их к распакованному коду. Если хотя бы один патч не подойдёт (например, из-за того, что апстрим сильно переписал файл в новой версии), процесс сборки немедленно прервётся с ошибкой. Это защитный механизм, гарантирующий, что сломанный код не попадёт к пользователям.

    Уровни вложенности патчей (Patch levels)

    При создании патча утилита diff записывает пути к изменённым файлам. Часто эти пути включают название корневой директории архива.

    Например, внутри патча путь может выглядеть так: a/htop-3.2.2/src/main.c

    Когда сборочная система распаковывает архив, она уже находится внутри директории htop-3.2.2. Если она попытается применить патч по указанному пути, она будет искать htop-3.2.2/htop-3.2.2/src/main.c и выдаст ошибку.

    Для решения этой проблемы существует концепция уровня отсечения путей (strip level), обозначаемая флагом -p (от слова path).

  • Флаг -p0 означает «использовать пути из патча как есть».
  • Флаг -p1 (стандарт де-факто) означает «отбросить первый уровень директорий в пути» (в нашем примере отбросится a/htop-3.2.2/, и останется правильный путь src/main.c).
  • Современные макросы подготовки по умолчанию применяют все патчи с уровнем -p1. Поэтому при создании собственных патчей мейнтейнеры всегда генерируют их с учётом этого стандарта.

    Подводные камни при работе с исходниками

    Проблема динамических архивов GitHub

    Многие разработчики не загружают готовые релизные архивы, а полагаются на функцию GitHub «Download ZIP/TAR.GZ», которая генерирует архив на лету из тега в Git.

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

    Поэтому мейнтейнеры предпочитают использовать стабильные релизные ассеты (Release Assets), которые разработчик загружает вручную, либо фиксируют конкретный коммит Git, если стабильных релизов нет.

    Сторонние зависимости (Vendoring)

    Некоторые языки программирования (Go, Rust, Node.js) используют пакетные менеджеры, которые скачивают сотни мелких библиотек-зависимостей прямо во время компиляции.

    Как мы помним, сборочная среда изолирована от интернета. Команда npm install или cargo build завершится ошибкой сети.

    В таких случаях мейнтейнер должен заранее собрать все эти мелкие зависимости в один большой архив (vendor tarball) на своём локальном компьютере, имеющем доступ в сеть, и добавить его в spec-файл как Source1. В секции подготовки этот архив распаковывается рядом с основным кодом, позволяя компилятору найти все необходимые библиотеки локально.

    Грамотное управление тегами Source и Patch — это основа воспроизводимости сборки. Если преамбула написана правильно, любой разработчик сможет взять ваш spec-файл через 5 лет, запустить сборку и получить бит-в-бит идентичный RPM-пакет.

    4. Стандартные макросы RPM: Пути к директориям и переменные пакета

    Стандартные макросы RPM: Пути к директориям и переменные пакета

    Представьте, что вы пишете инструкцию для курьера, куда доставить посылку. Вы можете написать: «Город Москва, улица Ленина, дом 15». Это точный, жестко заданный адрес. Но что, если завтра ваша компания откроет филиал в другом городе, где тоже есть улица Ленина, но нужный офис находится в доме 42? Ваша жесткая инструкция приведет к ошибке доставки.

    Вместо этого вы могли бы написать: «Доставить в Главный Офис». Курьер сам посмотрит в справочнике, где находится Главный Офис в текущем городе, и отвезет посылку по верному адресу.

    В мире RPM-пакетирования жестко заданные пути (например, /usr/lib) — это главная причина сломанных пакетов при переносе между разными архитектурами и дистрибутивами. Чтобы сделать spec-файл универсальным «справочником», используются стандартные макросы путей и переменных.

    Философия путей и стандарт FHS

    Исторически в Linux сложилась строгая иерархия директорий, описываемая стандартом FHS (Filesystem Hierarchy Standard). Этот стандарт определяет, что исполняемые файлы пользователей должны лежать в /usr/bin, системные конфигурации — в /etc, а изменяемые данные программ — в /var.

    Однако дьявол кроется в деталях. Разные дистрибутивы могут иметь легкие отклонения от FHS, а архитектура процессора напрямую влияет на расположение библиотек. Если вы напишете в секции установки команду cp myapp /usr/bin/, ваш пакет, скорее всего, соберется. Но это считается плохим тоном и грубым нарушением стандартов Rosa Linux.

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

    Базовые макросы файловой системы

    Ниже приведена таблица основных макросов, которые покрывают 99% потребностей при написании секций установки и описи файлов.

    | Макрос RPM | Стандартный путь в Rosa Linux | Назначение | | :--- | :--- | :--- | | %{_bindir} | /usr/bin | Исполняемые файлы (бинарники), доступные всем пользователям. | | %{_sbindir} | /usr/sbin | Системные исполняемые файлы, требующие прав администратора. | | %{_sysconfdir} | /etc | Глобальные конфигурационные файлы системы и программ. | | %{_datadir} | /usr/share | Архитектурно-независимые данные (иконки, локализации, общие скрипты). | | %{_includedir} | /usr/include | Заголовочные файлы C/C++ (нужны для пакетов с суффиксом -devel). | | %{_mandir} | /usr/share/man | Страницы документации (man pages). | | %{_localstatedir} | /var | Изменяемые данные, которые программа генерирует во время работы (базы данных, кэш). |

    Использование этих макросов гарантирует, что если в будущей версии дистрибутива архитектурный комитет решит переместить документацию из /usr/share/man в /usr/man, вам не придется переписывать сотни spec-файлов. Достаточно будет изменить значение макроса %{_mandir} на уровне сборочной системы, и все пакеты при пересборке автоматически установят файлы по новому адресу.

    !Дерево файловой системы Linux с привязанными к директориям RPM-макросами.

    Проблема %{_libdir} и концепция Multilib

    Самый коварный и важный макрос в арсенале мейнтейнера — это %{_libdir}. Именно он наглядно демонстрирует, зачем нужна абстракция путей.

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

    Если вы жестко пропишете в spec-файле путь /usr/lib, произойдет следующее:

  • На 32-битной архитектуре (i686) пакет соберется корректно.
  • На 64-битной архитектуре (x86_64) пакет положит 64-битные библиотеки в папку для 32-битных. Система не сможет их найти, и программа не запустится.
  • Использование %{_libdir} решает эту проблему элегантно. Сборочная система сама определяет текущую архитектуру и подставляет нужное значение:

  • При сборке под i686 макрос %{_libdir} превращается в /usr/lib.
  • При сборке под x86_64 макрос %{_libdir} превращается в /usr/lib64.
  • При сборке под aarch64 (64-битный ARM) он также станет /usr/lib64.
  • !Попробуйте переключить архитектуру системы — и посмотрите, как один и тот же код spec-файла превращается в разные пути установки.

    Переменные пакета: DRY (Don't Repeat Yourself)

    В преамбуле вы задаете метаданные: название, версию, релиз. Эти данные многократно используются на этапах распаковки, компиляции и установки.

    Чтобы не дублировать текст и не плодить ошибки при обновлениях, RPM автоматически создает макросы из тегов преамбулы:

  • %{name} — возвращает имя пакета (тег Name).
  • %{version} — возвращает версию (тег Version).
  • %{release} — возвращает номер релиза (тег Release).
  • Рассмотрим пример плохой и хорошей практики.

    Плохо (жесткое кодирование):

    Если выйдет версия 3.3.0, вам придется искать по всему spec-файлу строку 3.2.2 и менять ее вручную. Вы обязательно где-нибудь забудете это сделать.

    Хорошо (использование макросов):

    Теперь при обновлении пакета достаточно изменить только одну строку в преамбуле — Version: 3.3.0. Весь остальной код адаптируется автоматически.

    > Обратите внимание на слияние путей: %buildroot и %_datadir пишутся слитно: %{buildroot}%{_datadir}. > Поскольку %{_datadir} уже содержит ведущий слеш (раскрывается как /usr/share), добавление слеша между ними (%{buildroot}/%{_datadir}) приведет к двойному слешу //usr/share. Linux это проглотит, но визуально в логах сборки это выглядит неаккуратно.

    Макросы сборочной среды

    Помимо путей и метаданных, RPM предоставляет макросы, управляющие самим процессом компиляции.

    Главный из них — %{optflags}. Это системная переменная, содержащая стандартизированный набор флагов для компиляторов C и C++ (обычно передается в переменные окружения CFLAGS и CXXFLAGS).

    Зачем нужен %{optflags}? Разработчик программы (апстрим) может написать в своем Makefile базовые флаги компиляции, например -O2 (стандартная оптимизация). Однако дистрибутив Rosa Linux предъявляет жесткие требования к безопасности бинарных файлов.

    Макрос %{optflags} автоматически добавляет флаги, которые:

  • Включают защиту от переполнения буфера (-fstack-protector-strong).
  • Делают код позиционно-независимым для рандомизации адресов в памяти (-fPIE, -pie), что усложняет эксплуатацию уязвимостей.
  • Включают дополнительные предупреждения компилятора.
  • Если вы собираете программу в обход стандартных макросов подготовки и компиляции, вы обязаны вручную передать эти флаги:

    Создание собственных макросов: %global против %define

    Иногда стандартных переменных не хватает, и мейнтейнеру нужно создать свои собственные переменные внутри spec-файла. Для этого существуют две директивы: %global и %define.

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

    Директива %define (Ленивое вычисление)

    Когда вы используете %define, RPM просто запоминает текстовую строку. Само значение вычисляется только в тот момент, когда макрос вызывается в коде.

    Если позже в коде значение %{_bindir} почему-то изменится, вызов %{my_path} вернет новый, измененный результат.

    Директива %global (Немедленное вычисление)

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

    Рассмотрим классический пример, демонстрирующий разницу:

    В современном RPM-пакетировании стандартом де-факто является использование %global. Ленивое вычисление %define часто приводит к непредсказуемому поведению в сложных spec-файлах с множеством условий. Используйте %define только если вы точно понимаете, зачем вам нужно отложенное вычисление.

    Экранирование макросов

    Иногда вам нужно написать в spec-файле символ процента %, который не должен восприниматься как начало макроса. Например, вы пишете скрипт для cron или команду awk прямо внутри секции установки.

    Если вы напишете:

    При сборке пакета RPM удалит один процент, и в итоговый скрипт попадет правильная команда с одним %.

    Отладка: Как узнать значение макроса?

    При написании spec-файла часто возникает вопрос: «А во что именно превратится %{_sysconfdir} на моей системе?». Вам не нужно гадать или запускать полную сборку пакета, чтобы это проверить.

    Для этого существует консольная команда rpm --eval (от слова evaluate — вычислить).

    Откройте терминал и введите:

    Вывод мгновенно покажет:

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

    Вывод на 64-битной системе:

    Команда rpm --eval — это главный инструмент отладки мейнтейнера. Если ваш пакет устанавливает файлы не туда, куда вы ожидали, первым делом проверьте через эту команду, как именно раскрываются ваши макросы путей в текущей сборочной среде.

    Использование стандартных макросов требует привыкания. Поначалу строка %{buildroot}%{_bindir}/%{name} кажется нечитаемой криптографией по сравнению с простым /usr/bin/myapp. Но именно эта абстракция делает ваш пакет профессиональным, надежным и готовым к интеграции в любую архитектуру дистрибутива Rosa Linux.

    5. Пользовательские макросы: Различия между %define и %global

    Пользовательские макросы: Различия между %define и %global

    Создание RPM-пакета редко ограничивается использованием только стандартных путей вроде %{_bindir}. В реальной практике мейнтейнеру постоянно требуется сохранять промежуточные значения: нестандартные версии библиотек, специфические флаги компилятора для конкретной архитектуры или длинные URL-адреса, которые меняются от релиза к релизу.

    Для решения этих задач RPM предоставляет механизм создания пользовательских макросов. Выступая в роли переменных, они позволяют соблюдать принцип DRY (Don't Repeat Yourself) и делают spec-файл читаемым. Однако инструмент создания этих макросов имеет два совершенно разных лица: директивы %define и %global.

    Хотя базовое отличие между ними — время вычисления значения — кажется простым, на практике непонимание механики их работы приводит к самым трудноуловимым ошибкам при сборке пакетов для Rosa Linux.

    Анатомия вычисления: Парсинг против Экспансии

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

    Процесс делится на два основных этапа:

  • Парсинг (Parsing) — RPM читает spec-файл сверху вниз, строит в памяти дерево метаданных и регистрирует все найденные макросы.
  • Экспансия (Expansion) — RPM начинает фактически выполнять секции (%prep, %build, %install), подставляя вместо имен макросов их реальные значения.
  • Директива %global работает на этапе парсинга. Как только RPM встречает эту строку, он немедленно вычисляет всё, что находится справа от имени макроса, и сохраняет в память финальный, неизменный результат.

    Директива %define работает иначе. На этапе парсинга RPM просто берет строку текста справа от имени макроса и сохраняет её «как есть», не пытаясь понять, что внутри. Вычисление (экспансия) происходит только тогда, когда этот макрос вызывается где-то в коде секций сборки. Это называется ленивым вычислением.

    !Жизненный цикл обработки spec-файла: разница во времени вычисления макросов.

    Рассмотрим глубокий пример, демонстрирующий затенение переменных — ситуацию, когда значение базовой переменной меняется по ходу чтения файла.

    Что произойдет в секции %install?

  • Макрос %{strict_log_path} создаст директорию /opt/myapp/logs. В момент его объявления app_root был равен /opt/myapp, и %global намертво зафиксировал этот результат.
  • Макрос %{lazy_log_path} создаст директорию /var/lib/myapp/logs. В момент вызова в секции %install RPM посмотрел на текущее значение app_root, увидел, что оно изменилось, и подставил новое.
  • !Попробуйте изменить базовую переменную — и посмотрите, как по-разному отреагируют %define и %global

    Почему %global — это современный стандарт

    В сообществе Rosa Linux и других современных RPM-дистрибутивах негласным правилом является использование %global для 99% пользовательских переменных.

    Причина кроется в предсказуемости. Spec-файлы часто содержат сложные условные конструкции (например, ifarch для проверки архитектуры), которые могут переопределять базовые переменные. Если вы используете %define, значение вашего макроса может внезапно измениться в середине секции %build просто потому, что сработал какой-то системный триггер, изменивший вложенную переменную. Отлаживать такие «плавающие» значения крайне сложно.

    Используйте %global для:

  • Версий компонентов и суффиксов (например, %global commit_hash a1b2c3d).
  • Кастомных путей установки.
  • Флагов компилятора, специфичных для пакета.
  • Истинное предназначение %define: Параметризованные макросы

    Если %global так хорош, зачем вообще нужен %define? Ответ кроется в функциональности, недоступной для немедленного вычисления: создании макросов, принимающих аргументы.

    Параметризованный макрос — это, по сути, мини-функция внутри spec-файла. Вы можете передать в нее данные при вызове, и она вернет результат, обработанный по заданному шаблону.

    Синтаксис создания параметризованного макроса требует использования %define. Аргументы внутри макроса обозначаются как %1, %2 и так далее (где цифра — порядковый номер переданного аргумента).

    В этом примере %1 заменяется на имя пользователя (torvalds), %2 — на название репозитория (linux), а %3 — на версию (6.5.1).

    Почему здесь нельзя использовать %global? Если вы напишете %global github_url() ..., RPM попытается вычислить значение немедленно в момент объявления. Но в этот момент аргументы %1, %2 и %3 еще не переданы! RPM выдаст ошибку парсинга или подставит пустоту. Ленивое вычисление %define здесь жизненно необходимо: макрос ждет, пока его вызовут с конкретными данными, и только тогда раскрывается.

    Опциональные аргументы

    Иногда макросу нужно передать аргумент, но он не является обязательным. Для этого используется синтаксис %{?1}. Знак вопроса говорит парсеру: «Если первый аргумент передан — подставь его. Если нет — просто проигнорируй и не выдавай ошибку».

    Условные макросы и проверка существования

    Знак вопроса ? применяется не только к аргументам функций, но и к любым переменным. Это мощный инструмент для создания кросс-платформенных spec-файлов, которые адаптируются под сборочную среду.

    Конструкция %{?macro_name} проверяет, существует ли макрос в системе.

  • Если макрос определен, он раскрывается в свое значение.
  • Если макрос НЕ определен, конструкция бесшумно исчезает, не прерывая сборку.
  • Классический пример использования в Rosa Linux — флаги параллельной сборки. Системный макрос %{_smp_mflags} содержит команду вроде -j8 (использовать 8 ядер процессора при компиляции). Однако на некоторых старых или изолированных сборочных узлах этот макрос может быть не задан.

    Если макрос есть, выполнится make -j8. Если его нет, выполнится просто make.

    Значения по умолчанию

    Вы можете пойти дальше и задать фоллбэк (значение по умолчанию), которое применится, если макрос не найден. Для этого используется синтаксис %{?macro_name:значение_по_умолчанию}.

    Здесь мы видим обратную конструкцию: %{!?macro_name}. Восклицательный знак означает логическое НЕ. То есть: «Если макрос НЕ существует, подставь то, что идет после двоеточия».

    Модификаторы раскрытия: Магия работы со строками

    Часто данные, которые мы получаем из метаданных (например, из тега Version), имеют формат, не подходящий для использования в URL или путях. Вместо того чтобы вызывать внешние утилиты вроде sed или awk в секции %prep, RPM позволяет модифицировать строки прямо в момент раскрытия макроса.

    Это называется модификатором раскрытия. Синтаксис выглядит так: %{macro_name:поиск:замена}.

    Представьте, что апстрим (разработчик программы) выпустил предрелизную версию. По правилам версионирования RPM (которые мы разбирали ранее), мы обязаны использовать тильду для предрелизов в теге Version, чтобы сортировка работала корректно.

    Однако архив с исходным кодом на сервере разработчика называется awesome-app-2.0-rc1.tar.gz (с дефисом вместо тильды). Если мы напишем Source0: %{name}-%{version}.tar.gz, RPM попытается скачать awesome-app-2.0~rc1.tar.gz и выдаст ошибку 404.

    Решаем проблему с помощью модификатора:

    Конструкция %{version:~:-} говорит RPM: «Возьми значение макроса version, найди в нем символ ~ и замени его на символ -». В результате формируется правильная ссылка, а метаданные пакета остаются корректными.

    Удаление префиксов и суффиксов

    Еще один частый сценарий — удаление лишних символов в начале или конце строки. Для этого используются символы # (удаление спереди) и % (удаление сзади), заимствованные из синтаксиса bash.

    Допустим, макрос содержит путь к файлу, а нам нужно получить только имя файла (отсечь директории):

    Эти встроенные инструменты манипуляции строками делают spec-файл независимым от внешних утилит на этапе парсинга преамбулы, где вызов shell-команд невозможен или крайне нежелателен.

    Пространство имен и безопасность

    Создавая собственные макросы, вы вторгаетесь в глобальное пространство имен RPM. Если вы назовете свой макрос %global libdir /my/custom/path, вы перезапишете системный макрос %{_libdir} (если опечатаетесь) или создадите конфликт с макросами других пакетов, если ваш spec-файл используется как шаблон.

    В сообществе Rosa Linux принято соблюдать правила гигиены при именовании пользовательских переменных:

  • Используйте префиксы. Начинайте имена своих макросов с подчеркивания и короткого идентификатора пакета. Например, для пакета nginx кастомный макрос лучше назвать %global _nginx_conf_dir /etc/nginx/conf.d.
  • Избегайте коротких общих имен. Имена вроде %global path, %global dir или %global flag — это бомба замедленного действия. Рано или поздно они пересекутся с системными переменными.
  • Удаляйте временные макросы. Если макрос нужен только для сложного вычисления в одной секции, его можно удалить после использования с помощью директивы %undefine macro_name. Это освобождает память и предотвращает случайное использование старого значения ниже по коду.
  • Понимание тонкой грани между %global и %define, а также умение использовать параметризацию и модификаторы строк, отличает начинающего упаковщика от профессионального мейнтейнера. Эти инструменты позволяют создавать элегантные, самоадаптирующиеся spec-файлы, которые легко поддерживать годами, не переписывая код при каждом минорном обновлении программы.

    6. Секция %prep: Подготовка среды и распаковка исходников макросом %setup

    Секция %prep: Подготовка среды и распаковка исходников макросом %setup

    Преамбула spec-файла описывает пакет снаружи: как он называется, от чего зависит и кто его написал. Но как только пакетный менеджер считывает метаданные, начинается реальная работа. Секция %prep — это первый этап физической сборки пакета. Это мост между неизменными исходными архивами, скачанными из интернета, и готовым к компиляции кодом.

    В этой статье мы разберем механику работы секции %prep, детально изучим макрос %setup (и его современного наследника %autosetup), научимся обезвреживать нестандартные архивы и подготавливать код по стандартам Rosa Linux.

    Анатомия сборочной директории

    Прежде чем распаковывать файлы, нужно понимать, куда они распаковываются. Когда вы запускаете сборку RPM-пакета, система создает строго регламентированную структуру директорий (обычно в ~/rpmbuild/).

    Нас интересует директория BUILD. Это изолированная «песочница», в которой происходит вся магия секций %prep и %build.

    Главное правило секции %prep: каждый раз при запуске сборки директория пакета внутри BUILD должна создаваться с нуля. Пакетный менеджер должен быть уверен, что в коде не осталось артефактов от предыдущих неудачных попыток компиляции. Именно поэтому ручная распаковка через стандартные утилиты вроде tar -xzf в spec-файлах не применяется. Вместо этого используется специализированный макрос.

    Макрос %setup: Швейцарский нож распаковки

    Макрос %setup — это встроенная функция RPM, которая берет на себя рутину по подготовке исходного кода. На этапе парсинга spec-файла RPM разворачивает этот макрос в полноценный shell-скрипт.

    Базовый вызов макроса без аргументов:

    Под капотом этот короткий вызов выполняет следующую последовательность действий:

  • Переходит в директорию BUILD.
  • Удаляет директорию %{name}-%{version}, если она там уже существует.
  • Распаковывает архив, указанный в теге Source0.
  • Проверяет, что в результате распаковки действительно появилась директория %{name}-%{version}.
  • Меняет владельца и права доступа к распакованным файлам (обычно chmod -R a+rX).
  • Переходит внутрь созданной директории.
  • Если разработчик программы (апстрим) следует стандартам и упаковывает код в архив так, что внутри лежит корневая папка с именем название-версия, базового вызова %setup будет достаточно. Но в реальном мире так бывает редко.

    Проблема несовпадения имен: флаг -n

    Самая частая ошибка начинающих мейнтейнеров — падение сборки на этапе %prep с сообщением Bad exit status from /var/tmp/rpm-tmp... (No such file or directory).

    Это происходит, когда имя корневой папки внутри архива не совпадает с ожидаемым %{name}-%{version}. Классический пример — скачивание релизов с GitHub.

    Допустим, мы собираем пакет awesome_app версии 2.0. По умолчанию %setup ожидает, что архив распакуется в папку awesome_app-2.0. Однако GitHub при генерации архива из тега часто называет корневую папку в формате ИмяРепозитория-Версия. Если репозиторий называется AwesomeApp-core, папка внутри архива будет AwesomeApp-core-2.0.

    Чтобы объяснить макросу, какую папку искать после распаковки, используется флаг -n (name):

    Теперь RPM распакует архив, найдет папку AwesomeApp-core-2.0, перейдет в нее и продолжит сборку без ошибок.

    !Попробуйте разные флаги макроса %setup и посмотрите, как меняется структура директорий и статус сборки.

    Обезвреживание «тарбомб»: флаг -c

    Тарбомба (Tarbomb) — это архив, созданный с нарушением негласных правил хорошего тона. Вместо того чтобы содержать одну корневую директорию, внутри которой лежат все файлы проекта, тарбомба содержит десятки или сотни файлов прямо в корне архива.

    Если распаковать такой архив стандартным %setup, файлы вывалятся прямо в общую директорию BUILD, перемешавшись с исходниками других пакетов (если они там есть), а RPM выдаст ошибку, так как не найдет ожидаемую корневую папку.

    Для решения этой проблемы существует флаг -c (create directory before extracting):

    С этим флагом алгоритм меняется:

  • RPM сначала создает пустую директорию %{name}-%{version}.
  • Переходит в нее.
  • Распаковывает архив внутри этой новой директории.
  • Если тарбомба имеет еще и нестандартное имя, флаги можно комбинировать: %setup -c -n custom-name.

    Тихий режим: флаг -q

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

    Это критически усложняет поиск реальных ошибок компиляции. Поэтому в 99% spec-файлов вы увидите флаг -q (quiet), который подавляет вывод списка файлов:

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

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

    В преамбуле они объявляются как Source0, Source1, Source2 и так далее. Но базовый %setup распаковывает только Source0. Для остальных нужны дополнительные инструкции.

    Флаги -a и -b

    Для распаковки дополнительных архивов используются флаги -a (after) и -b (before). Цифра после флага указывает номер исходника (Source).

    * -a 1 — распаковать Source1 после перехода в корневую директорию проекта. * -b 1 — распаковать Source1 до перехода в корневую директорию проекта (то есть прямо в BUILD).

    Рассмотрим практический пример. У нас есть веб-сервер и отдельный архив с темами оформления для его админ-панели.

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

    Ручной контроль: флаги -T и -D

    Иногда логика сборки настолько сложна, что автоматика %setup только мешает. Например, если Source0 — это вообще не архив, а одиночный конфигурационный файл, макрос попытается его «распаковать» и выдаст ошибку.

    В таких случаях автоматику отключают: * -T — отключает автоматическую распаковку Source0. * -D — отключает автоматическое удаление директории перед распаковкой.

    Эти флаги часто используются при последовательном вызове нескольких макросов %setup:

    !Схема движения файлов: от скачивания до подготовки в директории BUILD.

    Эволюция: от %setup к %autosetup

    В современных дистрибутивах, включая Rosa Linux, стандартом де-факто стал макрос %autosetup.

    Главное отличие %autosetup от %setup заключается в работе с патчами. Ранее мейнтейнерам приходилось вручную перечислять применение каждого патча с помощью макроса %patch0, %patch1 и так далее. Если патчей было 50, секция %prep превращалась в нечитаемую простыню кода.

    %autosetup автоматически находит все теги Patch в преамбуле и применяет их по порядку сразу после распаковки исходников.

    Важно понимать: %autosetup полностью наследует все флаги %setup. Вы можете писать %autosetup -q -n custom_dir -a 1, и это сработает точно так же, плюс автоматически накатятся патчи.

    Единственный специфичный для %autosetup флаг — это -p (strip level), который передается утилите patch. Обычно используется %autosetup -p1, что указывает утилите отсечь первый уровень вложенности директорий в путях внутри diff-файла.

    Shell-скриптинг в %prep: Подготовка кода

    Распаковка и наложение патчей — это лишь половина дела. Секция %prep выполняется как обычный bash-скрипт, что позволяет использовать любые стандартные утилиты Linux (sed, awk, find, rm) для модификации исходников перед компиляцией.

    Зачем модифицировать код, если есть патчи? Патчи идеальны для изменения логики программы (исправление багов в C/C++ коде). Но для рутинных сборочных задач shell-команды подходят лучше.

    Дебандлинг (Unbundling)

    Дебандлинг — это процесс удаления сторонних библиотек, которые разработчик «заботливо» вложил внутрь своего архива.

    Политика безопасности Rosa Linux (как и Fedora, Red Hat) строго запрещает использование встроенных (bundled) библиотек. Если приложение использует zlib или sqlite, оно должно линковаться с системными версиями этих библиотек, а не компилировать свои собственные копии. Это гарантирует, что при обнаружении уязвимости в sqlite, достаточно обновить один системный пакет, а не пересобирать сотни приложений, которые принесли sqlite с собой.

    В секции %prep дебандлинг выглядит как безжалостное удаление директорий:

    Адаптация путей с помощью sed

    Часто разработчики хардкодят пути, специфичные для их локальных машин или других операционных систем (например, /usr/local/bin вместо стандартного для RPM /usr/bin).

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

    Золотое правило порядка действий

    Существует критически важное правило при работе в секции %prep: любые модификации файлов через shell-команды должны выполняться СТРОГО ПОСЛЕ применения патчей.

    Патч содержит не только сами изменения, но и контекст — несколько строк кода до и после изменяемого участка. Если вы сначала пройдетесь по файлу утилитой sed и измените пути, контекст файла изменится. Когда %autosetup попытается наложить патч, он не найдет ожидаемых строк, и сборка завершится с ошибкой Hunk #1 FAILED.

    Правильный порядок всегда такой:

  • Распаковка (%setup / %autosetup)
  • Патчинг (встроено в %autosetup)
  • Удаление лишних файлов (дебандлинг)
  • Текстовые замены (sed, awk)
  • Отладка: команда rpmbuild -bp

    При создании сложного spec-файла секция %prep редко получается идеальной с первого раза. Запускать полную сборку пакета (rpmbuild -ba) каждый раз, чтобы проверить, правильно ли отработал sed, крайне неэффективно — компиляция тяжелого проекта может занимать часы.

    Для решения этой проблемы существует команда rpmbuild -bp (build prep).

    Эта команда приказывает пакетному менеджеру прочитать spec-файл, выполнить только секцию %prep и остановиться.

    После выполнения этой команды вы можете открыть директорию ~/rpmbuild/BUILD/awesome_app-2.0 и своими глазами проверить:

  • Правильно ли распаковались исходники?
  • Удалились ли встроенные библиотеки?
  • Сработали ли замены sed?
  • Успешно ли наложились патчи?
  • Если что-то пошло не так, вы просто правите spec-файл и снова запускаете rpmbuild -bp. Директория BUILD будет автоматически очищена, и процесс подготовки начнется заново в чистой среде.

    Секция %prep — это фундамент успешной сборки. Тщательно очищенный от мусора, правильно распакованный и пропатченный код гарантирует, что следующий этап — компиляция в секции %build — пройдет предсказуемо и в строгом соответствии с правилами дистрибутива.

    7. Секция %prep: Наложение патчей и использование макроса %autosetup

    Секция %prep: Наложение патчей и использование макроса %autosetup

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

    Поскольку принцип неизменности исходного кода запрещает нам переупаковывать оригинальный архив, все изменения вносятся «на лету» с помощью патчей. В этой статье мы глубоко погрузимся в механику работы утилиты patch, разберем продвинутые возможности макроса %autosetup и научимся спасать сборку, когда патчи отказываются применяться.

    Механика наложения патчей: Контекст и Fuzz

    Чтобы понять, почему патчи ломаются, нужно понимать, как они устроены. Патч (обычно в формате Unified Diff) не просто говорит «замени строку 15 на строку 16». Он содержит Hunk (Блок изменений) — фрагмент кода, включающий удаляемые строки (с минусом), добавляемые строки (с плюсом) и, что самое важное, контекст.

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

    Что такое Fuzz (Смещение)?

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

    Когда вы попытаетесь наложить старый патч на новый код, утилита patch не найдет нужный контекст на строке 15. Но она просканирует файл дальше, найдет точное совпадение контекста на строке 45 и применит изменения там. Это называется Fuzz (Смещение) — способность утилиты накладывать патч с частичным несовпадением строк или контекста.

    Fuzz бывает разных уровней:

  • Fuzz 1: Совпадает почти весь контекст, кроме одной строки.
  • Fuzz 2: Совпадает меньше контекста, утилита делает «обоснованную догадку».
  • !Попробуйте изменить целевой код и посмотрите, как утилита patch реагирует на изменения: применяется чисто, со смещением (Fuzz) или выдает ошибку.

    Почему Rosa Linux запрещает Fuzz

    Казалось бы, Fuzz — это отличная функция, которая экономит время мейнтейнера. Однако в корпоративных дистрибутивах, таких как Rosa Linux, Red Hat или Fedora, политика безопасности строго диктует: патчи должны накладываться абсолютно чисто, без единого предупреждения о смещении.

    В конфигурации сборочных сред этих дистрибутивов жестко прописан системный макрос %_default_patch_fuzz 0.

    Зачем это сделано?

  • Предсказуемость: При Fuzz 2 утилита может «угадать» неправильно и вставить критический патч безопасности (например, проверку переполнения буфера) вообще в другую функцию. Код скомпилируется, но уязвимость останется.
  • Контроль деградации: Если патч применяется с Fuzz, это сигнал мейнтейнеру, что апстрим-код изменился. Мейнтейнер обязан глазами просмотреть изменения и убедиться, что логика патча все еще актуальна.
  • Если при сборке пакета в Rosa Linux патч попытается примениться со смещением, сборка немедленно завершится с ошибкой Patch applied with fuzz.

    Продвинутые возможности %autosetup

    Макрос %autosetup, как мы уже знаем, автоматически находит все теги Patch в преамбуле и применяет их. Но его возможности выходят далеко за рамки простого перебора файлов.

    Интеграция с системами контроля версий: флаг -S

    Самая мощная и часто используемая в сложных пакетах функция %autosetup — это флаг -S (System). Он позволяет инициализировать полноценную систему контроля версий прямо внутри временной директории сборки.

    Что происходит под капотом при использовании -S git:

  • Исходный архив распаковывается.
  • В корне распакованной директории выполняется git init.
  • Все оригинальные файлы добавляются в индекс и создается первый коммит (например, с сообщением "Initial import").
  • Макрос берет Patch0, применяет его и автоматически создает новый Git-коммит, используя имя патча в качестве сообщения коммита.
  • Процесс повторяется для Patch1, Patch2 и так далее.
  • !Схема трансформации: от набора разрозненных diff-файлов к линейной истории Git-коммитов в сборочной директории.

    Зачем превращать процесс сборки в Git-репозиторий? Это бесценно для отладки. Если сборка падает на этапе компиляции, вы можете зайти в директорию BUILD, выполнить git log и увидеть всю историю изменений. Вы можете использовать git bisect, чтобы найти, какой именно из 50 ваших патчей сломал сборку, или git diff, чтобы быстро посмотреть разницу между оригинальным кодом и текущим состоянием.

    Отключение автоматики: флаг -N

    Иногда автоматическое наложение всех патчей сразу ломает логику подготовки кода. Например, вам нужно применить Patch0, затем запустить shell-скрипт (или sed) для генерации некоторых файлов, и только потом применить Patch1 к этим сгенерированным файлам.

    Для этого используется флаг -N (No patches):

    Но как тогда применить патчи? Здесь на сцену выходит ручное управление.

    Ручное управление: макрос %patch

    Если вы использовали %autosetup -N (или работаете со старым spec-файлом, где используется базовый %setup), вам придется накладывать патчи вручную с помощью макроса %patch.

    В современных версиях RPM синтаксис вызова этого макроса изменился. Ранее использовалось слитное написание (например, %patch0 -p1). Сейчас стандартом является использование флага -P (Patch number):

    Условное применение патчей: %ifarch и %ifos

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

    Для этого используются условные макросы RPM, такие как %ifarch (If Architecture):

    Аналогично существует макрос %ifos для применения патчей в зависимости от операционной системы (хотя при сборке пакетов строго под Rosa Linux он используется редко).

    Анатомия ошибки: Hunk FAILED и файлы .rej

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

    Это означает, что один из блоков изменений (Hunk) не смог найти свой контекст даже с учетом Fuzz (или Fuzz был запрещен).

    Когда patch терпит неудачу, он не просто падает. Он оставляет на диске два критически важных файла для отладки:

  • Файл .orig (например, src/main.c.orig) — резервная копия оригинального файла до попытки изменения.
  • Файл .rej (Reject-файл, например, src/main.c.rej) — текстовый файл, содержащий только те Hunks, которые не удалось применить.
  • Ребейз патча (Patch Rebasing)

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

    Вот пошаговый алгоритм правильного ребейза:

  • Остановка на этапе %prep: Запустите сборку командой rpmbuild -bp. Она упадет, но оставит распакованные файлы в директории BUILD.
  • Поиск rejects: Перейдите в директорию BUILD/имя-версия и найдите файлы с расширением .rej.
  • Анализ: Откройте .rej файл. Посмотрите, какой именно кусок кода не применился.
  • Ручное вмешательство: Откройте целевой файл (например, src/main.c) в текстовом редакторе. Найдите место, куда должен был встать код из .rej файла. Скорее всего, разработчики переименовали переменную или сдвинули функцию.
  • Создание эталона: Скопируйте текущий src/main.c в src/main.c.orig.
  • Внесение правок: Вручную впишите необходимые изменения в src/main.c.
  • Генерация нового патча: Выйдите в корень проекта и используйте утилиту diff для создания нового, чистого патча:
  • Проверка: Снова запустите rpmbuild -bp. Если ошибок нет — ребейз прошел успешно.
  • Патчинг нестандартных файлов

    Утилита patch предназначена исключительно для текстовых файлов (исходный код, конфигурации, скрипты). Но что делать, если вам нужно заменить бинарный файл (например, скомпилированную библиотеку, картинку или базу данных SQLite), который поставляется в исходном архиве?

    Создать diff-файл для бинарника невозможно. В таких случаях концепция патчинга реализуется через прямую подмену файлов в секции %prep с использованием дополнительных исходников (Source).

    Допустим, апстрим положил в архив логотип logo.png, на который у нас нет прав, и нам нужно заменить его на свободный logo-free.png.

    Обратите внимание на использование макроса %{SOURCE1}. В секции %prep мы находимся внутри директории BUILD, а наш logo-free.png лежит в директории SOURCES. Макрос %{SOURCE1} автоматически разворачивается в полный абсолютный путь к файлу, избавляя нас от необходимости писать относительные пути вроде ../../SOURCES/logo-free.png, которые могут сломаться при изменении структуры сборочной среды.

    Порядок применения множества патчей

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

    Макрос %autosetup применяет патчи строго в порядке возрастания их номеров в преамбуле (Patch0, Patch1, Patch2...). Если Patch2 модифицирует ту же функцию, что и Patch0, критически важно, чтобы Patch0 был наложен первым. В противном случае Patch2 не найдет нужного контекста и выдаст ошибку Hunk FAILED.

    Лучшая практика мейнтейнеров Rosa Linux — группировать патчи логическими блоками, оставляя «зазоры» в нумерации:

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

    Секция %prep — это место, где мейнтейнер проявляет наибольшую инженерную смекалку. Умение виртуозно жонглировать макросами %autosetup, понимать природу Fuzz-ошибок и быстро делать ребейз патчей отличает новичка от профессионала, способного поддерживать сложные и объемные пакеты в актуальном состоянии.

    8. Секция %build: Конфигурация исходных кодов и макрос %configure

    Секция %build — это экватор жизненного цикла RPM-пакета. Если в секции %prep мы лишь подготавливали текстовые файлы, распаковывали архивы и накладывали патчи, то именно в %build происходит настоящая магия: исходный код превращается в исполняемые бинарные файлы, готовые к работе в операционной системе.

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

    Парадигма Autotools: Зачем нужен этап конфигурации

    Исторически в мире Unix-подобных систем сложился стандартный процесс сборки программного обеспечения, состоящий из трех шагов: конфигурация, компиляция и установка. Эта модель была популяризирована набором утилит Autotools (Autoconf, Automake, Libtool).

    Когда вы скачиваете исходный код программы, написанной на C или C++, вы обычно видите в корне директории скрипт с именем configure.

    Зачем он нужен? Исходный код пишется так, чтобы работать на множестве разных операционных систем (Linux, FreeBSD, macOS) и аппаратных архитектур. Однако на каждой из этих систем заголовочные файлы могут лежать в разных местах, версии компиляторов могут отличаться, а некоторые системные функции могут вообще отсутствовать.

    Скрипт configure — это диагност. При запуске он сканирует сборочную среду:

  • Ищет подходящий компилятор (например, gcc или clang).
  • Проверяет наличие необходимых библиотек-зависимостей.
  • Выясняет особенности целевой архитектуры (размер указателя, порядок байтов).
  • На основе собранных данных генерирует файл Makefile — пошаговую инструкцию для утилиты make, описывающую, как именно нужно компилировать код в данной конкретной системе.
  • Макрос %configure: Стандартизация путей

    В ручной сборке пользователь просто пишет ./configure. Но в RPM-пакетировании использование голого скрипта конфигурации является грубейшей ошибкой.

    Если запустить ./configure без аргументов, программа по умолчанию настроит себя на установку в директорию /usr/local. Это нарушает стандарты дистрибутивов, где системные пакеты должны устанавливаться в /usr.

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

    Давайте посмотрим, во что превращается короткий макрос %configure при сборке пакета под архитектуру x86_64:

    Как видите, макрос делает колоссальную работу. Он жестко привязывает пути к стандарту дистрибутива. Обратите внимание на строку --libdir=/usr/lib64. Если бы мы собирали этот же пакет под 32-битную архитектуру или ARM, макрос автоматически подставил бы --libdir=/usr/lib, обеспечивая корректную работу концепции Multilib без вмешательства мейнтейнера.

    Передача пользовательских аргументов

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

    Макрос %configure принимает любые дополнительные аргументы и передает их оригинальному скрипту. Они дописываются в конец сгенерированной команды:

    Использование \ в конце строки позволяет переносить длинные списки аргументов на новые строки, делая spec-файл читаемым.

    Системные флаги компиляции: CFLAGS и LDFLAGS

    Помимо путей, макрос %configure (а также другие сборочные макросы) автоматически экспортирует в среду переменные окружения CFLAGS, CXXFLAGS и LDFLAGS.

    Эти переменные содержат системный набор флагов (макрос %optflags), который диктует компилятору, как именно нужно транслировать код. В корпоративных дистрибутивах эти флаги стандартизированы для обеспечения максимальной производительности и безопасности всех пакетов в репозитории.

    Разберем ключевые флаги, которые Rosa Linux применяет по умолчанию:

    * -O2 — базовый уровень оптимизации. Компилятор тратит больше времени на анализ кода, чтобы сделать итоговый бинарник быстрее, но не применяет экстремальные оптимизации (как -O3), которые могут нарушить стабильность работы. -g — генерация отладочной информации. Компилятор сохраняет связь между машинным кодом и строками исходного кода. Это увеличивает размер файлов в директории BUILD, но позже RPM автоматически вырежет эту информацию в отдельный пакет -debuginfo, оставив основной бинарник легким. * -fstack-protector-strong — критически важный флаг безопасности. Он заставляет компилятор вставлять специальные проверки (канарейки) перед возвратом из функций. Если злоумышленник попытается использовать уязвимость переполнения буфера, программа немедленно завершится с ошибкой, предотвратив выполнение вредоносного кода. * -Wp,-D_FORTIFY_SOURCE=2 — еще один механизм безопасности, который заменяет потенциально опасные функции стандартной библиотеки C (например, strcpy или sprintf) на их безопасные аналоги, проверяющие границы памяти.

    LDFLAGS управляет поведением компоновщика (Linker) — программы, которая собирает разрозненные скомпилированные объектные файлы в единый исполняемый файл. Стандартный флаг -Wl,-z,relro делает часть памяти программы (таблицу глобальных смещений) доступной только для чтения после запуска, что блокирует целый класс хакерских атак.

    !Соберите свой бинарник: баланс между скоростью, размером и безопасностью

    Переопределение флагов

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

    Для безопасного добавления флагов в современные версии RPM встроен макрос модификации:

    Этап компиляции: Макрос %make_build

    После того как скрипт configure успешно отработал и сгенерировал Makefile, наступает этап физической трансляции кода. За это отвечает утилита make.

    В старых spec-файлах вы можете встретить прямой вызов команды make. Однако современным стандартом является использование макроса %make_build.

    Главная причина его существования — параллельная сборка.

    Компиляция крупного проекта (например, ядра Linux или браузера) может занимать часы. Если запустить просто make, утилита будет компилировать файлы строго по очереди, используя только одно ядро процессора, даже если на сборочном сервере их 64.

    Утилита make поддерживает флаг -j (jobs), который указывает, сколько параллельных процессов компиляции можно запустить одновременно. Макрос %make_build автоматически определяет количество доступных ядер в сборочной среде и подставляет нужное значение через системную переменную %_smp_mflags.

    !Этапы работы секции %build: от конфигурации до готовых бинарных файлов.

    Состояние гонки (Race Condition) в Makefile

    Параллельная сборка — это прекрасно, но она выявляет скрытые ошибки в архитектуре апстрим-кода.

    Makefile строится на графе зависимостей. Например, чтобы собрать файл app.exe, нужно сначала собрать lib.o и main.o. Если зависимости прописаны правильно, make -j16 начнет собирать lib.o и main.o одновременно на разных ядрах, дождется завершения обоих процессов, и только потом соберет app.exe.

    Но если разработчик забыл явно указать, что app.exe зависит от lib.o, возникает состояние гонки (Race condition). Одно ядро начинает собирать app.exe и пытается прилинковать lib.o, в то время как другое ядро еще не успело этот lib.o создать. Сборка падает с ошибкой No such file or directory.

    Коварство состояния гонки в том, что оно плавающее. На 4-ядерном ноутбуке разработчика пакет может собираться успешно (ядра успевают), а на мощном 64-ядерном сборочном кластере Rosa Linux — падать.

    Если ваша сборка падает с непонятными ошибками отсутствия файлов в процессе %make_build, первое правило отладки — отключить параллелизм. Замените %make_build на make -j1. Если после этого пакет собрался успешно — вы поймали Race condition. В идеале нужно написать патч, исправляющий Makefile, но в качестве быстрого решения допускается оставить make -j1 в spec-файле с соответствующим комментарием.

    Альтернативные системы сборки: CMake и Meson

    Autotools — это классика, но индустрия не стоит на месте. Скрипты configure работают медленно и сложны в поддержке. Современные проекты на C/C++ массово мигрируют на новые системы сборки, такие как CMake и Meson.

    RPM прекрасно адаптирован к этим системам. Для них существуют свои аналоги макросов конфигурации и сборки.

    CMake

    CMake не использует configure. Вместо этого он читает файл CMakeLists.txt и генерирует Makefile (или инструкции для более быстрой утилиты ninja).

    Важное отличие современных систем сборки — они требуют out-of-source build (сборки вне дерева исходников). Это значит, что компиляция происходит не в той же папке, где лежит код, а в отдельной директории (обычно build/), чтобы не засорять исходники объектными файлами.

    Макросы %cmake и %cmake_build берут всю эту рутину на себя:

    Meson

    Meson — еще более современная и быстрая система сборки, написанная на Python. Она использует утилиту ninja вместо make.

    Синтаксис в spec-файле абсолютно аналогичен:

    Независимо от того, какую систему сборки использует апстрим (Autotools, CMake или Meson), ваша задача как мейнтейнера в секции %build остается неизменной: использовать правильные RPM-макросы. Они гарантируют, что пакет будет скомпилирован с соблюдением стандартов файловой иерархии, получит все необходимые инъекции флагов безопасности и максимально утилизирует вычислительные мощности сборочного кластера.

    9. Секция %build: Процесс компиляции и использование %make_build

    Этап конфигурации позади: сборочная среда изучена, пути стандартизированы, а инструкции для компилятора сгенерированы. Наступает фаза физической трансляции исходного кода в машинные инструкции.

    В идеальном мире мейнтейнеру достаточно вызвать макрос %make_build, дождаться завершения работы компилятора и перейти к установке. В реальности секция %build — это место, где пакеты чаще всего ломаются. Специфичные флаги дистрибутива вступают в конфликт с логикой разработчиков апстрима, параллельные процессы блокируют друг друга, а в итоговые бинарные файлы просачиваются временные пути сборочного сервера.

    Проблема «Тихих» логов и Verbose Mode

    Разработчики программного обеспечения любят красивый и минималистичный вывод в консоли. Многие современные проекты по умолчанию используют Silent Rules (тихие правила) при компиляции. Если вы запустите сборку такого проекта, лог будет выглядеть примерно так:

    Для обычного пользователя это удобно. Для мейнтейнера RPM-пакета — это катастрофа.

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

    Политика качества корпоративных дистрибутивов (включая Rosa Linux) требует, чтобы логи сборки были максимально подробными (Verbose). Мейнтейнер должен видеть каждую команду, которую выполняет система.

    Чтобы отключить тихий режим, в секции %build необходимо передать специальные аргументы. Для проектов на базе классических Makefile это обычно переменная V=1:

    После этого лог преобразится, обнажив реальные процессы:

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

    Иерархия переменных в Make и их переопределение

    Часто возникает ситуация, когда скрипт конфигурации отработал корректно, но сам Makefile написан небрежно. Например, разработчик жестко прописал внутри файла:

    В этом случае системные переменные окружения, заботливо экспортированные макросом %configure, будут проигнорированы. Утилита make прочитает файл и перезапишет значения.

    Чтобы заставить make уважать наши правила, необходимо понимать иерархию приоритетов переменных в этой системе сборки (от низшего к высшему):

  • Переменные окружения (Environment variables).
  • Переменные, заданные внутри самого Makefile.
  • Переменные, переданные как аргументы командной строки при вызове make.
  • Чтобы победить жестко заданные значения в коде апстрима, мы должны использовать третий уровень приоритета. Макрос %make_build позволяет передавать любые аргументы напрямую утилите сборки:

    Этот прием называется Make Variable Overriding (переопределение переменных Make). Он спасает в ситуациях, когда писать патч для исправления Makefile слишком долго или нецелесообразно.

    Пределы параллельной сборки: Закон Амдала

    Современные сборочные кластеры обладают десятками процессорных ядер. Макрос %make_build автоматически запрашивает максимальное количество параллельных потоков. Однако новички часто удивляются: почему сборка на 64-ядерном сервере не происходит в 64 раза быстрее, чем на одноядерном?

    Ответ кроется в фундаментальном принципе информатики — Законе Амдала.

    Закон Амдала гласит, что максимальное ускорение программы при распараллеливании ограничено той частью кода, которая может выполняться только последовательно. Математически это выражается формулой:

    Где: * — теоретическое ускорение системы. * — доля задачи, которую можно распараллелить (от до ). * — количество процессорных ядер.

    !Подвигайте ползунки — и увидите, почему добавление ядер не всегда спасает долгую компиляцию

    В контексте компиляции C/C++ проекта:

  • Трансляция (Компиляция) — процесс превращения .c файлов в объектные .o файлы. Этот этап идеально распараллеливается (). Каждое ядро берет свой файл и компилирует его независимо от других.
  • Компоновка (Линковка) — процесс объединения тысяч .o файлов в один массивный исполняемый файл или библиотеку. Исторически этот процесс строго последовательный ().
  • Если проект состоит из 10 000 файлов, 64 ядра мгновенно скомпилируют их в объектные файлы. Но затем все 63 ядра остановятся и будут ждать, пока одно оставшееся ядро мучительно долго (иногда десятки минут) собирает их вместе.

    Понимание этого закона критически важно при чтении логов. Если вы видите, что процесс сборки «завис» на одной команде, потребляя 100% одного ядра, а остальные простаивают — это не ошибка. Это этап компоновки гигантского бинарного файла (например, браузерного движка).

    Ninja: Архитектура высокоскоростных сборок

    Классическая утилита make была создана в 1976 году. При всех ее достоинствах, она имеет архитектурный изъян: перед началом работы make должен прочитать все Makefile в проекте, построить в памяти граф зависимостей и проверить время модификации каждого файла на диске. Для огромных проектов (вроде ядра Linux или LLVM) только этап вычисления того, что именно нужно собрать, может занимать несколько минут.

    На смену пришла Ninja — минималистичная и невероятно быстрая система сборки.

    Ninja не предназначена для написания конфигурационных файлов вручную. Ее файлы (build.ninja) генерируются автоматически более высокоуровневыми системами, такими как CMake или Meson.

    Главные отличия Ninja от Make: * Отсутствие логики: Ninja не умеет работать с условиями (if/else) или сложными переменными. Граф зависимостей уже вычислен и записан в файл в максимально простом формате. * Умный параллелизм: Ninja по умолчанию запускает параллельную сборку, опираясь не только на количество ядер, но и на текущую загрузку системы (Load Average). Это предотвращает зависание сборочного сервера при нехватке оперативной памяти. * Мгновенная инкрементальная сборка: Ninja использует единый бинарный лог .ninja_log для отслеживания состояния файлов, что позволяет запускать пересборку за миллисекунды.

    В spec-файле для запуска этой системы используется макрос %ninja_build:

    Обратите внимание на флаг -v (verbose). Как и в случае с Make, он принудительно заставляет Ninja выводить полные команды компилятора в лог.

    Сборка вне дерева (Out-of-Source Build)

    Исторически проекты собирались прямо в директории с исходным кодом. Объектные файлы .o появлялись рядом с исходниками .c. Это загрязняло рабочую директорию и делало невозможным одновременную сборку проекта под разные архитектуры из одних и тех же исходников.

    Современный стандарт индустрии — Out-of-Source Build (Сборка вне дерева).

    При таком подходе создается отдельная пустая директория (обычно build/ или redhat-linux-build/). Сборочная система читает исходный код из оригинальной директории, но все результаты компиляции записывает исключительно в новую папку. Оригинальное дерево исходников остается девственно чистым (read-only).

    !Схема структуры директорий при Out-of-source сборке — видно, как исходники остаются чистыми, а артефакты изолируются.

    Макросы %cmake и %meson автоматически реализуют этот подход. Они создают директорию, переходят в нее, выполняют конфигурацию, а макросы %cmake_build и %meson_build знают, где искать результаты.

    Однако мейнтейнеру часто нужно выполнить дополнительные shell-команды после компиляции (например, сгенерировать документацию). Если вы просто напишете команду в секции %build, она выполнится в корне исходников, а не в папке сборки, что приведет к ошибке «файл не найден».

    Для навигации используется системный макрос %_vpath_builddir, который хранит имя сгенерированной папки:

    Утечка сборочных путей: RPATH и RUNPATH

    Одна из самых коварных проблем этапа %build — это загрязнение бинарных файлов путями сборочного сервера.

    Когда программа компилируется и компонуется с динамическими библиотеками, она должна знать, где искать эти библиотеки при запуске. В Linux для этого используется системный путь (обычно /usr/lib64).

    Но во время сборки пакета библиотеки еще не установлены в систему! Они лежат во временной директории внутри BUILD (например, /home/builder/rpmbuild/BUILD/app-1.0/lib). Чтобы свежесобранный бинарный файл мог запуститься прямо из сборочной директории (например, для прохождения тестов), компоновщик временно вшивает этот локальный путь внутрь бинарного файла.

    Этот вшитый путь называется RPATH (Run-time search path) или его современной версией RUNPATH.

    Если этот путь останется в итоговом RPM-пакете, произойдет катастрофа:

  • Уязвимость безопасности: Программа на компьютере пользователя будет пытаться загрузить библиотеки из несуществующей директории /home/builder/.... Злоумышленник может создать такую директорию на сервере и подкинуть туда вредоносную библиотеку.
  • Поломка переносимости: Пакет будет жестко привязан к структуре папок конкретного сборочного сервера.
  • Утилита rpmlint (которую мы детально разберем в будущих статьях) немедленно забракует такой пакет с ошибкой binary-or-shlib-defines-rpath.

    Как бороться с RPATH на этапе сборки

    Разные системы сборки решают эту проблему по-разному:

  • Libtool (Autotools): Использует скрипты-обертки. В директории BUILD создается не настоящий бинарник, а bash-скрипт, который подменяет пути и запускает скрытый бинарник. Настоящая компоновка (без RPATH) происходит позже, на этапе установки.
  • CMake: По умолчанию вшивает RPATH во время компиляции (чтобы работали тесты), но имеет встроенную функцию автоматического удаления RPATH на этапе установки.
  • Если апстрим сломал стандартное поведение CMake, мейнтейнер может принудительно отключить генерацию RPATH прямо в секции %build:

    Если система сборки нестандартная и RPATH все равно просачивается, мейнтейнеры используют утилиту chrpath, которая физически вырезает эти пути из готовых бинарных файлов. Это делается в самом конце секции %build или в %install.

    Чтение логов: Как найти иголку в стоге сена

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

    Новички часто копируют эти строки и пытаются искать их в интернете. Это бессмысленно. Ошибка Error 2 означает лишь то, что утилита make завершила работу из-за ошибки в одном из дочерних процессов.

    Из-за параллельной работы ядер реальная ошибка (например, синтаксическая ошибка в коде C++) может находиться на сотни или тысячи строк выше в логе! Пока одно ядро упало с ошибкой, другие ядра продолжали успешно компилировать свои файлы и писать в лог, скрывая причину сбоя.

    Алгоритм поиска ошибки в логе параллельной сборки:

  • Игнорируйте последние 20-30 строк с сообщениями Error 2.
  • Ищите ключевые слова снизу вверх: error:, undefined reference, No such file or directory.
  • Если лог гигантский, пересоберите пакет в один поток (%make_build -j1). Это отключит параллелизм, и реальная ошибка окажется ровно перед сообщением об остановке сборки.
  • Отладка без боли: rpmbuild -bc

    Процесс отладки секции %build может быть утомительным. Если пакет собирается 10 минут и падает на 9-й минуте, вы не хотите каждый раз заново распаковывать архивы и применять патчи.

    Для этого существует команда частичной сборки:

    Флаг -bc (Build Compile) приказывает пакетному менеджеру выполнить секции %prep и %build, а затем остановиться. Если исходники уже были распакованы ранее, RPM не будет делать это заново. Он просто зайдет в директорию BUILD и запустит компиляцию. В сочетании с инкрементальной сборкой Ninja это позволяет тестировать изменения в Makefile или флагах за считанные секунды.

    Секция %build завершается, когда в сборочной директории лежат готовые, скомпилированные бинарные файлы, библиотеки и сгенерированная документация. Они еще не находятся на своих местах в файловой системе — они просто лежат в песочнице. Распределение этих файлов по правильным системным директориям (/usr/bin, /etc) — это задача следующего этапа, секции %install.