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.srm либо 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, и много других.
Однако, понятно, что каким бы богатым не был бы набор опций, он не в состоянии покрыть всю необходимую функциональность.
Здесь на помощь приходит встроенный язык.
программирование на языке манипуляций 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');
Секции можно найти и по-другому. Переберем все секции, какие есть в 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/});
To be continued ...
Это, конечно, не вся функциональность. API еще в разработке, и я упомянул только самую стабильную его часть. Но представление о возможностях и первое знакомство это введение дает.