Skip to content

Repository files navigation

Рекомендации и пояснения

Сервис

  • Для создания mqtt клиента используйте MQTTClient (на базе paho) из библиотеки wb_common. Оба варианта запуска показаны в main.py. Функция клиента start() по умолчанию запускает его в отдельном потоке, так сделан класс ThreadedServiceTemplate. Создание отдельного потока можно выключить и запускать клиента вручную через start() + loop_forever() в текущем потоке, так сделан класс OneThreadServiceTemplate. Использовать функцию loop() для работы не рекомендуется, тк она не делает автоматический реконнект в случае разрыва соединения с брокером.

  • Для перехвата сигналов используйте библиотеку signal: signal.SIGTERM (остановка через systemctl stop) и signal.SIGINT (остановка через Ctrl-C в консоли). В обработчике сигнала нужно вызвать stop(). Если вы используете loop_forever(), то в обработчике не нужно принудительно завершать основной процесс, выполнение после обработки сигнала вернётся в loop_forever(), которая завершится. Больше информации и примеров http://www.steves-internet-guide.com/client-connections-python-mqtt/.

  • Запуск. Весь основной код оформляется отдельным python-пакетом. В пакете создается основной файл (например main.py или по имени сервиса), в нем — функция main, которая может быть как импортирована в другой код, так и запущена при обычном запуске файла из консоли. В папке bin/ создается обёртка, которая импортирует и запускает main из пакета.

    Обёртку в bin/ делаем всегда, даже если сервису не нужен CLI. Причин три. Единообразие — любой сервис WB запускается одинаково и его точка входа лежит в предсказуемом месте. Готовое место под --version, которую мы добавляем каждому сервису. И запас на будущее — если из сервиса понадобится сделать настоящую утилиту с флагами, пакетирование переделывать не придется.

  • --version. Каждый сервис умеет печатать свою версию. Флаг регистрируется там же, где живет парсер, через _PrintVersionAction — версия читается только когда флаг реально передан, поэтому сервис, запущенный из исходников без установленного пакета, работает как раньше. Делаем именно --version без короткой формы, потому что -v в наших утилитах часто занят под verbose. Сам version.py про argparse не знает ничего и остается пригодным для пакетов вообще без CLI.

Версия пакета

Источник версии один — debian/changelog. Дальше она расходится так.

  1. setup.py при сборке зовет get_version_from_changelog() и кладет версию в метаданные python-пакета. Суффикс бранчевой сборки (~exp~...) при этом отрезается, потому что PEP 440 символ ~ не разрешает.
  2. Код в рантайме зовет get_version(), который читает версию из метаданных установленного пакета через importlib.metadata. Искать файлы рядом с собой не нужно, поэтому одна и та же строка доступна и CLI, и сервису, и любому библиотечному коду. Имя дистрибутива берется из __package__ — по спецификации нормализации имен серии точек и подчеркиваний равны дефису, поэтому wb.python_service_template находит метаданные wb-python-service-template.
  3. tests/test_version.py проверяет ту же строку из метаданных. Тест намеренно не читает changelog сам — во время сборки pytest работает из .pybuild/<интерпретатор>/build, где каталога debian/ нет, а через метаданные тест заодно покрывает get_version(), то есть ту функцию, которой пользуется реальный код.

Метаданные пакета появляются только на стадии install, уже после тестов. Поэтому в debian/rules стоит хук PYBUILD_BEFORE_TEST, который создает их в сборочном каталоге перед тестами. Убирать их после не нужно, pybuild удаляет метаданные из сборочного каталога сам перед установкой.

--version печатает версию релиза, без суффикса бранчевой сборки. Полная deb-версия всегда видна в dpkg -l и в диагностическом архиве.

Тесты

Тесты запускает pybuild на шаге dh_auto_test, командой pytest tests из сборочного каталога.

python3-pytest в Build-Depends обязателен, и он там не для того, чтобы запускать тесты руками. Именно по наличию этого пакета pybuild выбирает pytest, а без него молча уходит на unittest discover, и тесты в стиле голых функций просто не соберутся.

