RPM/Macros

Материал из ALT Linux Wiki
< RPM

Эта статья, в некоторой степени, является переводом соответствующего руководства, но расширенного и адаптированного под реалии 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.

Имеет ли это практический смысл? Сомнительно.

Стоит ли понимать это? Скорее - да, чем нет.

Ссылки