Packaging Automation/Embedded Language

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


Утилиты для манипуляции 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

начнем знакомство с файла template.pl, содержащего шаблон для наиболее частых правок, которые приходится вносить в спек-файлы при импорте из Fedora. На основе такого шаблона при необходимости удобно создавать личный hook пакета с именем %{name}.pl, который в дальнейшем будет автоматически использоваться при импорте следующих версий этого пакета в системе fedoraimport.

#!/usr/bin/perl -w

Итак, файл:

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 нужную секцию, затем редактируем ее.

Получаем секции

Как получить секцию? примеры с 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: проверить, не встречается ли в секции заданное регулярное выражение.

Пример: Если в спек-файле не определен макрос _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');
};
  • Метод 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');

Для пользователей, которые хорошо знакомы с 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/});

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 еще в разработке, и я упомянул только самую стабильную его часть. Но представление о возможностях и первое знакомство это введение дает.

Ссылки

Разработано при поддержке Фонда содействия развитию МП НТС в рамках НИОКР 01201066526 rigft