Доктесты включены по умолчанию флагом --doctest-modules в pytest.ini. Пример — parse_changelog_version() в version.py, где примеры вызова документируют разбор строки changelog внятнее, чем это сделал бы комментарий. Доктесты уместны ровно там, где пример и есть самая понятная документация функции, и падают вместе со сборкой, если поведение разошлось с примером.

Из-за --doctest-modules на сборке импортируется каждый модуль пакета, а не только те, которые тянут за собой тесты. Отсюда два требования к любому модулю под wb/. Первое, все его зависимости должны стоять в Build-Depends, а не только в Depends. Второе, на уровне модуля не должно быть кода, который что-то делает при импорте, то есть читает конфиг, создает клиента или ходит в сеть, потому что на сборщике ни конфига, ни сети нет. Нарушение любого из двух роняет сборку на стадии сбора тестов.

Прогоняться должны все тесты вместе с доктестами, и одинаково в двух местах — на сборке и локально командой pytest без параметров из корня проекта. Ради этого в addopts перечислены оба каталога. Без wb доктесты не достают до модулей пакета, потому что явный путь tests от pybuild перебивает testpaths. Без tests ломается обратное, при запуске без параметров единственным корнем сбора становится wb и обычные тесты выпадают из прогона. Оба пути резолвятся от каталога запуска, поэтому звать pytest надо из корня. pythonpath = . нужен, чтобы пакет вообще импортировался — голый pytest не кладет текущий каталог в sys.path, в отличие от python -m pytest. Он резолвится от каталога с pytest.ini, а его pybuild копирует в сборочный каталог, поэтому строка верна и локально, и на сборке.

Локально перед pytest нужен один шаг, python3 setup.py --quiet egg_info. Тесты версии читают метаданные пакета, а вне сборки их никто не создает, поэтому без этого шага тесты версии падают. На сборке то же самое делает хук PYBUILD_BEFORE_TEST из debian/rules.

Проверки, у которых нет потребителя в рабочем коде, живут в тестах, а не в пакете. Поэтому регулярное выражение для формата версии лежит в tests/test_version.py, а в version.py такой функции нет намеренно.

Автодополнение в bash

Если у сервиса появляется CLI с подкомандами, к нему стоит положить файл автодополнения. Пример — wb-homeui-gates.bash в homeui, он сгенерирован один раз утилитой shtab по argparse-парсеру и закоммичен как есть.

Файл кладется в completions/<имя-утилиты>.bash, а в debian/<пакет>.install добавляется строка

completions/wb-python-service-template.bash /usr/share/bash-completion/completions

В самом шаблоне такого файла нет намеренно. У шаблонного CLI всего два флага, дополнять в них нечего. shtab тоже подключаем только когда у утилиты действительно много подкоманд и опций, иначе к сборке добавляется зависимость и шаг генерации ради пары строк.

Типы и докстринги

Аннотации типов обязательны — у всех параметров и у всех возвращаемых значений, включая -> None. Требование записано в codestyle.

Докстринг пишем там, где он говорит то, чего нет в имени и подписи — контракт возвращаемого значения, выбор между несколькими вариантами, неочевидное поведение. Докстринг, пересказывающий имя функции, читать только мешает, поэтому в шаблоне их нет ни у MQTT-колбэков, ни у обработчиков сигналов. Есть у обоих классов-примеров, потому что из имени не видно, когда какой брать, у их run(), потому что он возвращает код выхода, и у main().

