Сборка пакета с препятствиями

Ниже описаны процесс сборки одного нестандартного пакета под дебиан, а также замечания по ходу сборки и возникших проблем.

Пакет который я пытался собрать - обучающая среда "КуМир", созданная нашими славными российскими разработчиками. Представляет с собой что-то наподобие IDE - редактор с подсветкой синтаксиса, лексическим анализатором, отладчиком и несколькими "исполнителями". "Исполнитель" в данном случае - аналог разделяемой библиотеки.

debhelper - описание

Основным способом сборки пакетов под deb-based дистрибутивы является сборка с помощью debhelper. Это набор скриптов, призванных, в теории, упростить жизнь создателям и мейнтейнерам пакетов.

Прежде чем начать, прочитайте руководство сборщика пакетов Debian, хотя бы по диагонали, чтобы примерно представлять себе как с этим обращаться.

Технически, файлы описание пакета представляют собой кучу мелких файликов строго определенного формата в директории debian/ в корне пакета. Чтобы не создавать их вручную, воспользуйтесь dh_make - это развернет типовой шаблон для выбранного типа пакета.

tar -xzf kumir-1.7.3.2369.tar.gz
cd kumir-1.7.3
dh_make -f ../kumir-1.7.3.2369.tar.gz
<отвечаем на вопросы dh_make>

Нам выдается предупреждение, что пакет должен уметь устанавливаться в произвольную директорию определяемой переменной DESTDIR1

Сразу после этого можно поудалять лишнее, чтобы не мозолило глаза. Для данного пакета это будет так:

  • changelog - меняется с помощью dch -i (от DebianChangelog --increase)
  • compat - оставляем без изменений, служебный файл
  • control - файл описания пакета: название, версия, лицензия, зависимости, зависимости для сборки, и т.д.
  • copyright - это лицензия на вашу работу по "дебианизации" пакета
  • docs - файлы в пакете, которые считаются документацией
  • emacsen-*.ex
  • init.d.ex - это не демон, нет необходимости запускать его при старте системы
  • kumir.default.ex - параметры для init-скрипта
  • kumir.cron.d.ex - приложение не должно выполняться системным планировщиком (cron)
  • kumir.doc-base.ex - документацию к проекту нужно будет оформлять отдельно
  • manpage.*.ex - у нас нет документации в формате manpage
  • menu.ex - файл, добавляющий программу в меню DE
  • post*.ex - скрипты, выполняющиеся после установки и удаления пакета
  • pre*.ex - скрипты выполняющиеся до установки и перед удалением пакета,
  • README.Debian
  • README.source
  • rules - этот файл отвечает за сам процесс сборки пакета
  • source
  • watch.ex - файл описания для системы слежения за появлением новых версий пакетов

О файле "rules" стоит упомянуть отдельно. По синтаксису - это обычный make-файл, но с фиксированными целями: binary binary-arch binary-indep build clean install. При типичной сборке последовательность выполнения целей такова: build -> binary -> install В новых версиях этот файл по умолчанию содержит следующее:

#!/usr/bin/make -f
\%:
    dh $@

При выполнении, эти правила (а \% + @$ говорят нам что это "неявные правила" (implicit rules)) разворачиваются вот так:

build:
    dh build

binary:
    dh binary

install:
    dh install

