Привет, Хабр!

Сегодня мы рассмотрим, как написать один RPM .spec-файл так, чтобы он одинаково успешно собирался на Fedora, RHEL и даже на openSUSE.

Но для начала: зачем вообще поддерживать несколько дистрибутивов одной спецификацией?

Есть несколько кейсов:

  • CI/CD на всех платформах сразу.
    Вы ведёте опенсорс. У вас .gitlab-ci.yml и в нём matrix build: Fedora 39, EPEL9, openSUSE Leap 15.6. Если .spec один, можно тривиально написать rpmbuild -ba на каждой ноде и забыть.

  • Патчи upstream прилетают разом.
    Если у вас три разные .spec: на Fedora нужна версия 3.x библиотеки, на SUSE — 1.1, а на RHEL ещё и со старым автотулзом. И в каждой специи — свои костыли. В единой специи ты хотя бы видишь, чем они отличаются, и можешь аккуратно поднять if или bcond.

  • Снижение числа ошибок.
    Три спеки — это три точки для багов. Особенно когда люди начинают забывать их синхронизировать. Кто-то забыл обновить релиз на SUSE — и всё, пользователи пишут тикеты.

Так что мысль простая: один файл — один источник истины.

Conditional-макросы: %if 0%{?fedora}, %if 0%{?rhel}, %bcond_with tests

Вот тут большинство задумываютися. Ты либо напишешь свои if-ы топорно — и через полгода сам их не прочитаешь, либо аккуратно заведёшь глобалы и обернёшь в bcond.

0%{?…} нужен именно для того, чтобы %if не свалился в синтаксическую ошибку, если макроса нет. SUSE не определяет ни %fedora, ни %rhel, а только %suse_version. Поэтому всегда начинаем спецификацию примерно так:

%global distro fedora
%if 0%{?rhel}
%global distro rhel
%endif
%if 0%{?suse_version}
%global distro suse
%endif

Почему не %distname? В Fedora %distname в будущем может конфликтовать, а %distro — наш, локальный.

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

# У Fedora >=39 нужен OpenSSL3
%if 0%{?fedora} >= 39
BuildRequires: openssl-devel >= 3
%endif

# SUSE уже давно на OpenSSL3
%if 0%{?suse_version} >= 1550
BuildRequires: libopenssl-devel >= 3
%endif

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

%bcond_with tests

%prep
%autosetup

%build
%configure
make %{?_smp_mflags}

%check
%if %{with tests}
make check
%endif

На проде обычно tests выключают, а в CI включают.

Раздел Source/BuildRequires: как выбирать разные URL и патчи

На этом этапе уже важно помнить, что Fedora любит bleeding-edge, SUSE — более консервативна, а RHEL — это всегда «не спешим».

Например, в Fedora пакет собирается на GCC 13, а на RHEL всё ещё живёт GCC 8. Значит — не стесняемся брать патчи из апстрима под более старые версии и заворачивать их условно:

%if 0%{?rhel} == 8
Patch0: rhel8-gcc8-fix.patch
%endif

%if 0%{?suse_version} <= 1500
Patch1: suse15.5-backport.patch
%endif

С URL аналогично — иногда приходится брать другие зеркала или форки для SUSE:

%if "%{distro}" == "suse"
Source0: https://download.opensuse.org/repositories/home:/myproject/my-awesome-tool-%{version}.tar.gz
%else
Source0: https://github.com/myorg/my-awesome-tool/archive/v%{version}.tar.gz
%endif

На уровне BuildRequires не боимся явно указывать разные пакеты — Fedora и SUSE часто расходятся по неймингу:

%if "%{distro}" == "fedora"
BuildRequires: pkgconfig(libfoo)
%endif
%if "%{distro}" == "suse"
BuildRequires: libfoo-devel
%endif

На Fedora принято писать pkgconfig(...), на SUSE — старомодно -devel.