Форма одна, многострочная. """ на отдельной строке, за ней суммарная фраза, при необходимости пустая строка и подробности, дальше секции в стиле Google (Args:, Returns:, Raises:). Однострочную форму """Текст.""" не используем, даже когда описание помещается в строку.

Пакетирование

Исходная система - где происходит сборка deb-пакета. В терминологии debian - build-система Целевая система - куда устанавливается пакет. В терминологии debian - host-система

Для сборки deb-пакетов в Wiren Board используется кросс-компиляция. Это значит, что сборка будет происходить на машине с архитектурой, отличной от той, на которой всё будет запускаться, и нужно подсказать сборщику, как правильно разрешать зависимости.

  • Собрать пакет вручную и посмотреть что получается можно внутри devcontainer-a командой

    dpkg-buildpackage -rfakeroot -us -uc
    

    Директория debian в проекте (исходная система) будет содержать структуру директорий относительно корня целевой системы. Перед сборкой не забыть установить build-depends.

  • /debian/control для Python

    Для source пакета в Build-Depends нужно использоватьpython3-all вместо python3, потому что это отвязывает процедуру сборки от конкретной реализации Python, и distutils автоматически подтягивается с подходящей версией. Писать python3-all:any в пакетах-библиотеках обычно не имеет смысла, тк используем arch-all.

    Source: wb-my-lib
    ...
    Build-Depends: python3-all, dh-python, ...
    

    Для binary пакетов

    Package: wb-my-lib
    Architecture: all
    Depends: ${python3:Depends}, ${misc:Depends}, ...
    

    Architecture: all (не путать с Architecture: any) нужен для того, чтобы собранный пакет был платформо-независимый. python3 в зависимости не записывается, его заменяет ${python3:Depends}.

  • Кодогенерация при кросс-компиляции

    У нас этот сценарий применяется в wb-mqtt-serial. Кодогенерация там - сборка шаблонов с помощью j2cli. Для того чтобы установить j2cli, совместимый с архитектурой сборщика, используем :native.

    Source: wb-mqtt-serial
    ...
    Build-Depends: ..., j2cli:native
    
  • Jenkinsfile

    В шаблоне вызов пайплайна закомментирован намеренно. Иначе Jenkins собирал бы сам шаблон и публиковал его в релизный фид, где шаблону не место. В настоящем пакете эти строки надо раскомментировать.

    buildDebArchAll defaultRunPythonChecks: true,
                    defaultRunLintian: true,
                    defaultAngryPylint: true

    defaultRunPythonChecks добавляет стадию проверок кода. На ней по всем файлам питона в репозитории проходят black, isort и pylint с конфигами из codestyle.

    defaultRunLintian добавляет стадию после сборки, на которой готовый deb проверяет дебиановский линтер lintian с профилем WB из того же codestyle. Он смотрит на упаковку, а не на код, и валит стадию на замечаниях уровня error.

    defaultAngryPylint делает ошибки pylint фатальными для сборки. Без него они попадают только в лог, а стадия остаётся зелёной.

  • Резервное копирование конфигов

    Сервис wb-configs-early умеет делать резервное копирование файлов перед началом работы системы и перед выключением. Он перемещает сам файл конфига в раздел, который не сбрасывается при установке обновлений, а сам конфиг в исходном расположении /etc/ заменяет на симлинк. Для этого нужно в папку /etc/wb-configs.d положить скрипт, который вызовет wb_move для всех нужных конфигов (в данном примере это 10wb-python-service-template).

FAQ по сборке и пакетированию для продвинутых

  • Почему all-пакеты не собираются в sbuild с --host=armhf?

    Альтернативная формулировка - почему all-пакеты нельзя было раньше собрать через wbdev cdeb.

    Во-первых, потому что в Debian обычно делят сборку all-пакетов и кросс-компиляцию, вот (пруф).

    Во-вторых, это могло бы заработать, если бы мейнтейнеры Python-библиотек в Debian озадачивались добавлением Multi-Arch: foreign в свои пакеты. На ноябрь 2022 этой пометки нет как минимум в python3-paho-mqtt и python3-jinja2, а эти пакеты мы часто используем.

    В итоге оказалось проще пойти каноничным путём и собирать all и any отдельно.

  • Для чего при сборке cdeb запускаются и --arch-all, и --arch-any?

    Раньше для сборки пакетов отдельно использовались wbdev cdeb (для any) и wbdev ndeb (для all). Это было очень актуально во времена wheezy, когда не было ещё multiarch, и пакеты для Wiren Board приходилось собирать в chroot с qemu. Тогда для экономии времени сборки (qemu медленный) сделали отдельно команду ndeb, которой собирали all-пакеты прямо в Docker-окружении.

    Потом мы перешли на sbuild, и разница между cdeb и ndeb стала размываться, потому что для обеих команд уже использовалось sbuild-окружение, только с разным аргументом --host.

    Так получилось, что при переходе на релизную систему, когда для сборки стало нужно указывать, для какой платформы происходит сборка (wb5/wb6/wb7), добавление наших репозиториев (http://deb.wirenboard.com/wbX/<deb-release>) делалось только в cdeb. Для ndeb появился репозиторий dev-tools, который нужен скорее для сборки пакетов, нужных нам на производстве и для CI.

    Пока наши all-пакеты не зависели друг от друга при сборке, можно было продолжать пользоваться такой системой, выбирая в зависимости от типа пакета cdeb или ndeb. При разработке wb-device-manager нам понадобились all-зависимости из нашего репозитория.

    В этот момент @webconn решил, что пора объединять cdeb и ndeb - так получается меньше пайплайнов для CI и не надо дублировать код в devenv. Также меньше возможностей ошибиться при сборке - скрипты сами сделают всё, что нужно.

    ndeb с этого момента стал deprecated, о чём пишется WARNING.

  • Почему в Python-библиотеках не пишем Multi-Arch в пакетах?

    Как минимум потому что это не делают даже в Debian.

Перед контрибьютом

Перед тем как открывать PR, убедитесь, что:

  1. Код проходит тесты (из корня проекта python3 setup.py --quiet egg_info && pytest);
  2. Концы строк во всех файлах остаются LF (это обеспечивает .gitattributes);
  3. Пакет успешно собирается и на выходе получается deb-файл;
  4. Пакет проверен на реальном контроллере:
    • Устанавливается;
    • После установки импортируется namespace-пакет;
    • Пакет сообщает свою версию по --version;
    • Сервис запускается сам после установки;
    • Сервис автоматически стартует после перезагрузки контроллера;
    • Пакет удаляется.

Проверка сборки на контроллере

Выполняется на самом Wiren Board (арх all, подойдёт любой). Репозитории WB на контроллере должны быть настроены — из них тянется python3-wb-common.

# 1. Инструменты сборки (один раз на контроллер)
sudo apt update
sudo apt install -y build-essential devscripts equivs git

# 2. Копия из своей ветки
GIT_BRANCH_NAME="feature/branch-name"
git clone -b "${GIT_BRANCH_NAME}" --single-branch \
  https://github.com/wirenboard/wb-python-service-template.git
cd wb-python-service-template

# 3. Build-зависимости из debian/control и сборка пакета
sudo mk-build-deps -ir debian/control
dpkg-buildpackage -us -uc -b

Далее нужно проверить что пакета корректно собран и сервис поднимается сам

# 1. Установка собранного пакета
sudo apt install -y ../wb-python-service-template_*.deb

# 2. Проверка импорта namespace-пакета и версии из метаданных пакета
python3 -c "import wb.python_service_template.main as m; print('import OK:', m.main)"
wb-python-service-template --version              # ожидается: версия из debian/changelog

# 3. Проверка работы сервиса
systemctl is-enabled wb-python-service-template   # ожидается: enabled
systemctl status wb-python-service-template       # ожидается: active (running)

# 4. Проверка автозапуска после перезагрузки
sudo reboot
# после загрузки снова зайти по SSH и проверить, что сервис поднялся сам:
systemctl status wb-python-service-template       # ожидается: active (running)

# 5. Удаление после проверки
sudo apt purge -y wb-python-service-template

При успешной сборке pytest прогонит и тесты из tests/, и доктесты из модулей пакета (шаг сборки dh_auto_test), а после установки сервис включится автоматически благодаря секции [Install] в юните.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Contributors

Languages