Packaging Automation/Embedded Language
Утилиты для манипуляции spec-файлами и src.rpm пакетами. Введение во встроенный язык редактирования.
Введение
При работе со специализированной предметной областью хорошей стратегией является начать с создания специализированного языка, и далее продолжать работать с предметной областью на этом специализированном языке.
Эта стратегия была использована при создании различных утилит для манипуляции spec-файлами и src.rpm пакетами, таких как srpmnmu, srpmbackport, srpmconvert, набора утилит для массовых операций с пакетами girar-nmu, утилит repocop-nmu, автономных сервисов, таких как cronbuild, croncopy, cronbackports, autoports, fedoraimport и других.
В основе этих программ лежит библиотека perl-RPM-Source-Editor. Это библиотека perl, и при желании с ней можно работать как с любым другим perl'овым модулем. Однако все вышеперечисленные программы позволяют "на лету" изменять и дополнять свое поведение, подгружая с помощью опций --hook куски кода на perl, в котором можно напрямую работать с уже готовым инициализированным объектом, соответствующим spec-файлу или src.rpm пакету.
Поэтому не зря этот текст и назван введением во встроенный язык редактирования spec-файлов и src.rpm пакетов.
На кого рассчитано это введение? На всех, кто сопровождает более десятка пакетов. Не пристало бесконечно повторять руками монотонные операции, когда возможен unix-way: пишем небольшой скрипт и за 5 минут обрабатываем все 100 своих пакетов или вообще все 12.000 пакетов Сизифа.
В дальнейшем я попытаюсь проиллюстрировать этот текст примерами так, чтобы много простых повседневных задач можно было выполнить с помощью копипаста из приведенных примеров, не требуя знания perl.
Утилиты.
Сначала познакомимся, как пользоваться вышеупомянутыми утилитами, поскольку загружаемые через --hook расширения работают не самостоятельно, а с помощью этих утилит.
Утилиты выполняют начальную распаковку src.rpm пакета, чтение spec-файла, инициализируют необходимые объекты, выполняют свои действия и код, указанный пользователем в опциях --hook, и записывают новый измененный src.rpm либо spec-файл.
Эти утилиты обладают достаточно богатой встроенной функциональностью и разделяют ряд общих опций, унаследованных от модуля RPM::Source::Transform. Рассмотрим их на примере утилит srpmnmu и srpmtool.
Вызов srpmnmu:
srpmnmu --changelog '- Yes! We can!' /path/to/foo-<oldversion>-alt<release>.src.rpm
или
srpmnmu --changelog '- Yes! We can!' -i foo.spec
Опция --changelog может быть сокращена до --ch, а в некоторых утилитах --- и до -c.
Действие srpmnmu по умолчанию - инкрементировать релиз в соответствии со политикой nmuadd (добавить .1 к релизу, если нет точки в релизе; иначе увеличить число после точки; примеры: alt2 -> alt2.1, alt2.1 -> alt2.2 и т.д.) и добавить changelog.
Есть и другие политики увеличения релиза, которые выбираются опцией --next-release-policy (также, --nextrel). Например,
srpmnmu --nextrel=incr -- обычное увеличение релиза (alt2 -> alt3 и т.д.) srpmnmu --nextrel=nmuappend -- "классический" nmu c расческой из единиц
(alt2.1 -> alt2.1.1, alt2.1.1 -> alt2.1.1.1 и т.д.) Отключается увеличение релиза
srpmnmu --nextrel=none (это то же самое, что вызвать srpmtool; srpmtool -- это srpmnmu с политикой увеличения релиза none по умолчанию.
Можно явно указать Release:
srpmnmu --release alt1.rc4_final
Можно явно указать Version:
srpmnmu --version <newversion>
При этом, если "<newversion>" > "<oldversion>", то релиз, если не
указан, будет сброшен в alt1. Если же у нас downgrade,
"<newversion>" < "<oldversion>", то, если другое не указано, будет
увеличен и Serial, и Release (В присутствии Provides/Obsoletes это наиболее разумное поведение).
Явно указать Serial: можно опциями --serial и --epoch, в зависимости от того, какой тег мы хотим получить в spec-файле.
С опцией --version, если редактируется src.rpm, а не spec-файл, обычно нужно еще указать архив исходных текстов новой версии. Т.е. набор опций для обновления src.rpm пакета до новой версии выглядит так:
srpmnmu --version <newversion> --copy_to_sources=foo-<newversion>.tar.gz /path/to/foo-<oldversion>-alt<release>.src.rpm
(Здесь без разницы, srpmnmu или srpmtool.)
Для библиотеки вместе с обновлением исходных текстов до новой версии можно выпустить и compat-версию:
srpmnmu --rename foo4 \ --changelog '- compat library' \ --group-translate 'Development/C|System/Legacy libraries,Development/C++|System/Legacy libraries' \ /path/to/foo-<oldversion>-alt<release>.src.rpm
Есть еще опция --uupdate, автоматически обновляющая src.rpm, содержащий .watch файл, --group-translate, меняющая группы rpm, и много других.
Однако, понятно, что каким бы богатым не был бы набор опций, он не в состоянии покрыть всю необходимую функциональность.
Здесь на помощь приходит встроенный язык.
Загрузка полезного кода опциями --hook. hook по умолчанию.
Можно использовать сколько угодно опций --hook.
srpmnmu --no-default-hook --hook /path/to/hook1.pl --hook ./hook2.pl --hook hook3.pl
В примере выше к hook1.pl и к ./hook2.pl указан путь, к hook3.pl путь не указан. Поэтому hook3.pl будет искаться в HOOKDIR, по умолчанию это ./hooks.
Также для каждого пакета утилиты будут пытаться загрузить его умолчальный файл-hook %name.pl, если явно не запретить такое поведение с помощью опции --no-default-hook,
Удобно, например, при подготовке транзакции с помощью girar-nmu создать папку hooks и складывать туда файлы %name.pl с персональными правками. Это позволит в любой момент сгенерировать транзакцию заново, даже если файлы в Сизифе за это время изменились. Как правило, эти правки стереотипные, т.е. в итоге в ./hooks будет 2-3 уникальных файла, а остальные - симлинки на эти уникальные файлы.
программирование на языке манипуляций spec-файлом
Синтаксис файлов --hook
начнем знакомство с файла hooks/template.pl, содержащего шаблон для наиболее частых правок, которые приходится вносить в спек-файлы при импорте из Fedora. На основе такого шаблона при необходимости удобно создавать личный hook пакета с именем hooks/%{name}.pl, который в дальнейшем будет автоматически использоваться при импорте следующих версий этого пакета в системе fedoraimport.
Итак, файл hooks/template.pl:
push @SPECHOOKS,
sub {
my ($spec, $parent) = @_;
$spec->add_patch('',STRIP=>1);
$spec->get_section('package','')->subst_body(qr'','');
$spec->get_section('package','')->subst_body_if(qr'','',qr'Requires:');
$spec->get_section('prep')->push_body(q!!."\n");
$spec->get_section('package','')->unshift_body('BuildRequires: '."\n");
};
Собственно, минимально необходимая обвязка - это
push @SPECHOOKS,
sub {
my ($spec, $parent) = @_;
};
Мы видим, что в наш код утилита передает 2 объекта perl: $spec, который соответствует текущему редактируемому spec-файлу или src.rpm пакету, и $parent, который соответствует предку редактируемого пакета - например, предыдущей версии этого пакета в Сизифе при импорте или старой версии пакета при обновлении.
$spec определен всегда, $parent может и не быть определен. В наших простых примерах $parent не понадобится.
Методы объекта $spec делятся на методы, применяемые ко всему spec-файлу, и методы, применяемые к отдельным секциям spec-файла. Примером методов на уровне spec-файла является метод add_patch.
push @SPECHOOKS,
sub {
my ($spec, $parent) = @_;
$spec->add_patch('foo-1.2-alt-fix-something.patch',STRIP=>3);
};
Этот код скопирует файл foo-1.2-alt-fix-something.patch из ./patches в %_sourcedir данного пакета (возможно, временный каталог, созданный утилитой srpmnmu или ей родственной); Добавит в спек тег
PatchXX: foo-1.2-alt-fix-something.patch
где XX -- некоторый незанятый номер, и добавит в секцию %prep строку
%patchXX -p3
где 3 указано через параметр STRIP=>3.
Другой пример -- метод add_source. Допустим, мы хотим провести NMU -- добавить в пакеты файлы .service для systemd. Насобираем коллекцию .service файлов, названных по имени пакета (вида %name.service), в папке ./patches. Создадим файл add_systemd_service.pl
push @SPECHOOKS,
sub {
my ($spec, $parent) = @_;
my $sourcenum=$spec->add_source('%name.service');
$spec->get_section('install')->push_body(
'install -Dm644 %{SOURCE'.$sourcenum.'} %buildroot%_systemd/%name.service'."n");
$spec->get_section('files')->push_body('%_systemd/%name.service'."n");
};
Передадим этот файл в утилиту
girar-nmu-prepare --hook add_systemd_service.pl ...
Метод add_source разворачивает '%name.service' в момент выполнения, поэтому наш код будет работать для каждого пакета, обрабатываемого с помощью girar-nmu utils, для которого найдется ./patches/%name.service. Метод add_source скопирует файл, добавит в спек тег
SourceXX: %name.service
Метод add_source возвращает число XX, которое позднее использовано в коде для добавления в секцию %install строки
install -Dm644 %{SOURCEXX} %buildroot%_systemd/%name.service
Методы редактирования на уровне секции.
В предыдущем примере использовался метод редактирования на уровне секции push_body. В действительности, использовалась связка
$spec->get_section('...'[,'...'])->push_body('some text'."\n");
Этот код можно еще переписать
my $section = $spec->get_section('install'); $section->push_body('some text'."\n");
Т.е. сначала получаем объект секции %install, затем добавляем в конец секции строку 'some text'."\n" .
Это отражает дизайн: сначала находим в spec-файлe нужную секцию, затем редактируем ее.
Что такое секции
spec файл делится на части, которые называются секции, с помощью специальных заголовков, которые начинаются с %. Примеры таких заголовков:
%description %package %prep %post ... %files
У этих заголовков могут быть аргументы, например, има подпакета:
%package devel
Начало spec файла не имеет заголовка, однако по смыслу это тоже отдельная секция %package (без аргументов) пакета по умолчанию.
Получаем секции
Как получить секцию? примеры с get_section:
# одна и та же главная секция (секция, # с которой начинается любой spec-файл) $spec->get_section('package',); $spec->get_section('package'); # выделенный метод специально для главной секции $spec->get_main_section; # разные извращения, которые тоже работают, # и в данном случае (Name: foo) возвращают главную секцию $spec->get_section('package','-n foo'); $spec->get_section('package','-n %name');
# секция package для подпакета foo-doc $spec->get_section('package','doc'); $spec->get_section('package','-n foo-doc'); $spec->get_section('package','-n %name-doc'); # секция files для подпакета foo-doc $spec->get_section('files','doc'); # секция description (основная) для подпакета foo-doc $spec->get_section('description','doc'); # секция description (ru_RU.CP1251) для подпакета foo-doc $spec->get_section('description','doc','-l ru_RU.CP1251');
Секции можно найти и по-другому. Переберем все секции, какие есть в spec-файлe, и выберем те, которые нам нужны. Для этого у объекта $section есть методы get_type, get_canonical_package, get_package_name, get_raw_package.
Например, для секции files подпакета foo-doc
get_type равно 'files',
get_canonical_package равно 'doc',
get_package_name равно 'foo-doc'.
Для главной секции
get_type равно 'package',
get_canonical_package равно ,
get_package_name равно 'foo'.
get_raw_package равно .
Для секции description подпакета python-module-foo
get_type равно 'description',
get_canonical_package равно '-n python-module-foo',
get_package_name равно 'python-module-foo'.
get_raw_package равно '-n python-module-%name',
Метод get_raw_package возвращает название подпакета в точности в том виде, как оно записано в заголовке секции, с нераскрытыми макросами и пробелами между -n и названием.
Пример: сосчитаем число секций %changelog. Наш rpm не допускает больше одной секции %changelog, но в сети можно найти всякое.
push @SPECHOOKS, sub { my ($spec, $parent) = @_; my $count_changelog=0; foreach my $section ($spec->get_sections()) { $count_changelog++ if $section->get_type() eq 'changelog'; } print "What a horrible spec! $count_changelog changelogs." if $count_changelog>1; };
Заметим, что метод get_sections создает временный массив. и не годится для некоторых специальных случаев, когда мы в цикле удаляем секции, которых еще не посетили. Для таких особых случаев лучше использовать итератор:
my $section = $spec->get_main_section; do { ... } while ($section=$section->next);
Работаем с секциями
Работа с тегами
Методы get_tag/set_tag/clear_tag позволяют работать с тегами (ключевые слова, оканчивающиеся на ":" : Name, Version, Release, Source3, ...). У get_tag есть опция RAW=>1; с этой опцией get_tag возвращает значение так, как оно записано в spec-файле, без нее - с раскрытыми макросами.
Пример: добавить путь из тега URL: в тег Source: (если в spec-файлe написано Source0:, то добавлено будет в Source0:)
use File::Basename;
push @SPECHOOKS,
sub {
my ($spec, $parent) = @_;
my $section=$spec->get_main_section;
my $urlprefix=dirname($section->get_tag('URL', RAW=>1));
$section->set_tag('Source',$urlprefix.'/'.$section->get_tag('Source', RAW=>1));
};
Работа с телом секции
простые методы
- Метод push_body (список строк) нам уже знаком. Он позволяет аккуратно вставить свои аргументы в конец секции, при чем если в конце есть пустые строки, условные макросы %if,... то пустые строки, условные макросы останутся в конце.
- Метод unshift_body (список строк): вставить в начало секции, после заголовка секции. Пример: вставить в spec-файл %define _unpackaged_files_terminate_build 1.
- Метод match_body(qr'регулярное выражение'): проверить, не встречается ли в секции заданное регулярное выражение.
Пример: Если в спек-файле не определен макрос _unpackaged_files_terminate_build, добавить его.
push @SPECHOOKS,
sub {
my ($spec, $parent) = @_;
my $section=$spec->get_main_section;
$section->unshift_body('# better safe than sorry
%define _unpackaged_files_terminate_build 1
') unless $section->match(qr'\%define\s+_unpackaged_files_terminate_build');
};
- Метод unshift_body_after (qr'регулярное выражение', список строк): вставить после первой строки, подпадающей под qr'регулярное выражение'.
- Метод unshift_body_before (qr'регулярное выражение', список строк): вставить перед первой строкой, подпадающей под qr'регулярное выражение'.
- Метод subst_body: заменить регулярное выражение old text новым текстом new text во всех строчках данной секции.
$section->subst_body(qr'old text','new text');
Пример: поменять во всех секциях, кроме %changelog, %{macros1} на %{macros2}.
push @SPECHOOKS,
sub {
my ($spec, $parent) = @_;
foreach my $section ($spec->get_sections) {
next if $section->get_type eq 'changelog';
$section->subst_body(qr'(?<!%)\%{macros1}','%{macros2}');
$section->subst_body(qr'(?<!%)\%macros1','%macros2');
}
};
- Метод subst_body_if: заменить регулярное выражение old text новым текстом new text в строчках данной секции, которые подпадают под регулярное выражение 'anchor'.
$section->subst_body_if(qr'anchor', qr'old text','new text');
Пример: поменять во всех секциях package во всех тегах Requires: и BuildRequires: bar-devel на libbar2-devel
push @SPECHOOKS,
sub {
my ($spec, $parent) = @_;
foreach my $section ($spec->get_sections) {
next if $section->get_type ne 'package';
$section->subst_body_if(qr'Requires:',qr'bar-devel','libbar2-devel');
}
};
- Метод exclude_body: убрать из секции строку, которая попадает под заданное регулярное выражение.
Пример: убрать из секции %post устаревший макрос %update_desktopdb
$spec->get_section('post','')->exclude_body(qr'\%update_desktopdb');
- Метод multi_exclude_body: убрать из секции несколько строк, которые поочередно попадают под заданный список регулярных выражений.
Пример: убрать из секции %build конструкцию, рассчитанную на другие дистрибутивы:
if ! rpm -E %%cmake|grep -q "cd build"; then %__mkdir_p build cd build fi
$spec->get_section('build')->multi_exclude_body(
qr'if\s*!\s*rpm\s+-E\s+\%\%cmake\s*\|\s*grep\s+-q\s+"cd\s*build";\s*then',
qr'(__mkdir_p|mkdir\s+-p)\s+build',
qr'cd\s+build',
qr'fi');
как оформлять аргументы для простых методов
Их аргументы - строка или регулярное выражение.
Простые правила, для тех, кто мало знаком с perl;
- Добавляемые строки надо заканчивать концом строки "\n", иначе они слипнутся со следующей строкой. Пример:
# можно так (perl позволяет вставлять внутрь кавычек конец строки)
$spec->main_section->push_body('Requires: foo
');
# или же так
$spec->main_section->push_body('Requires: foo'."\n");
- Регулярные выражения лучше брать в специальные кавычки qr (можно при необходимости и qr!!, qr{}, и т.д. см. синтаксис perl). Без них вы рискуете тем, что сложное регулярное выражение будет испорчено при подстановке.
# вместо
$spec->main_section->exclude_body('^Requires: foo');
# пишите
$spec->main_section->exclude_body(qr'^Requires: foo');
продвинутые методы
Для пользователей, которые хорошо знакомы с perl, есть методы map_body и visit_body. Аргументом для них является ссылка на функцию, в которую метод построчно передает тело секции в специальной переменной perl $_.
- Метод map_body -- функция пользователя возвращает измененную строку в той же специальной переменной perl $_, в которой было оригинальное значение.
Пример: реализация subst_body_if через map_body
$section->map_body(sub {s/old text/new text/ if /anchor/});
- Метод visit_body -- то же, что и map_body, но возвращаемые значения игнорируются, тело секции не меняется.
Пример: реализация match_body через visit_body
my $match_found=0;
$section->visit_body(sub {$match_found=1 if /pattern/});
работа с предупреждениями
Если операция не удалась, библиотека выдает предупреждение. В некоторых случаях это нежелательно. К примеру, пусть пакет не собрался с диагностикой
sisyphus_check: check-intersects ERROR: intersections with system packages /usr/share/icons/hicolor/48x48/apps
рассмотрим код, который удаляет из секций %files строки вида %dir %{_datadir}/icons/hicolor.*
foreach my $section ($spec->get_sections()) {
next if $section->get_type() ne 'files';
$section->exclude_body(qr'^\%dir\s+\%\{_datadir\}/icons/hicolor');
}
Если в пакете 6 подпакетов (соответственно, 6 секций %files), а %dir %{_datadir}/icons/hicolor.* найдена только в 2-х из них, то 4 команды exclude_body завершатся неудачей, о чем будут выданы предупреждения. Поскольку так и задумано, что часть команд exclude_body завершатся неудачей, предупреждения можно убрать. Для этого перед их выполнением нужно окружить вызовами applied_off/applied_on
$spec->applied_off();
foreach my $section ($spec->get_sections()) {
next if $section->get_type() ne 'files';
$section->exclude_body(qr'^\%dir\s+\%\{_datadir\}/icons/hicolor');
}
$spec->applied_on();
Однако полное отключение предупреждений неудобно тем, что в следующем релизе апстрим может совсем убрать %dir %{_datadir}/icons/hicolor из spec-файла, а с отключенными предупреждениями мы об этом не узнаем и будем хранить в hook бесполезный уже код. Поэтому в таких случаях лучше пользоваться вызовом applied_block (" строка предупреждения ", sub{ блок кода })
$spec->applied_block(
"убрать %dir %{_datadir}/icons/hicolor из %files",
sub {
foreach my $section ($spec->get_sections()) {
next if $section->get_type() ne 'files';
$section->exclude_body(qr'^\%dir\s+\%\{_datadir\}/icons/hicolor');
}
}
);
Такой код будет молчать, если хоть одна операция применена успешно, но предупредит, если ни в одной секции операцию применить не удалось.
Добавляем патчи
Пусть у нас есть (Условно) для пакета trinity-tqt3, который мы хотим сопровождать роботом импорта, набор патчей от версии - предка, qt3, при чем всего патчей 9, а прикладываемых из них только 5. В спеке qt3 есть следуюшее:
# FC
Patch2: qt-3.0.5-nodebug.patch
Patch3: qt-3.3.8d-xim.patch
# MDK
Patch21: qt-3.0.5-fix-pyqt-config.patch
Patch22: qt3-opentype-aliasing.patch
# SuSE
Patch30: qt3-never-strip.diff
Patch31: shut-up.diff
# Qt-copy
Patch51: 0046-qiconview-no-useless-scrollbar.diff
Patch52: 0078-argb-visual-hack.patch
# Sergey A. Sukiyazov <sukiyazov@mail.ru>
Patch9000: 9000-qt-x11-free-3.3.3-menubar.patch
из них прикладываются только
%patch21 -p1
%patch22 -p1
%patch30 -p0
%patch51 -p2
%patch52 -p2
остальные хотелось бы не прикладывая, подержать под рукой для будущих разборок.
Создадим папку patches и скопируем туда все патчи.
Преобразуем Patch и %patch в команды add_patch. Опцию -pN заменим на STRIP=>N, если патч не прикладывается, добавим DISABLE=>1, комментарий сохраним в опции HEADER=>'# comment text'."\n"
Отметим, что можно сохранить и номер патча, в опции NUMBER => XXX. При этом библиотека проверит этот номер на конфликт с уже имеющимися патчами (вдруг апстрим добавил патч с тем же номером) и при необходимости увеличит номер, чтобы избежать конфликта.
Добавим полученные команды в файл hooks/trinity-tqt3.pl, в POSTHOOKS
[...]
$spec->add_patch('qt-3.0.5-nodebug.patch', STRIP=>1, DISABLE=>1, HEADER=>'# FC'."\n");
$spec->add_patch('qt-3.3.8d-xim.patch', STRIP=>1, DISABLE=>1);
$spec->add_patch('qt-3.0.5-fix-pyqt-config.patch', STRIP=>1, HEADER => '# MDK'."\n",);
$spec->add_patch('qt3-opentype-aliasing.patch', STRIP=>1);
$spec->add_patch('qt3-never-strip.diff', STRIP=>0, HEADER => '# SuSE'."\n",);
$spec->add_patch('shut-up.diff', STRIP=>0, DISABLE=>1);
$spec->add_patch('0046-qiconview-no-useless-scrollbar.diff', STRIP=>2, HEADER=>'# Qt-copy'."\n");
$spec->add_patch('0078-argb-visual-hack.patch', STRIP=>2);
$spec->add_patch('9000-qt-x11-free-3.3.3-menubar.patch', STRIP=>1, DISABLE=>1,
NUMBER => 9000,
HEADER => '# Sergey A. Sukiyazov <sukiyazov@mail.ru>'."\n",
);
[...]
Если сбрасывать патчи от разных пакетов в одну папку patches кажется неэстетичным, то можно создать в patches подпапку patches/tqt3 и переложить патчи туда. Но тогда надо будет указать эту подпапку в команде add_patch: вместо
$spec->add_patch('0078-argb-visual-hack.patch', STRIP=>2);
надо будет
$spec->add_patch('tqt3/0078-argb-visual-hack.patch', STRIP=>2);
debug
Волшебная опция для создания digest.diff файла
--SET RPM::Source::TransformContainer::PLAYER=RPM::Source::Transformation::DiffWritePlayer \
и усиленный ее вариант, если нужно найти конкретный фильтр:
--SET RPM::Source::TransformContainer::PLAYER=RPM::Source::Transformation::DiffWritePlayer \ --SET RPM::Source::Transformation::Factory::DependencyFilter::group_filters_by_transformation=0
To be continued ...
Это, конечно, не вся функциональность. API еще в разработке, и я упомянул только самую стабильную его часть. Но представление о возможностях и первое знакомство это введение дает.
Ссылки
- видеозапись выступления на YAPC::Russia + Perl Mova 2012
- тезисы доклада в Обнинске, 2012 (с. 57)
- слайды доклада в Киеве, 2011
- тезисы доклада в Обнинске, 2011 (с. 36)
Разработано при поддержке Фонда содействия развитию МП НТС в рамках НИОКР 01201066526 |