"Но и это ещё не всё!" © dh - это на самом деле скрипт, который вызывает некоторую последовательность более низкоуровневых команд. (в документации он называется sequencer'ом). т.е. следующим шагом это развернётся так:

#!/usr/bin/make -f

\%:
    dh $@

build:
    dh_testdir
    dh_auto_configure
    dh_auto_build
    dh_auto_test

binary:
    dh_testroot
    dh_prep
    dh_installdirs
    dh_auto_install
    dh_install
    dh_installdocs
    dh_installchangelogs
    dh_installexamples
    dh_installcatalogs
    dh_installdebconf
    dh_installinfo
    dh_pysupport
    dh_installinit
    dh_installmenu
    dh_installmime
    dh_installmodules
    dh_installlogcheck
    dh_installlogrotate
    dh_installppp
    dh_installwm
    dh_installxfonts
    dh_bugfiles
    dh_lintian
    dh_icons
    dh_perl
    dh_usrlocal
    dh_link
    dh_compress
    dh_fixperms
    dh_strip
    dh_makeshlibs
    dh_shlibdeps
    dh_installdeb
    dh_gencontrol
    dh_md5sums
    dh_builddeb

В свою очередь dh_* - это тоже скрипты, которые вызывают ещё более низкоуровневые команды, которые уже и выполняют, собственно, полезную работу.

Т.е мы имеем минимум 3 слоя абстракции в системе сборки. Вот здесь есть хорошее чтиво на эту тему.

Система рассчитана опять же, на сборку сферичного пакета в вакууме. Но в реальности это не работает, обязательно всплывут какие-либо ньюансы™. Но пока что мы о них не знаем и только лишь рихтуем файл "control" примерно до такого состояния:

--- a/debian/control    2012-03-15 12:02:36.000000000 +1100
+++ b/debian/control    2012-03-16 14:25:15.000000000 +1100
@@ -1,15 +1,17 @@
 Source: kumir
-Section: unknown
+Section: Education
 Priority: extra
-Maintainer: unknown <user@vm9.test.lan>
-Build-Depends: debhelper (>= 7.0.50~)
+Maintainer: Alex 'AdUser' Z <ad_user@mail.ru>
+Build-Depends: dpatch, debhelper (>= 7.0.50~), qt4-qmake, libqt4-dev, libx11-dev, libxext-dev, python (>= 2.5),
 Standards-Version: 3.8.4
-Homepage: <insert the upstream URL, if relevant>
-#Vcs-Git: git://git.debian.org/collab-maint/kumir.git
-#Vcs-Browser: http://git.debian.org/?p=collab-maint/kumir.git;a=summary
+Homepage: http://www.niisi.ru/kumir/
+Vcs-Svn: svn://lpm.org.ru/svn/kumir/branches/1.7/
+Vcs-Browser: http://lpm.org.ru/svn/kumir/

 Package: kumir
-Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}
-Description: <insert up to 60 chars description>
- <insert long description, indented with spaces>
+Architecture: i386
+Depends: libqt4-core, libqt4-gui, libqt4-network, libqtscript4-core, libqt4-svg, libqt4-xml, libqt4-webkit, ${shlibs:Depends}, ${misc:Depends}
+Description: Kumir Language Implementation
+  Implementation of Kumir programming language, designed by academic Ershov.
+  Includes compiler, runtime, IDE, Robot and Draw.
+  Also includes addons: Turtle, Aquarius, Grasshopper.

Описания всех полей смотрите в документации по ссылке выше. Зависимости определяются по следующему алгоритму:

  • ищем в исходниках список зависимостей указанный разработчиком
    • смотрим в README/INSTALL/BUILD/...
    • если нашли - ищем подобные пакеты через apt-cache/aptitude/synaptic/...
    • ищем подобные пакеты, но с суффиксом "-dev"
  • если программа собрана для другого дистрибутива
    • смотрим зависимости там
    • опять ищем подобные пакеты, как в предыдущем шаге
  • прописываем то, что нашли - через запятую в поле "Depends" (а пакеты с суффиксом "-dev" - в Build-Depends)
  • пробуем собрать пакет через dpkg-buildpackage -rfakeroot
  • если не собралось - смотрим, на чем остановились, и здесь уже гугл в помощь
  • повторять предыдущий пункт пока не соберется
  • всё что мы забыли - допишет в зависимости макрос ${shlibs:Depends}

debhelper - сборка

С теорией вроде разобрались, теперь посмотрим, как же нам всё-таки собрать конкретно этот пакет.

В корне директории есть configure.py и ссылка "configure" на него. Ага, начало стандартное2.

Здесь нас поджидают первые грабли, заботливо разложенные разработчиками - этот файл не исполняемый, т.е. ./configure - не сработает. Запускать нужно явно, через python ./configure.py

