Зачем всё это
Привет! Вторая статья на 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=simple
ExecStart=/usr/bin/pg-server --config /etc/pg-server/config.yaml
DynamicUser=yes
NoNewPrivileges=yes
Restart=on-failure
RestartSec=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/control
touch packages/pg-server-amd64-dpkg/DEBIAN/postinst
touch packages/pg-server-amd64-dpkg/DEBIAN/prerm
packages/pg-server-amd64-dpkg/DEBIAN/control - здесь хранится манифест нашего проекта:
Package: pg-server
Section: utils
Priority: optional
Version: 0.0.1
Maintainer: Mochalov Anton <xoore4ik@gmail.com>
Standards-Version: 3.9.4
Architecture: amd64
Description: Учимся собирать свой простой deb-пакет
Мы задаём название проекта, владельца пакета, архитектуру и описание. Разбор postinst, prerm и юнита systemd оставим на потом. Как только файл control готов, мы уже можем собрать наш deb-пакет - хоть изначально он и будет бесполезен:
dpkg --build ./packages/pg-server-amd64-dpkg
sudo 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-server
dpkg --build ./packages/pg-server-amd64-dpkg
sudo apt-get install ./packages/pg-server-amd64-dpkg.deb
Проверяем, что файлы реально встали на свои места:
cat /etc/pg-server/config.yaml
port: 3031
ls /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/sh
systemctl --system daemon-reload
systemctl enable pg-server
systemctl start pg-server
vi ./packages/pg-server-amd64-dpkg/DEBIAN/prerm
#!/bin/sh
systemctl stop pg-server
systemctl --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-server
dpkg --build ./packages/pg-server-amd64-dpkg
sudo 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-server
journalctl -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-server
Unit 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.deb
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Note, 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-14
Use 'sudo apt autoremove' to remove them.
The following packages will be upgraded:
pg-server
1 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.deb
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Note, 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-14
Use '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.5
libpq5
dpkg -S /usr/lib/x86_64-linux-gnu/libyaml-cpp.so.0.7
libyaml-cpp0.7
Имена пакетов найдены - вписываем их в наш control:
Package: pg-server
Section: utils
Priority: optional
Version: 0.0.2
Maintainer: Mochalov Anton <xoore4ik@gmail.com>
Architecture: amd64
Depends: libpq5, libyaml-cpp0.7
Description: Учимся собирать свой простой deb-пакет
Пересобираем и ставим на чистой машине ещё раз:
apt-get install /packages/pg-server-amd64-dpkg.deb
Reading package lists... Done
Building dependency tree... Done
Note, 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.7
The following NEW packages will be installed:
libldap-2.5-0 libldap-common libpq5 libsasl2-2 libsasl2-modules libsasl2-modules-db libyaml-cpp0.7 pg-server
0 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-server
Section: net
Priority: optional
Maintainer: Student Developer <student@example.com>
Build-Depends:
debhelper-compat (= 13),
g++,
make,
pkg-config,
libpq-dev,
libyaml-cpp-dev
Standards-Version: 4.6.0
Rules-Requires-Root: no
Package: pg-server
Architecture: any
Depends:
${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.install
pg-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 -f
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
include /usr/share/dpkg/buildflags.mk
%:
dh $@ --buildsystem=makefile
override_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.conf
machine localhost:8081
login <NEXUS-USER>
password <NEXUS-PASSWORD>
Теперь можем смело выполнять sudo apt-get update - Linux заглянет в наш репозиторий и сохранит список доступных пакетов, после чего нам станет доступен apt-get install pg-server. Проверим заветную команду на чистой машине:
sudo apt-get update
sudo apt-get install pg-server
Пакет, который мы час назад собирали руками, ставится из нашего репозитория одной командой - без пути к .deb, без remove перед установкой и на любой машине, у которой есть доступ к Nexus.
Благодарность
Вот и всё! Спасибо большое, что дошли до этой главы, - значит, что-то интересное всё-таки было :) Теперь мы с Вами знаем чуть больше про магию deb-пакетов в Linux.
Спасибо большое за прочтение этой скромной статьи, буду рад комментариям под постом 🙌

Комментарии 0