RPM/Macros
Эта статья, в некоторой степени, является переводом соответствующего руководства, но расширенного и адаптированного под реалии ALT и rpm-build-4.0.4.206-alt1 (Следите за актуальностью!)
Введение
Макросы - это именованные конструкции, которые позволяют вставлять заранее определённые шаблоны текста (или команд) в нужные места spec файла. Применительно к RPM можно интуитивно воспринимать их как своего рода переменные с расширенной логикой.
Раскрытие макросов происходит при обработке spec файла, результатом этой обработки являются Shell скрипты (отдельные для каждого этапа сборки, находятся по пути $TMPDIR/rpm-tmp.xxxxx).
Порядок обработки следующий:
- На вход
rpmbuildполучает исходный .spec файл, в котором есть нераскрытые макросы. - Макросы раскрываются и получается .spec без макросов, только с “чистым” текстом.
- Из итогового файла генерируются скрипты сборки.
То есть, в макросах может содержаться:
- Код на Shell
- Метаинформация для spec файла
Зачем это нужно?
Макросы могут быть использованы для централизации управления процессом сборки сразу множества пакетов. Например, если у нас есть пакет, использующий cmake, то вместо указания вручную типовых параметров (флаги оптимизации, пути к системным директориям) можно создать макросы %cmake и %cmake_build и свести секцию %build к следующему виду:
%build %cmake %cmake_build
Подобная централизации полезна и при изменении процесса сборки. Если, например, потребуется добавить настройку конфигурации для cmake, то это нужно будет сделать всего в одном месте - в объявлении макроса %cmake. Тогда все пакеты, использующие эти макросы, обновят конфигурацию при пересборке. Пример подобной ситуации можно почитать в этом треде.
Как отлаживать?
Для того, чтобы посмотреть, во что раскрывается макрос, можно использовать
rpmbuild --eval "МАКРОС"
результатом будет уже конечный, целиком раскрытый текст.
ВАЖНО: именно rpmbuild, а не rpm. В ALT они расходятся по функционалу.
Для лучшего отслеживания происходящих подстановок можно использовать
rpmbuild --eval "%trace ... ... "
Определения и свойства
Обычные макросы (Simple macros) - наиболее базовый тип макросов в RPM. Они работают путём прямой текстовой подстановки — без параметров, логики или обработки аргументов.
Макросы, используемые в RPM спек-файлах, являются полностью рекурсивными. Это означает, что если в теле одного макроса используются другие макросы, то они тоже будут раскрыты. И так до самого глубокого уровня вложенности.
Макросы в RPM spec-файле можно использовать практически в любом месте, в том числе внутри файлов, которые передаются как параметр -f в %files -f <file>. (TODO: Что значит “практически в любом?”. А где нельзя?) (UPD: Точно нельзя раскрывать макросы в названиях других макросов, см. соответствующий пример)
Макросы могут быть вложенными, то есть один макрос может содержать внутри себя другой макрос. При этом, если вложенный макрос имеет то же имя, что и внешний (ранее определённый), то внутреннее определение временно “скрывает” или переопределяет внешнее во время раскрытия макроса, который содержит это вложение.
Синтаксис
Макросы определяются следующим образом:
Внутри spec файла:
%define <name>[(opts)] <body>
Внутри файлов с предопределёнными макросами (расположенными в /usr/lib/rpm/macros.d):
%<name>[(opts)] <body>
ВАЖНО: не должно быть пробела между именем и строкой с опциями. %test(a) и %test (a) - разные сущности. Первый - параметрический макрос, второй - обычный.
Имя макроса должно быть длинной минимум в 3 символа и может состоять из латинских букв, цифр и знака _.
Макрос (пере-)раскрывается на каждом вызове.
Все пробельные символы вокруг тела макроса удаляются.
Использование макросов
Для того, чтобы раскрыть макрос, используется следующая конструкция:
%<name>
Если нужно закомментировать используемый макрос, то нужно использовать два знака процента:
%%<name>
Многострочные макросы
Само тело представляет из себя ОДНУ Си-строку, считывание которой заканчивается переносом строки.
Если макрос хочется/нужно написать в несколько строк, то есть несколько способов это сделать.
Классические
Экранировать переносы строк при помощи \.
Стиль 1
%cmake_insource \
%define _cmake__builddir . \
%cmake
ВАЖНО: после \ не должно быть trailing whistespace, иначе потеряется весь смысл производимых действий.
Так же стоит отметить, что тело макроса кончается на первом неэкранированном переносе строки. Это значит, что следующий макрос
%mymacro \
if [ "$PWD" == "$TMPDIR" ] \
echo ...
fi
не включается в себя fi!
Стиль 2
Примерно то же самое, но для явного обозначения конца тела используется специальный макрос %nil. Он не несёт в себе синтаксического смысла и раскрывается просто в пустую строку.
Но, его можно использовать для визуального и явного обозначения конца макроса. Пример:
%journal_catalog_update() \
journalctl --update-catalog || : \
%{nil}
Здесь journalctl --update-catalog || : - основное тело макроса, а %{nil} в конце не выполняет никакой функции в плане логики или синтаксиса, но помогает визуально обозначить то, что тело макроса завершено.
Экранирование экранирования
Иногда макросы ради читаемости удобно писать в несколько строк. Но, он должен раскрыться в одну команду, а не в последовательность отдельных команд. Пример:
%zig_install \\\
DESTDIR="%buildroot" \\\
%zig_build \\\
install \\\
%{?_zig_install_options}
Здесь:
- Первый слэш экранирует второй.
- Второй - означает для Shell, что строка продолжается.
- Третий — означает для RPM, что тело макроса продолжается.
При раскрытии получится следующее:
DESTDIR="%buildroot" \
\
/usr/bin/zig \
build \
Поскольку результат макроса - это код для Shell, то эти строки будут проинтерпретированны как одна команда.
DESTDIR="%buildroot" /usr/bin/zig build
Через операции со строками
Начиная с версии 4.0.4.205-alt1 (январь 2025) в rpmbuild появилась поддержка многострочных макросов через операции со строками.
Все примеры будут из rpm-build-haskell.
На момент создания статьи это единственный (вроде бы) пакет макросов, который использует такой подход. А ещё это пакет автора статьи :D
%expand
Такая конструкция
%cabal %{expand:
if [ ! -x Setup ]; then
%__ghc_init_setup
fi
./Setup \\
}
раскрывается в содержимое, находящееся внутри фигурных скобок. Концом макроса выступает закрывающаяся фигурная скобка.
Что стоит отметить?
- В конце применяется
\\для экранирования переноса строки. Но обратных слеша - два, а не три. %expandне просто раскрывается в своё тело, а ещё перераскрывает его. См. соответствующий пример
%shrink
%shrink удаляет trailing и leading whistespaces, заменяет переносы строк на пробелы и сжимает несколько промежуточных пробелов на один.
И эту операцию тоже можно использовать как многострочный макрос!
Пример:
%ghc_gen_filelist %{shrink:
%__ghc_gen_filelist %buildroot %_libdir %_ghclibdir
%_builddir/%{?buildsubdir}
%_docdir %_datadir
}
Раскроется (содержимое внутренних макросов сути не меняет) в
%__ghc_gen_filelist %buildroot %_libdir %_ghclibdir %_builddir/%{?buildsubdir} %_docdir %_datadir
Такой подход удобен например для передачи параметров командной строки. По использованию (практически всегда) он является аналогом экранирования экранирования из “классических” многострочных макросов.
Что стоит отметить?
- Операция применяется после всех раскрытий. Если во внутренних макросах есть пробелы и/или переносы строк - они тоже будут урезаны.
Комбинирование операций
Вот так:
%cabal_configure %{expand:
%cabal %{shrink: configure
--prefix=%_prefix
--libdir=%_libdir
--disable-library-stripping
--enable-debug-info=3
}
}
тоже можно!
Параметризованные макросы
Параметризованные макросы (Parametric macros) - это макросы с аргументами, которые могут принимать параметры при вызове, подобно функциям в обычных языках программирования.
Строка внутри круглых скобок (opts) в неизменном виде передаётся в функцию getopt(3) — стандартный C-интерфейс для разбора командной строки (argc/argv).
Поддерживаются только короткие опции. Длинные флаги вида --flag не поддерживаются.
Символы -- можно использовать для отделения флагов от позиционных аргументов. Всё, что идёт после --, воспринимается как обычные аргументы, а не как флаги.
Синтаксис getopt
%parametric(abc:d) ...
Каждый символ представляет короткий флаг (например, -a, -b, -c, -d), а двоеточие : указывает, что флаг принимает аргумент.
Автоматические макросы
Для доступа к аргументам и параметрам определены Автоматические Макросы (Automatic Macros), аналогичные переменным в Shell скриптах.
| Макрос | Описание |
|---|---|
%0
|
Имя вызываемого макроса |
%*
|
Все аргументы (не включая обработанные параметры) |
%**
|
Все аргументы (включая обработанные параметры) |
%#
|
Количество аргументов |
%{-f}
|
Если при вызове указан флаг -f, подставляется последнее его вхождение (и флаг и аргумент)
|
%{-f*}
|
Аргумент, переданный с последним вхождением флага -f (если задан)
|
%1, %2…
|
Соответствующие позиционные аргументы после обработки |
Здесь используется ИМЕННО символ %, а не $. Это макросы, а не Shell переменные.
Обращаться к ним нельзя вне тела параметризованного макроса.
Обращение к параметрам
Простейшая конструкция - %{-f}, которая раскрывается в -f, если соответствующий флаг был указан при вызове макроса.
Условное раскрытие: %{-f:X} раскрывается в X, если макросу БЫЛ передан параметр -f.
Обратная форма %{!-f:Y} раскрывается в Y, если параметр -f НЕ был передан.
Примеры
Простейший
%debugprint() \
echo "Macro: %0"\
echo "Args: %*"\
echo "Arg count: %#"\
echo "First arg: %1"\
%{nil}
При вызове:
%debugprint hello world
раскрывается в
echo "Macro: debugprint" echo "Args: hello world" echo "Arg count: 2" echo "First arg: hello"
Условная подстановка
%buildopts(v:) -O3 %{-v:--verbose}
При вызове
%buildopts
раскрывается в
-O3
А при указании флага
%buildopts -v
раскрывается в
-O3 --verbose
При этом, если использовать --, то параметры, переданные после разделителя, не будут переданы getopt(3).
То есть,
buildopts -- -v
раскроется в
-O3
Глобальные макросы
Глобальные макросы (Global macros) могут быть определены внутри других макросов и быть доступными вне их.
Синтаксис:
%global <name>[(opts)] <body>
Свойства
Раскрытие тела макроса происходит сразу в момент определения. Значение вычисляется один раз и сохраняется. Это можно назвать энергичной моделью вычислений, тогда как обычные макросы - ленивые.
Такое (энергичное) поведение позволяет ссылаться на локально-определённые макросы, которые позже, во время использования глобального макроса, будут вне зоны видимости.
Также это свойство может быть полезно при раскрытии “тяжёлых” макросов, например с внешними вызовами и использованием конструкции %(). Т.к. все необходимые вычисления произойдут однократно.
Примеры
Простейший
Рассмотрим следующий макрос:
%test \
%global global_macro HERE! \
%{nil}
Тогда, следующие вызовы
%test %global_macro
раскроются в:
Here!
Но, при этом, БЕЗ предварительного вызова %test макрос %global_macro будет недоступен. То есть,
%global_macro
не раскроется.
Практический
При помощи глобальных макросов можно реализовать односвязные списки!
Пример основан на макросах для сборки GHC - rpm-build-haskell-extra
Допустим (зачем-то) мы хотим сделать макрос для работы с подпакетами, а после - изнутри спека автоматически получить список всех подпакетов, с которыми мы провзаимодействовали.
Тогда мы можем написать нечто подобное:
# n - name
# v - version
%build_subpackage(n:v:) \
%define pkgid %{-n*}-%{-v*}\
\
%global subpackages_list %pkgid %{?subpackages_list}\
\
echo 'Building %pkgid...'\
%{nil}
Что здесь происходит?
- Объявляется параметрический макрос
%build_subpackageс двумя параметрами --nи-v - Внутри него объявляется макрос
%pkgid. Посколько он объявлен через%define, то это локальный макрос и его область видимости ограничивается телом%build_subpackage - После чего происходит определение глобального макроса
%subpackages_listследующим образом:- В его начало записывается значение
pkgid. - Через пробел подставляется текущее глобальное значение
%subpackages_list, если оно доступно. - Результат (
pkgidв начале и старое значение%subpackages_listв конце) становится новым значением%subpackages_list.
- В его начало записывается значение
- Дальше выполняется непосредственная логика макроса.
Следующий текст
echo 'Current list: %subpackages_list' %build_subpackage -n text -v 1.3 echo 'Current list: %subpackages_list' %build_subpackage -n check -v 0.1 echo 'Current list: %subpackages_list' %build_subpackage -n test -v 3000.1 echo 'Processed: %subpackages_list'
раскроется в (табуляции и незначащие пустые строки опущены)
echo 'Current list: %subpackages_list' echo 'Building text-1.3...' echo 'Current list: text-1.3 ' echo 'Building check-0.1...' echo 'Current list: check-0.1 text-1.3 ' echo 'Building test-3000.1...' echo 'Processed: test-3000.1 check-0.1 text-1.3 '
Здесь можно увидеть, что - в начале макрос %subpackages_list не объявлен, поэтому он ни во что не раскрылся - после каждого вызова %build_subpackage список %subpackages_list пополняется всё новыми и новыми pkgid
Почему не %define?
Обычные макросы здесь нельзя использовать сразу по ряду причин:
- Самая простая - нам интересен доступ к
%subpackages_listвне тела макроса - Также мы ссылаемся на внутрениие сущности (макрос
%pkgidсуществует только внутри%build_subpackage) - В том случае, если бы
%build_subpackageбыл определён ранее как обычный макрос - то вместо формирования списка, происходило бы бесконечное повторное самораскрытие.- Т.е.
%define list 1 %list->1 1 %list->1 1 1 %list->1 1 1 1 %list-> …. - Это выглядит тоже очень интересно, но явно не то, что нам требуется.
- Т.е.
На что ещё стоит обратить внимание?
- В данном макросе у нас нет гарантий того, что
-nили-vобязательно будут переданы. Вполне возможен вызов%build_subpackageбез каких-либо аргументов. - При раскрытии простых макросов как параметра для shell-команды вовсе не обязательно использовать двойные кавычки -
"", вполне можно обойтись одинарными -' '. Двойные кавычки следует использовать в случае целенаправленного желания подставить переменные для раскрытия. - Скорее всего вам такие извращения будут не нужны.
Макросы с внешними вызовами
В такой конструкции:
%(...)
при раскрытии содержимое скобок передаётся для исполнения /bin/sh. Результат выполнения подставляется вместо макроса.
Пример:
%define test %( date +%%y%%m%%d whoami | rev )
раскроется в
YYMMDD resu
Последний перенос строки опускается.
Что стоит отметить:
- Можно вызывать как одну команду, так и полноценный сценарий
%нужно экранировать (как в примере)
ВАЖНО: Стоит избегать внешние вызовы изо всех сил. Их использование ведёт к неочевидному поведению и невероятно усложняет задачу анализа/парсинга спеков.
Условия в макросах
Использование %if
Синтаксис:
%if <cond1> ... %elif <cond2> ... %else ... %endif
Части %elif и %else - опциональны.
В условии можно:
- Проверять, существует ли определённый макрос, например
%if %{defined with_foo} && %{undefined with_bar}
- Сравнивать строки
%if "%{optimize_flags}" != "none"
- Вычислять математические выражения
%if %__glibc_version_minor < 40 || %_python3_abi_version > 3.11
В математических выражениях можно использовать логические операторы &&, ||, !, операторы сравнения !=, ==, <, >, <=, >=, арифметические операторы +, -, /, *, тернарный оператор ? :, а также круглые скобки.
Условные блоки завершаются директивой %endif. Директивы %endif и %else не должны сопровождаться каким-либо текстом. Условные конструкции можно вкладывать друг в друга.
Условное раскрытие макросов
Периодически может быть полезно выполнить какие-то действия в зависимости от того, существует макрос или нет. Следующие конструкции
%{?macro_name:value}
%{!?macro_name:value}
могут быть использованы для этих целей.
%{?macro_name:value} раскрывается в value, если макрос с именем macro_name существует.
%{?macro_name:value} действует зеркально, раскрывается в value, если макрос с именем macro_name НЕ существует.
В противном случае, раскрытие происходит в пустую строку.
%{?macro_name} выступает как синоним %{?macro_name:%macro_name}
Пример
Конструкция, раскрывающаяся в 1, если существует _with_python3 и в 0, если - нет,
%{?with_python3:1}%{!?with_python3:0}
или короче
0%{?with_python3:1}
См. как работает if_with
Интересные примеры, вне категорий
Раскрытие макросов в названии других макросов
Так не работает.
%define test TEST
%define hello_%test HELLO_TEST
%{hello_}
Раскрывается в
TEST HELLO_TEST
Т.е. второй define (после раскрытия) интерпретицируется следующим образом:
%define hello_ TEST HELLO_TEST
Вывод? Мы не можем ни создавать динамические имена макросов, ни динамически изменяемые параметры макроса.
Побочные эффекты expand
Оператор %{expand: …} выполняет первичное раскрытие макросов в теле, а после - повторно раскрывает уже полученный результат, как если бы это был новый текст с макросами.
То есть он буквально “раскрывает то, что раскрылось”.
К чему это может привести?
Пример:
%define aaabbb TEST
%define tst1 %aa
%define tst2 abbb
%define val_noe %tst1%tst2
%define val_e %{expand: %tst1%tst2}
- tst1 раскрывается в %aa (такого макроса нет)
- tst2 раскрывается в abbb
В результате:
%val_noe -> %aaabbb %val_e -> TEST
Что здесь происходит?
В первом случае (val_noe) происходит отдельное раскрытие макросов tst1 и tst2, первый - раскрывается в %aa, но макроса с таким названием нет (и быть не может). Поэтому раскрытие останавливается. Второй раскрывается в текст abbb. Это просто текст, раскрытие не продолжается. Результатом является %aaabbb, это валидный макрос, но, поскольку раскрытие идёт в один проход, то он уже не раскрывается.
А во втором случае (из-за действия expand) - происходит рекурсивное перераскрытие, и результатом становится TEST.
Имеет ли это практический смысл? Сомнительно.
Стоит ли понимать это? Скорее - да, чем нет.
Ссылки
- Оригинальная статья
- Документация по RPM Spec (отсюда взята часть про условные макросы)
- Дополнительно про условия
- Spec/Предопределенные_макросы