Зачем всё это
Привет! Вторая статья на habr и третья за жизнь. Сегодня мы в формате мини-урока поговорить про такую вещь, как дебинизация проекта — процесс формирования .deb-пакета.
Весь приведённый материал в статье представлен здесь: https://github.com/X0re4ik/dpkg-study-cpp. Так как данная статья позиционирует себя как туториал, то мы добавим ей немножко интерактивности. Переходите по ссылке: https://x0re4ik.ru/study/tutorials/001-cpp-study — заботливый GLM сформировал интерактивный учебник по статье (буду очень благодарен за обратную связь: насколько идея живая).
Чему научимся
По ходу статьи пройдём путь от ручной сборки до автоматизации:
-
Соберём минимально жизнеспособный пакет руками, без специализированных утилит (
dpkg --build). -
Добавим бинарник, конфиг и systemd-юнит с postinst/prerm-скриптами.
-
Поймём, почему пакет не обновляется при повторном
apt-get installи как работает версионирование. -
Разберёмся с зависимостями: как выяснить, от чего зависит бинарник, и что писать в
Depends:. -
Заглянем внутрь .deb и увидим, что никакой магии там нет.
-
Автоматизируем всё то же самое через
dpkg-buildpackageиdebhelper— и поймём, что утилиты просто делают за нас то, что мы уже умеем делать руками.
Дано
Проект pg-server — небольшой HTTP-сервер на C++, который:
-
собирается обычным
makeв один бинарник; -
читает из YAML-конфига порт, на котором ему нужно подняться;
-
на любой HTTP-запрос отвечает версией установленного драйвера PostgreSQL (
libpq).
Структура нашего приложения на данный момент следующая:
.├── Makefile├── Dockerfile├── example.config.yaml # конфигурационный файл├── pg-server.service # юнит systemd└── src └── main.cpp
example.config.yaml:
port: 3031
pg-server.service:
[Unit]Description=PostgreSQL version HTTP server[Service]Type=simpleExecStart=/usr/bin/pg-server --config /etc/pg-server/config.yamlDynamicUser=yesNoNewPrivileges=yesRestart=on-failureRestartSec=3[Install]WantedBy=multi-user.target
Чего хотим
Мы хотим, чтобы наше приложение ставилось одной командой:
apt-get install ./pg-server.deb
А удаляться наше приложение должно так же легко, как и ставиться:
apt-get remove pg-server
Ручная сборка проекта
Начнём с ручного формирования deb-пакета. Первым делом обсудим структуру проекта, она будет следующей:
├── Makefile├── packages│ ├── pg-server-amd64-dpkg│ │ ├── DEBIAN│ │ │ ├── control # описание пакета: имя, версия, архитектура│ │ │ ├── postinst # скрипт, выполняемый после установки│ │ │ └── prerm # скрипт, выполняемый перед удалением│ │ ├── etc│ │ │ └── pg-server│ │ │ └── config.yaml # конфигурационный файл проекта│ │ ├── lib│ │ │ └── systemd│ │ │ └── system│ │ │ └── pg-server.service # юнит systemd│ │ └── usr│ │ └── bin│ │ └── pg-server # наш бинарник│ └── pg-server-amd64-dpkg.deb└── src └── main.cpp
При первом взгляде на целевую картинку пакета можно заметить, что она повторяет то, как файлы будут располагаться на целевой машине: pg-server после установки попадёт в /usr/bin/pg-server, config.yaml — в /etc/pg-server/config.yaml, а юнит — в /lib/systemd/system/pg-server.service. Один из фокусов deb-пакетов раскрыт: куда встанет тот или иной файл, определяется при создании самого пакета.
Для начала соберём каркас пакета — папку с манифестом и пока пустыми скриптами:
# Создаём каркас пакетаmkdir -p packages/pg-server-amd64-dpkg/mkdir -p packages/pg-server-amd64-dpkg/DEBIAN/# Скрипты пока оставим пустыми - их содержимым займёмся нижеtouch packages/pg-server-amd64-dpkg/DEBIAN/controltouch packages/pg-server-amd64-dpkg/DEBIAN/postinsttouch packages/pg-server-amd64-dpkg/DEBIAN/prerm
packages/pg-server-amd64-dpkg/DEBIAN/control — здесь хранится манифест нашего проекта:
Package: pg-serverSection: utilsPriority: optionalVersion: 0.0.1Maintainer: Mochalov Anton Standards-Version: 3.9.4Architecture: amd64Description: Учимся собирать свой простой deb-пакет
Мы задаём название проекта, владельца пакета, архитектуру и описание. Разбор postinst, prerm и юнита systemd оставим на потом. Как только файл control готов, мы уже можем собрать наш deb-пакет — хоть изначально он и будет бесполезен:
dpkg --build ./packages/pg-server-amd64-dpkgsudo apt-get install ./packages/pg-server-amd64-dpkg.deb
После установки пакета Linux хранит связь между пакетом и связанными с ним файлами, посмотреть её можно по пути /var/lib/dpkg/info/pg-server.list.
В нашем простом случае этот файл будет иметь следующий вид:
cat /var/lib/dpkg/info/pg-server.list/.
Единственная запись /. — это корень пакета: файлов в нём пока нет, только манифест.
Наполняем пакет
Теперь положим в пакет бинарник и конфигурацию. Сначала соберём сам сервер:
# Собираем бинарникmake# Кладём исполняемый файл в пакетmkdir -p packages/pg-server-amd64-dpkg/usr/bin/cp pg-server packages/pg-server-amd64-dpkg/usr/bin/pg-server# Кладём конфигурационный файлmkdir -p packages/pg-server-amd64-dpkg/etc/pg-server/cp example.config.yaml packages/pg-server-amd64-dpkg/etc/pg-server/config.yaml
Пересобираем и переустанавливаем. Нюанс: apt не станет ставить пакет, если его версия не изменилась — почему так происходит, разберём в главе про версионирование. Пока просто удалим и поставим заново:
sudo apt-get remove pg-serverdpkg --build ./packages/pg-server-amd64-dpkgsudo apt-get install ./packages/pg-server-amd64-dpkg.deb
Проверяем, что файлы реально встали на свои места:
cat /etc/pg-server/config.yamlport: 3031ls /usr/bin/pg-server/usr/bin/pg-server
Подключаем systemd
Сервер ставится и работает, но у нас же сервер — хочется, чтобы он управлялся через systemctl. Юнит pg-server.service у нас уже есть — мы показали его в «Дано». Кладём его туда, где systemd ищет юниты, — по пути из дерева выше:
# Кладём юнит в пакетmkdir -p packages/pg-server-amd64-dpkg/lib/systemd/system/cp pg-server.service packages/pg-server-amd64-dpkg/lib/systemd/system/pg-server.service
Пересобирать пакет уже сейчас не будем — сделаем это вместе со скриптами в следующем разделе.
Автозапуск: maintainer-скрипты
Ранее мы упоминали, что наш сервер должен автоматически запускаться сразу же после установки и корректно останавливаться после удаления пакета — не зря же мы положили в пакет systemd-юнит.
Здесь нам помогут maintainer-скрипты: preinst (pre-install), postinst (post-install), prerm (pre-remove) и postrm (post-remove). Названия говорят сами за себя — они выполняются до и после установки/удаления соответственно. В нашем пакете мы будем использовать postinst и prerm; preinst и postrm упомянули для полноты картины.
Использовать эти файлы нужно аккуратно: установка пакета происходит от root, а значит и скрипты выполняются от root — в этот момент у нас в руках вся система.
В нашем случае мы сразу после установки запустим наш сервер (postinst), а при удалении выполним мягкий стоп (prerm):
vi ./packages/pg-server-amd64-dpkg/DEBIAN/postinst#!/bin/shsystemctl --system daemon-reloadsystemctl enable pg-serversystemctl start pg-server
vi ./packages/pg-server-amd64-dpkg/DEBIAN/prerm#!/bin/shsystemctl stop pg-serversystemctl --system daemon-reload
dpkg не исправляет права сам — скрипты должны быть исполняемыми, иначе установка упадёт:
chmod 755 ./packages/pg-server-amd64-dpkg/DEBIAN/postinst ./packages/pg-server-amd64-dpkg/DEBIAN/prerm
Мы изменили содержимое пакета, но разбором обновления пакетов займёмся позже, поэтому, как и раньше, просто удалим пакет, пересоберём его и поставим заново:
sudo apt-get remove pg-serverdpkg --build ./packages/pg-server-amd64-dpkgsudo apt-get install ./packages/pg-server-amd64-dpkg.deb
Теперь проверим наш сервер и убедимся, что он автоматически запустился сразу же после установки:
sudo systemctl status pg-server● pg-server.service - PostgreSQL version HTTP server Loaded: loaded (/lib/systemd/system/pg-server.service; enabled; vendor preset: enabled) Active: active (running) since Sun 2026-11-15 09:37:47 MSK; 40s ago Main PID: 1660198 (pg-server) Tasks: 1 (limit: 18676) Memory: 1.7M CPU: 17ms CGroup: /system.slice/pg-server.service └─1660198 /usr/bin/pg-server --config /etc/pg-server/config.yamlноя 15 09:37:47 ... systemd[1]: Started PostgreSQL version HTTP server.ноя 15 09:37:47 ... pg-server[1660198]: Listening on http://127.0.0.1:3031
Проверим теперь, что при удалении пакета сервер автоматически остановится. Вывод systemctl status здесь не поможет — юнит исчезнет вместе с пакетом, поэтому подсмотрим в журнал systemd:
sudo apt-get remove pg-serverjournalctl -u pg-server --no-pager | tail -n 3ноя 15 09:41:02 ... systemd[1]: Stopping PostgreSQL version HTTP server...ноя 15 09:41:02 ... systemd[1]: pg-server.service: Deactivated successfully.ноя 15 09:41:02 ... systemd[1]: Stopped PostgreSQL version HTTP server.
prerm отработал: в момент удаления сервер был корректно остановлен. А сам юнит исчез вместе с пакетом:
sudo systemctl status pg-serverUnit pg-server.service could not be found.
На этом ручная сборка закончена. Заодно проявилось главное свойство пакета: он — снапшот того, каким мы хотим видеть состояние файловой системы на целевой машине.
Как происходит обновление пакета
Ранее мы отмечали: если внести изменения в пакет и отдать клиенту обновлённый .deb, у него ничего не произойдёт. Клиенту пришлось бы либо явно переустановить пакет (apt-get install --reinstall ...), либо сначала удалить старый (apt-get remove pg-server) и только потом поставить новый — это крайне неудобно. В Linux обновление пакетов работает через версии: если версия пакета не изменилась, переустановка не произойдёт, даже если содержимое пакета изменилось.
Начнём с апгрейда. Изменим версию пакета в файле control, например, с 0.0.1 на 0.0.2 (Version: 0.0.2), и попробуем поставить его поверх уже установленного:
sudo apt-get install ./packages/pg-server-amd64-dpkg.debReading package lists... DoneBuilding dependency tree... DoneReading state information... DoneNote, selecting 'pg-server' instead of './packages/pg-server-amd64-dpkg.deb'The following packages were automatically installed and are no longer required: libc++1-14 libc++abi1-14 libunwind-14Use 'sudo apt autoremove' to remove them.The following packages will be upgraded: pg-server1 upgraded, 0 newly installed, 0 to remove and 112 not upgraded.Need to get 0 B/20,2 kB of archives.After this operation, 0 B of additional disk space will be used.Get:1 /home/xoore4ik/X0re4ik/projects/deb-cpp-study/packages/pg-server-amd64-dpkg.deb pg-server amd64 0.0.2 [20,2 kB](Reading database ... 418612 files and directories currently installed.)Preparing to unpack .../pg-server-amd64-dpkg.deb ...Unpacking pg-server (0.0.2) over (0.0.1) ... # <-- Смотреть сюдаSetting up pg-server (0.0.2) ...
apt сравнил версии, понял, что новая выше, и выполнил полноценный апгрейд.
А теперь попробуем поставить тот же самый пакет — с той же версией 0.0.2 — ещё раз:
sudo apt-get install ./packages/pg-server-amd64-dpkg.debReading package lists... DoneBuilding dependency tree... DoneReading state information... DoneNote, selecting 'pg-server' instead of './packages/pg-server-amd64-dpkg.deb'pg-server is already the newest version (0.0.2). # <-- Смотреть сюдаThe following packages were automatically installed and are no longer required: libc++1-14 libc++abi1-14 libunwind-14Use 'sudo apt autoremove' to remove them.0 upgraded, 0 newly installed, 0 to remove and 112 not upgraded.
Версия не изменилась — и dpkg даже не стал распаковывать содержимое, хотя внутри пакета мы могли что-то поменять. Именно поэтому после изменения пакета нужно поднимать его версию в control.
Как вести Depends
Все это время мы не замечали слона в комнате: ещё на первом шаге мы намеренно не указали Depends: — список библиотек, от которых зависит наше приложение. На моей машине, как и на вашей, все необходимые библиотеки уже установлены, поэтому всё работало. Но если перенести пакет на чистую систему, получим неприятный сюрприз. Проверим в изолированной ubuntu через docker:
docker run --rm -it -v "$PWD/packages:/packages:ro" ubuntu:22.04 bash
И внутри контейнера поставим наш пакет:
apt-get update && apt-get install /packages/pg-server-amd64-dpkg.deb
Установка прошла успешно. Пробуем запустить сервер:
/usr/bin/pg-server --config /etc/pg-server/config.yaml/usr/bin/pg-server: error while loading shared libraries: libpq.so.5: cannot open shared object file: No such file or directory
Пакет встал, но приложение не запускается. Всё потому, что наш сервер зависит от двух внешних библиотек — libpq и yaml-cpp — которые на чистой машине просто отсутствуют. Нам повезло, что на нашей машине они были.
Узнаем, от чего именно зависит наш бинарник:
objdump -p ./pg-server | grep "NEEDED" NEEDED libpq.so.5 NEEDED libyaml-cpp.so.0.7 NEEDED libstdc++.so.6 NEEDED libgcc_s.so.1 NEEDED libc.so.6
Список NEEDED — это и есть содержимое Depends: в терминах deb-пакета. Причём libstdc++, libgcc_s и libc есть в любой системе — их можно не указывать. А вот libpq.so.5 и libyaml-cpp.so.0.7 нужно обеспечить.
Где физически лежит библиотека, подскажет ldconfig — он хранит кэш того, что видит линковщик:
sudo ldconfig -p | grep libpq.so.5 libpq.so.5 (libc6,x86-64) => /lib/x86_64-linux-gnu/libpq.so.5
Осталось понять, какому пакету принадлежит файл — это умеет dpkg, он хранит список файлов каждого установленного пакета:
dpkg -S /usr/lib/x86_64-linux-gnu/libpq.so.5libpq5dpkg -S /usr/lib/x86_64-linux-gnu/libyaml-cpp.so.0.7libyaml-cpp0.7
Имена пакетов найдены — вписываем их в наш control:
Package: pg-serverSection: utilsPriority: optionalVersion: 0.0.2Maintainer: Mochalov Anton Architecture: amd64Depends: libpq5, libyaml-cpp0.7Description: Учимся собирать свой простой deb-пакет
Пересобираем и ставим на чистой машине ещё раз:
apt-get install /packages/pg-server-amd64-dpkg.debReading package lists... DoneBuilding dependency tree... DoneNote, selecting 'pg-server' instead of '/packages/pg-server-amd64-dpkg.deb'The following additional packages will be installed: libldap-2.5-0 libldap-common libpq5 libsasl2-2 libsasl2-modules libsasl2-modules-db libyaml-cpp0.7The following NEW packages will be installed: libldap-2.5-0 libldap-common libpq5 libsasl2-2 libsasl2-modules libsasl2-modules-db libyaml-cpp0.7 pg-server0 upgraded, 8 newly installed, 0 to remove and 2 not upgraded.
Бинго! apt прочитал Depends: из control и сам дотянул недостающее — среди новых пакетов наши libpq5 и libyaml-cpp0.7, а libldap и libsasl — это уже зависимости самой libpq. Сервер теперь запускается и на чистой системе.
Но вписывать зависимости руками — скучно и ненадёжно: забыл одну библиотеку — и пакет сломается на чужой машине. Логичный вопрос: можно ли автоматизировать и это? Можно — об этом следующая глава.
Автоматическая сборка проекта при помощи dpkg-buildpackage
Конечно, всё то, что мы делали ранее руками, так уже никто не делает — мы прошли этот путь, чтобы понять, что никакой магии в работе пакетов нет. Современные инструменты заточены под автоматизацию сборки пакетов. Одним из самых популярных инструментов является dh_make. О нём есть несколько статей на habr, например тут и тут. Я не хотел бы рассказывать о его работе и тонкостях настройки, так как статьи выше сделали это за меня. Я лишь покажу, что всё то, что мы создавали руками, умеет автоматизировать dh_make.
Для начала поставим инструменты: dh_make генерирует заготовку пакета, debhelper — набор помощников, которые эту заготовку собирают, а debuild — обёртка над dpkg-buildpackage, которая управляет всей сборкой:
sudo apt-get install -y dh-make debhelper devscripts
Создаём наш debian-пакет:
dh_make -p pg-server_0.0.1 --createorig
По итогу получим следующую структуру, именно здесь мы и будем описывать наши systemd-юниты, пост/пре команды и документацию:
.└── debian ├── changelog ├── control ├── copyright ├── manpage.1.ex ├── manpage.md.ex ├── manpage.sgml.ex ├── manpage.xml.ex ├── pg-server.cron.d.ex ├── pg-server.doc-base.ex ├── pg-server-docs.docs ├── postinst.ex ├── postrm.ex ├── preinst.ex ├── prerm.ex ├── README.Debian ├── README.source ├── rules ├── salsa-ci.yml.ex ├── source │ └── format └── watch.ex
Файлы с расширением .ex — это заготовки-примеры (manpage, cron, ci-конфиг и прочее). Они не используются, пока вы сами не переименуете их без суффикса, поэтому спокойно удаляем всё лишнее и оставляем только то, что нам действительно нужно.
Посмотрим на debian/control. Он устроен чуть иначе, чем наш ручной манифест, — разделён на две секции:
-
Source — исходный проект и требования для его сборки;
-
Package — описание устанавливаемого бинарного пакета.
Source: pg-serverSection: netPriority: optionalMaintainer: Student Developer Build-Depends: debhelper-compat (= 13), g++, make, pkg-config, libpq-dev, libyaml-cpp-devStandards-Version: 4.6.0Rules-Requires-Root: noPackage: pg-serverArchitecture: anyDepends: ${shlibs:Depends}, ${misc:Depends}Description: Small HTTP server reporting the libpq version A small educational HTTP server written in C++. It reads the listening port from a YAML configuration file and reports the PostgreSQL client library version.
Здесь Build-Depends перечисляет то, что нужно для сборки пакета (компилятор, заголовочные файлы библиотек), а вот Depends мы указываем не руками, а заготовками-переменными:
Depends: ${shlibs:Depends}, ${misc:Depends}
В момент сборки dpkg-shlibdeps просканирует наш бинарник, выяснит, с какими библиотеками он слинкован, найдёт пакеты-владельцы этих библиотек и запишет список в файл ./debian/pg-server.substvars. А debhelper в конце подставит этот список на место ${shlibs:Depends} в итоговом control. Важный нюанс: если бы в debian/control не было ${shlibs:Depends}, ничего бы само не вписалось — переменная должна быть объявлена заранее. Это не магия, а простая подстановка.
Но откуда dpkg-shlibdeps знает, где лежит наш бинарник? Из debian/pg-server.install — файла, который описывает, какой файл в какой каталог устанавливать, по шаблону:
исходный_файл каталог_назначения_в_пакете
В нашем случае:
vi ./debian/pg-server.installpg-server /usr/bin
Обратите внимание: бинарник кладётся в /usr/bin — это та самая конвенция Debian, о которой мы говорили в «Дано»: содержимое пакетов ставится в /usr, а /usr/local остаётся территорией ручной установки администратором. Заодно эта же строка отвечает и на вопрос, как dpkg-shlibdeps найдёт бинарник для сканирования.
За файл lib/systemd/system/pg-server.service отвечает отдельный специализированный файл — debian/pg-server.service: debhelper увидит его и сам положит юнит в пакет. Юнит нам уже знаком — он лежал у нас в проекте с самого начала, поэтому просто копируем:
cp pg-server.service ./debian/pg-server.service
Содержимое debian/rules — правила сборки. Именно здесь определяется, что и в какой последовательности выполняется:
vi ./debian/rules#!/usr/bin/make -fexport DEB_BUILD_MAINT_OPTIONS = hardening=+allinclude /usr/share/dpkg/buildflags.mk%:dh $@ --buildsystem=makefileoverride_dh_auto_install:
Пустой override_dh_auto_install — не случайность: он отключает дефолтный make install, и тогда debhelper раскладывает файлы по описанию из pg-server.install, а не по тому, что прописано в нашем Makefile.
Остался ещё один файл — debian/changelog. И он важнее, чем кажется: в этой схеме версию пакета задаёт именно он, а не Version: в control. Проверить, какую версию увидит сборщик, можно так:
dpkg-parsechangelog -S Version
Заметьте: сборщик здесь очень капризный и ругается на любое отличие от эталонного формата changelog.
Нам остаётся выполнить debuild -us -uc -b и наблюдать магию:
debuild -us -uc -b
Итоговая структура проекта будет следующая:
./debian/pg-server/├── DEBIAN│ ├── control│ ├── md5sums│ ├── postinst│ ├── postrm│ └── prerm├── lib│ └── systemd│ └── system│ └── pg-server.service└── usr ├── bin │ └── pg-server └── share └── doc └── pg-server ├── changelog.Debian.gz ├── copyright └── README.Debian
И здесь мы видим, что debuild сделал всё то, что ранее делали мы сами — он аккуратно разложил файлы в нужные каталоги, самостоятельно вписал Depends в control-файл — пакет создан и готов к запуску.
Как скачать наш дистрибутив с удаленного репозитория
На протяжении этого небольшого урока мы собирали пакет на машине и на ней же его устанавливали. Но нам, конечно же, хочется разместить наш пакетик в репозитории артефактов и раздавать оттуда. Для этого в проекте есть docker-compose.yml с запуском nexus: Часть разворачивания nexus и его настройки опустим, ведь есть прекрасная статья на эту тему здесь. Условимся, что на данном этапе вы запустили nexus локально на порту :8081, создали репозиторий артефактов apt и пользователя для скачивания дистрибутивов.
docker compose up -d
Теперь наш пакет должен появиться в репозитории. Проще всего залить его через UI: в Nexus откроем Upload → выберем наш apt-репозиторий (my-debian) → добавим pg-server-amd64-dpkg.deb → Upload.
Чтобы пакет стал доступен apt-get, сообщим системе адрес нашего репозитория. Список репозиториев apt берёт из /etc/apt/sources.list.d/ — положим туда файл nexus.list:
echo 'deb [trusted=yes] http://localhost:8081/repository/my-debian/ jammy main' | sudo tee /etc/apt/sources.list.d/nexus.list
Конечно же, для наших приватных пакетов нужны логин и пароль. Для этого в Linux есть специальная папка: /etc/apt/auth.conf.d/. При попытке скачать какой-либо дистрибутив Linux найдёт подходящий хост среди файлов папки /etc/apt/auth.conf.d/:
sudo vi /etc/apt/auth.conf.d/nexus.confmachine localhost:8081login password
Теперь можем смело выполнять sudo apt-get update — Linux заглянет в наш репозиторий и сохранит список доступных пакетов, после чего нам станет доступен apt-get install pg-server. Проверим заветную команду на чистой машине:
sudo apt-get updatesudo apt-get install pg-server
Пакет, который мы час назад собирали руками, ставится из нашего репозитория одной командой — без пути к .deb, без remove перед установкой и на любой машине, у которой есть доступ к Nexus.
Благодарность
Вот и всё! Спасибо большое, что дошли до этой главы, — значит, что-то интересное всё-таки было 🙂 Теперь мы с Вами знаем чуть больше про магию deb-пакетов в Linux.
Спасибо большое за прочтение этой скромной статьи, буду рад комментариям под постом 🙌
Источник: habr.com