Вроде что-то отработало, попробуем дальше - make. На этом этапе сборка может прерваться из-за неустановленных зависимостей. Но вроде тоже всё в порядке.

Пробуем последний этап: make DESTDIR=/tmp/ install и сборка валится с сообщением об отказе в доступе для "/usr/local". $DESTDIR не работает, плохо, придется разбираться в системе сборки и смотреть как это поправить.

В качестве системы сборки здесь используется qmake.

qmake

Небольшое лирическое отступление. Зачем вообще нужен qmake? Дело в том, что Qt - это фреймворк и уже давно включает в себя модули на все случаи жизни. Эти модули периодически переписывают, добавляя новый функционал, убиряя устаревший, а иногда и и просто ради эстетичности и "красоты" кода. Так вот, qmake нужен, чтобы автоматически отслеживать зависимости и подставлять нужные флаги при линковке приложения.

Файлы описания проекта выглядят как куча файлов *.pro, раскиданная по дереву с исходниками. Типовой файл выглядит примерно так (Kumir/Kumir.pro):

FORMS += Forms/ProgramTab.ui         <пропущен список файлов>
    Forms/multifilesavewizard.ui
HEADERS += Headers/strtypes.h         <пропущен список файлов>
    ../TaskControl/csInterface.h
SOURCES += Sources/mainwindow.cpp         <пропущен список файлов>
    Sources/kummodules.cpp
TRANSLATIONS = Languages/english.ts         Languages/russian.ts         Languages/french.ts
TEMPLATE = app
DEPENDPATH += ../Libraries/kpython
INCLUDEPATH += Headers
INCLUDEPATH += ../
INCLUDEPATH += ../Libraries/kpython
INCLUDEPATH += ../Addons/protoModule

QT += xml         script         svg         network        webkit
RESOURCES += Resources/MainWindow.qrc         Resources/Assistant.qrc
TARGET = kumir
macx:ICON=../app_icons/mac/kumir.icns
linux-g++: LIBS += -lX11
linux-g++64: LIBS += -lX11
include(../Scripts/common.pri)
unix:dummy.extra=python ../Scripts/install_kumir.py --spec=unix --prefix=$$PREFIX --kumir-dir=$$KUMIR_DIR
macx:dummy.extra=../Mac/Kumir/embed_macosx_resources.sh
win32:dummy.extra=python ../Scripts/install_kumir.py --spec=win32 --prefix=$$PREFIX --kumir-dir=$$KUMIR_DIR
dummy.path=./
INSTALLS = dummy

Следите за руками. Видимо, неосилив qmake в части установки целей средствами самого qmake, разработчики, недолго думая, написали скриптик на питоне, который это будет делать за qmake - install_kumir.py3. Скриптик, естественно, ни про какой DESTDIR слыхом не слыхивал и вообще имел его ввиду.

Обратите внимание на "FORCE" и "dummy.extra". ".extra", как следует из документации, предназначен для выполнения дополнительных действий после установки, и использование его для самой установки противоречит "философии" qmake.

Чтение документации по qmake также показало, что существует местный аналог DESTDIR - INSTALL_ROOT, но его использование тоже не работает, из-за тех же "нескучных" питоновских скриптов.

Битва за $DESTDIR

Что можно сделать?

  • можно исправить строчку с вызовом питоновского скрипта на $$DESTDIR/$PREFIX. Неудачная идея. В выходном Makefile $$DESTDIR разворачивается и мы получаем неверный префикс для приложения.
  • можно исправить параметр ".path", но этого тоже делать не стоит, скрипт просто не найдет нужного файла.
  • наконец, можно исправить сам питоний скрипт, чтобы он учитывал, собака такая, наши пожелания.

Действуем по третьему варианту. Накладываем следующий патчик на Scripts/install_kumir.py:

--- src/Scripts/install_kumir.py    2012-03-20 21:23:59.628590648 +1100
+++ dst/Scripts/install_kumir.py    2012-03-20 21:24:56.186145954 +1100
@@ -1,5 +1,6 @@
 #!/usr/bin/python

+import os
 import sys

 for arg in sys.argv:
@@ -14,6 +15,11 @@
 if PREFIX=="": PREFIX="/usr/local"
 if KUMIR_DIR=="": KUMIR_DIR = PREFIX+"/kumir"

+env = os.environ
+if env.has_key('DESTDIR'):
+    PREFIX = os.environ['DESTDIR']+PREFIX
+    KUMIR_DIR = os.environ['DESTDIR']+KUMIR_DIR
+
 from install_generic import *

 if SPEC=="unix":

В результате получим это:

#!/usr/bin/python

import os
import sys

for arg in sys.argv:
    if arg.startswith("--prefix="):
        PREFIX = arg[9:]
    if arg.startswith("--spec="):
        SPEC = arg[7:]
    if arg.startswith("--kumir-dir="):
        KUMIR_DIR = arg[12:]

if SPEC=="": SPEC="unix"
if PREFIX=="": PREFIX="/usr/local"
if KUMIR_DIR=="": KUMIR_DIR = PREFIX+"/kumir"

env = os.environ
if env.has_key('DESTDIR'):
    PREFIX = os.environ['DESTDIR']+PREFIX
    KUMIR_DIR = os.environ['DESTDIR']+KUMIR_DIR

from install_generic import *

if SPEC=="unix":
    install_file("../kumir",KUMIR_DIR+"/kumir",True)
    create_shell_link(KUMIR_DIR+"/kumir",PREFIX+"/bin/kumir")
elif SPEC=="win32":
    install_file("../kumir.exe",KUMIR_DIR+"/kumir.exe",True)
install_dir("Config",KUMIR_DIR+"/Kumir")
install_dir("Environments",KUMIR_DIR+"/Kumir")
install_dir("Examples",KUMIR_DIR+"/Kumir")
install_dir("Help",KUMIR_DIR+"/Kumir")
install_dir("HyperText",KUMIR_DIR+"/Kumir")
install_dir("Macro",KUMIR_DIR+"/Kumir")
install_dir("PDScripts",KUMIR_DIR+"/Kumir")
install_dir("Languages",KUMIR_DIR+"/Kumir")
install_dir("Images/customwindow_pixmaps",KUMIR_DIR+"/Kumir")
if SPEC=="unix":
    install_file("Images/kumir.png",
                 PREFIX+"/share/pixmaps/kumir.png",False)
    install_file("X-Desktop/kumir.desktop",
                 PREFIX+"/share/applications/kumir.desktop",False)

Пробуем собрать и установить цель kumir:

cd Kumir
make
make DESTDIR=/tmp install

Работает! Обрабатываем по образу и подобию остальные скрипты Script/install_*.py

Из-за нестандартного 'configure' разворачиваем dh build в debian/control и заменяем dh_auto_configure на наш вариант:

build:
    dh_testdir
-   dh_auto_configure
+   python ./configure.py --prefix=/usr --target-dir=/opt/kumir
    dh_auto_build
    dh_auto_test

"--target-dir" нужен, т.к. у нас нестандартное размещение бинарников, ресурсов и т.д., если его не указывать, всё поставится в /usr/kumir4.

Пробуем собрать пакет ...успешно. :-)

Опять qmake

После успешной сборки, казалось бы можно перевести дух, но не тут-то было. Смотрим в содержимое пакета и помимо ожидаемых "opt" и "usr" видим также "home" с полным путем до места сборки и пустые директории для всех целей, которые он собирал. Непорядок, этого мусора в пакете быть не должно.

Возвращаемся к файлу проекта и внимательно смотрим на параметр ".path". Читаем документацию и выясняем, что в зависимости от его значения, он разворачивается по разному в выходном Makefile, что добавляет немного wtf'ака в процесс сборки. :-) Причем вышеупомянутый $INSTALL_ROOT подставляется ко всем устанавливаемым файлам неявно и автоматически (ещё один wtf'к).

extra.path = ./ -> /home/user/build/.../.../
extra.path = / -> /