Секция %package и %files: split-subpackages и различия в /usr/lib64 vs /usr/lib

Это частый косяк: «у меня билд прошёл, а файлы не поставились». Потому что в Fedora всё строго /usr/lib64/, а в SUSE часто /usr/lib/.

Проверяем это заранее:

%files
%if "%{distro}" == "suse"
/usr/lib/my-awesome-tool
%else
/usr/lib64/my-awesome-tool
%endif

Или ещё лучше — определем глобальный макрос libdir и используем везде его:

%global libdir %{_libdir}

%files
%{libdir}/my-awesome-tool

Так читать проще, менять тоже.

Для split-пакетов, вроде -devel, -doc, -tests, заводим отдельные секции %package и %files, как положено:

%package doc
Summary: Documentation for %{name}
BuildArch: noarch

%files doc
%doc README.md docs/

Так пакеты выглядят аккуратнее и позволяют пользователям не тащить лишнее.

Обработка системных юнитов, tmpfiles и скриплетов: systemd vs legacy init

Пакет, который ставит сервис — почти всегда таит в себе проблему: systemd у всех свой, пути разные, да и tmpfiles.d, sysusers.d и прочие штуки далеко не одинаково реализованы. Особенно в openSUSE, где любят systemd-rpm-macros, но всё равно что-нибудь отличается от Fedora.

Описываем установку системного юнита:

Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd

%post
%systemd_post myservice.service

%preun
%systemd_preun myservice.service

%postun
%systemd_postun_with_restart myservice.service

Но в openSUSE свои макросы: %service_* вместо %systemd_*, и если бездумно вставлять Fedora-шные макросы — на SUSE всё взорвётся с ошибкой Unknown %systemd_post.

Правильный кросс-подход:

%if "%{distro}" == "suse"
%define systemd_post() /usr/lib/rpm/postun-systemd %{*}
%define systemd_preun() /usr/lib/rpm/preun-systemd %{*}
%define systemd_postun_with_restart() /usr/lib/rpm/postun-systemd %{*}
%endif

Или ещё лучше — использовать %{?systemd_post:...} как условную конструкцию. Для tmpfiles:

%post
%tmpfiles_create %{_tmpfilesdir}/myservice.conf

Не забываем:

Requires: systemd
Requires(post): systemd
Requires: systemd-tmpfiles

Всё это — чтобы сервис заработал, каталоги создались, а юнит был корректно включён и переинициализирован.

Ещё нюанс: SUSE кладёт юниты в /usr/lib/systemd/system/, Fedora — туда же, но RHEL 7 любит /lib/systemd/system/. На это тоже можно писать if или использовать %{_unitdir}.

Заключение

В итоге, единый .spec — это меньше дублирования, проще поддержка, чище CI и предсказуемое поведение пакета на Fedora, RHEL и SUSE. Используйте макросы, bcond_with, аккуратные if, заведите libdir и тестируйте всё в COPR и OBS.

Приглашаем вас ознакомиться со специализацией «Administrator Linux», в рамках которой рассматриваются современные методы и инструменты администрирования Linux‑систем.

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

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

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

Комментарии (2)


  1. ajijiadduh
    21.07.2025 22:04

    но RHEL 7 любит

    может пора 7 отпустить?


  1. 13werwolf13
    21.07.2025 22:04

    вам бы освоить openbuildservice. некоторые разработчики уже освоили и собирают в одном месте пакеты и для центоси и для суси и для debian и даже всякое неоднозначное вроде appimage и flatpack. пример.

    да и по тексту есть неточности:
    1) pkgconfig() прекрастно применяется в спеках для suse
    2) использовать билдреки из других репозиториев это задача obs а не rpmbuild
    3) /usr/lib/ vs /usr/lib64 - так не надо, есть макрос которому виднее что там в конкретном дистрибутиве для libexec

    вообще по тексту впечатление что знания о сборке пакетов для suse/opsensuse у вас несколько устаревшие.

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