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

начнем знакомство с файла 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);

Работа с подпакетами

Пример: в спеке есть подпакет static (название не соответствует Alt Linux policy).

%package static
Group: Development/C
[...]
  • rename_package(oldname, newname)

можно так

 $spec->rename_package('static','devel-static');

или так

 $spec->rename_package('-n foo-static','-n foo-devel-static');
  • disable_package(name)
 $spec->disable_package('static');


Работаем с секциями

Работа с тегами

Методы 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");
  • в командах push_body, unshift_body, unshift_body_before, unshift_body_after можно свтавить не одну сторку, целый массив.

Кроме того, можно вставлять многострочные куски текста:

$spec->main_section->push_body('# example
Requires: foo
Requires: bar
Requires: baz
');
  • Регулярные выражения лучше брать в специальные кавычки 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 еще в разработке, и я упомянул только самую стабильную его часть. Но представление о возможностях и первое знакомство это введение дает.

Ссылки

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