У нас стоит "./" что и вызывает создание "home" в выходной директории. Исправление на "/" решило бы проблему, но этого делать нельзя, чтоб не поломать сборку под win и mac, починив сборку под *nix. Это тоже хак, и в идеале здесь должно быть что-то типа "$$PREFIX/bin", но чтобы сделать правильно, придётся переделать половину системы сборки для этого пакета.

Проблему решаем, добавив строчку "unix:extra.path = /", что будет применяться только для сборки под *nix, не затрагивая остальные системы. Этот «/» при установке будет дополнен питоновским скриптом до правильного пути.

Все изменения выше оформляем в виде патчей, складываем в debian/patches и переписываем имена патчей в debian/patches/series:

cp Scripts/install_kumir.py Scripts/install_kumir.py.old
nano Scripts/install_kumir.py
cd ..
diff -durN kumir-1.7.3/Scripts/install_kumir.py.old kumir-1.7.3/Scripts/install_kumir.py >> debian/patches/001-build.patch
echo 001-build.patch >> debian/patches/series

"После сборки - обработать напильником"

Ставим получившийся пакет и пробуем его запустить. Как бы не так. Пробуем запустить из терминала - "Нет такого файла или каталога". Смотрим на файл /usr/bin/kumir и видим, что он представляет shell-скрипт, запускающий /home/user/build/.../.../opt/kumir/kumir5. Похоже, мы опять налажали с путями. Ищем место, где создается этот скрипт и находим его опять в install_kumir.py

Ага, вот эти ребята:

if SPEC=="unix":
    install_file("../kumir",KUMIR_DIR+"/kumir",True)
    create_shell_link(KUMIR_DIR+"/kumir",PREFIX+"/bin/kumir")

Модифицируем скрипт так:

--- Scripts/install_kumir.py.old    2012-03-20 21:29:18.036593285 +1100
+++ Scripts/install_kumir.py    2012-03-20 21:30:17.976651480 +1100
@@ -17,6 +17,7 @@

 env = os.environ
 if env.has_key('DESTDIR'):
+    KUMIR_DIR_ORIG = KUMIR_DIR
     PREFIX = os.environ['DESTDIR']+PREFIX
     KUMIR_DIR = os.environ['DESTDIR']+KUMIR_DIR

@@ -24,7 +25,7 @@

 if SPEC=="unix":
     install_file("../kumir",KUMIR_DIR+"/kumir",True)
-    create_shell_link(KUMIR_DIR+"/kumir",PREFIX+"/bin/kumir")
+    create_shell_link(KUMIR_DIR_ORIG+"/kumir",PREFIX+"/bin/kumir")
 elif SPEC=="win32":
     install_file("../kumir.exe",KUMIR_DIR+"/kumir.exe",True)
 install_dir("Config",KUMIR_DIR+"/Kumir")

Пересобираем пакет, ставим, пробуем запустить. Теперь ругается на отсутствующий /opt/kumir/Addons/turtle.ini. Прогресс, однако.

Ищем в документации, как в пакет можно положить дополнительные файлы, и находим - dh_install. Разворачиваем dh binary, заменяем вызов dh_install на следующий:

-   dh_install
+   dh_install Addons/turtle.ini opt/kumir/Addons/

Обратите внимание, вторым аргументом указывается директория, в которую будет помещён файл.

Опять пересобираем пакет, ставим, запускаем, радуемся жизни.

Вот так это выглядит в запущенном виде: главное окно. FIXME (заливка файлов не работает)

Вместо заключения

Чистого времени на сборку понадобилось ~ 3 вечера, один из которых я читал документацию. Сложность конкретно этого пакета оцениваю на 4 из 10. Замечания, дополнения, просьба оставлять в "обсуждении".

Но если учесть распространённость формата, время его появления и груз обратной совместимости - несмотря на недостатки он ещё долго будет в ходу.


  1. на самом деле она является стандартом "де-факто", в LSB она не описана ↩

  2. опять же - стандарт "де-факто" для сборки из исходников - configure && make && make install ↩

  3. фирменные грабли №2 ↩

  4. фирменные грабли от разработчиков №3 ↩

  5. кстати, кто-нибудь объяснит мне, почему нельзя было обойтись простым симлинком? ↩