Русский
Русский
English
Статистика
Реклама

Makefile

Перевод Как я сломал и починил кластер Kubernetes, работающий на Raspberry Pi

21.04.2021 20:21:36 | Автор: admin

Я доигрался с обновлениями и это привело к катастрофе: все ноды внезапно перестали видеть сетевые интерфейсы, и, сколько я ни бился, оживить кластер у меня не получилось.

Мой домашний кластерёнок вырос в зрелый кластер из шести нод (всё благодаря супруге, которая знала, что мне подарить на день рождения, естественно, Raspberry Pi, и не один!), и я встал перед выбором либо ещё раз выполнить собственные инструкции из статьи об установке кластера Kubernetes на Raspberry Pi, либо, применив системную инженерию (DevOps и SRE), полностью автоматизировать процессы переделки кластера и создания системы управления кластером. Эту статью можно считать прямым дополнением к моей первой статье Как я собрал домашний кластер Kubernetes на базе Raspberry Pi.


Соображения о времени и трудозатратах

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

Вариант 1

Встать с утра пораньше и повторить все операции вручную в соответствии с инструкциями из моей собственной статьи и, возможно, кое-что в процессе улучшить.

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

Затем всё чистится под ноль и начинается снова. Я проделывал это уже не единожды. Но, в конце концов, никому не будет вреда, если попытаться ещё раз.

Вариант 2

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

Конечно, для этого потребуется куда больше времени. Но светят и долгосрочные преимущества: мне больше не надо будет заботиться о самом кластере, я буду знать, что это решение стабильно и можно перенести на него всю свою домашнюю инфраструктуру.

Хроника автоматизации. Начало

Прежде всего я проработал основы и определил ряд принципов, которым следовал в течение всего процесса. Среда у меня получилась довольно необычная, однако с хорошим логическим разделением кода между её частями, так что вам будет несколько проще менять любые части кода или комментировать целые куски файлов, чтобы в файлах отключить те или иные функции.

Принцип 1. Настройка кластера Kubernetes на Raspberry Pi выполняется в три этапа: это настройка карты памяти, настройка нод на системном уровне и развёртывание ресурсов Kubernetes.

Принцип 2. На моём старом Intel NUC работает NFS-сервер, подключённый к хранилищу DROBO. Было бы заманчиво использовать его в качестве постоянного общего хранилища для всех нод.

Принцип 3. Кластер Raspberry Pi работает в моей домашней сети VLAN, поэтому вопросы безопасности меня волнуют не особо. Все службы и ноды должны быть легко доступны без всяких хитростей с именами и паролями.

Итак, держа всё это в голове, я приступил к программированию моего маленького Франкенштейна. Для повторения результатов (то есть для того, чтобы система заработала) вам понадобятся:

  • Mac (для форматирования карты). Если выберу время для установки Linux VM, попытаюсь обновить скрипт обнаружения платформы.

  • Ansible (я использовал версию 2.10.6).

  • Terraform (я использовал версию 0.13.4, но 0.14.8 тоже подойдёт).

  • Make, полезный инструмент, чтобы не колдовать над параметрами.

Кластер rPi. Первый шаг

В качестве жёсткого диска Raspberry Pi использует карту памяти. Возможно, это не вполне оптимальное и не самое быстрое решение для чтения и записи, но его должно быть достаточно для игр и любительских проектов.

Что происходит на первом шаге?

  • Форматируется карта памяти.

  • Карта памяти делится на два раздела: 1 ГБ плюс то, что останется.

  • Образ Alpine Linux копируется на карту памяти.

  • Создаётся системный оверлей.

Системный оверлей настраивает eth0 на "неразборчивый" (promisc) режим, это нужно для работы MetalLB, и разрешает подключение SSH к нодам Raspberry Pi без пароля.

Важно: проверьте источник 001-prepare-card.sh и убедитесь, что /dev/disk5 это именно вставленная карта памяти, иначе можно потерять данные.

Результат: подготовка шести карт памяти займёт около одной минуты.

Кластер rPi. Второй шаг

Начинается самое интересное. Итак, вы вставили в Raspberry Pi карты памяти, подсоединили все кабели (сетевые и питания) и загрузили систему. Теперь нужно получить IP-адреса устройств. Это можно сделать, либо подключив экран к каждому из них и запустив команду ifconfig eth0, либо зайдя в маршрутизатор и проверив информацию на нём. Введите в файл pi-hosts.txt соответствующие значения.

[masters]pi0 ansible_host=192.168.50.132 # Pi0[workers]pi1 ansible_host=192.168.50.135 # Pi1pi3 ansible_host=192.168.50.60  # Pi3pi4 ansible_host=192.168.50.36  # Pi4pi2 ansible_host=192.168.50.85  # Pi2pi5 ansible_host=192.168.50.230 # Pi5

Важно: для работы некоторых программ может потребоваться имя узла pi0.

Добавьте в файл ~/.ssh/config следующую строку, она даст root-доступ для всех нод с именами pi*.

Host pi?  User root  Hostname %h.local

Теперь наши микровычислители (вот видите, насколько я стар!) готовы, и нам нужно подготовить их к запуску Ansible. Это можно легко сделать с помощью скрипта 001-prepare-ansible.sh, который подключиться по ssh к каждому определённому в файле pi-hosts серверу, на каждом сервере настроит chrony для NTP и установит интерпретатор Python.

Важно: возможно, потребуется открыть файл rpi.yaml и изменить раздел vars в соответствии с вашими предпочтениями. Я поступил именно так.

После этого шага запускаем на Ansible команду ansible-playbook rpi.yaml -f 10, которая выполнит следующие действия:

ОБЩИЕ:

  • Установит необходимые пакеты.

  • Разобьёт на разделы и отформатирует карту памяти RPI.

  • Настроит параметры "большого" раздела как системного диска.

  • Добавит записи в файл fstab.

  • Подтвердит изменения.

  • Перезапустит Pi, чтобы тот загрузился с "постоянного" раздела.

KUBEMASTER:

  • Настроит мастер-ноду с помощью kubeadm.

  • Сохранит токены локально (в файле static/token_file).

  • Определит на Pi пользователя с правами root с доступом к kubectl.

  • Сохранит настройки Kubernetes локально (в файле static/kubectl.conf).

KUBEWORKER:

  • Скопирует токены на рабочие ноды.

  • После этого через файл токенов рабочие ноды будут присоединены к мастер-ноде.

  • Скопирует kubectl.conf на root-пользователей рабочих нод.

БАЗОВЕ:

  • Снимет отметку с мастер-ноды, что позволит ей принимать рабочие нагрузки.

  • Установит на ноды py3-pip, PyYaml и Helm.

Если вы дошли до этого места, мои поздравления! Вы только что создали базовый кластер Kubernetes, который делать пока ничего не умеет, но готов научиться, если уделить ему немного внимания. Всё оказалось довольно элементарно нужно просто запустить несколько скриптов и дождаться их завершения. Я считаю, что это определённо лучшее решение, чем возиться руками.

Важно: скрипты можно запускать сколько угодно раз. Переформатировать карты памяти после каждого раза не нужно.

Результат: подготовка шести нод с базовой установкой Kubernetes занимает пару минут, в зависимости от скорости подключения к Интернету.

Кластер rPi. Третий шаг

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

Вначале посмотрим на конфигурацию.

# Variables used for barebone kubernetes setupnetwork_subnet    = "192.168.50"net_hosts = {  adguard = "240"  adguard_catchall = "249"  traefik = "234"  torrent_rpc = "245"}nfs_storage = {  general = "/media/nfs"  torrent = "/mnt/drobo-storage/docker-volumes/torrent"  adguard = "/mnt/drobo-storage/docker-volumes/adguard"}# ENV variable: TRAEFIK_API_KEY sets traefik_api_key# ENV variable: GH_USER, GH_PAT for authentication with private containers

Кластер запускается в сети по адресу 192.168.50.0/24, но по умолчанию MetalLB будет использовать "конец" пула сетевых адресов с адресами 200-250. Поскольку у меня есть домашний торрент-сервер и DNS от Adguard, мне нужно задать для них конкретные адреса. Также мне нужен обслуживающий дашборды и прочие инструменты балансировщик нагрузки Traefik.

Важные замечания:

Значения nfs_*_path должны быть совместимы с настройками, заданными на втором шаге.

Убедитесь, что в конфигурационный файл Kubernetes ~/.kube/config is добавлены данные из файла static/kubernetes.conf. В качестве контекстного имени я использую home-k8s.

Что делает Terraform?

Устанавливает flannel, а также патч конфигурационных параметров для host-gw; устанавливает metalLB и задает сетевые параметры var.network_subnet 200250.

Устанавливает прокси Traefik и открывает к нему доступ в домашней сети через балансировщик нагрузки metalLB. Доступ к самому дашборду Traefik осуществляется по traefik.local.

Дашборд Traefik запускается на кластере Pi.Дашборд Traefik запускается на кластере Pi.

Устанавливает DNS-службу Adguard с запросами к хранилищам данных (persistent volumes claims) с использованием NFS; открывает доступ к дашборду (adguard.local) через Traefik и к самой службе через выделенный в домашней сети IP-адрес.

Adguard Home запускается на кластере Pi.Adguard Home запускается на кластере Pi.

Устанавливает и развёртывает на всех нодах стек мониторинга Prometheus и Grafana. Вносит изменения в DaemonSet Prometheus, устраняя необходимость в монтировании томов. Также через Traefik определяет Grafana как grafana.local. Имя и пароль пользователя Grafana по умолчанию admin:admin. В Grafana уже есть предустановленный плагин devopsprodigy-kubegraf-app. Я считаю его лучшим для мониторинга кластеров.

На кластере Pi запускается дашборд Grafana.На кластере Pi запускается дашборд Grafana.

Устанавливает дашборд Kubernetes и через Traefik определяет его как k8s.local.

Дашборд Kubernetes запускается на кластере Pi.Дашборд Kubernetes запускается на кластере Pi.

Устанавливает и развёртывает торрент-сервер (rTorrent) с веб-интерфейсом Flood. Дашборд отображается как torrent.local. Для хранения данных (в том числе конфигурационных) дашборд использует множество точек монтирования. Причина, по которой значение репликации должно быть установлено в 1, объясняется просто. У rTorrent есть проблемы с файлами блокировки, и, поскольку эта программа использует разделяемый каталог, она просто не запустится, если будет обнаружен файл блокировки. У меня rTorrent настроен на прослушивание на порте 23340.

Поскольку Raspberry Pi запускается с карты памяти, эта карта из-за постоянных операций чтения-записи со временем может выйти из строя. Поэтому я решил регулярно делать резервные копии etcd на NFS. Программа резервного копирования запускается раз в сутки с параметрами, устанавливаемыми Terraform. Каждая резервная копия весит около 32 мегабайт.

Запуск Terraform

Чтобы несколько упростить задачу, я создал Makefile, который может оказаться полезным при настройке. Возможно, понадобится установить следующие переменные окружения:

TRAEFIK_API_KEY // Traefik API keyGH_USER // Github userGH_PAT // Github Personal Access Token

Важно: учётные данные Github сейчас не задействованы, но в ближайшее время я планирую добавить функцию аутентификации для извлечения частных образов из GHCR.

ADDITIONAL_ARGS=-var 'traefik_api_key=$(TRAEFIK_API_KEY)' -var "github_user=$(GH_USER)" -var "github_pat=$(GH_TOKEN)"apply:cd infrastructure; terraform apply $(ADDITIONAL_ARGS) -auto-approve -var-file ../variables.tfvarsplan:cd infrastructure; terraform plan $(ADDITIONAL_ARGS) -var-file ../variables.tfvarsdestroy:cd infrastructure; terraform destroy $(ADDITIONAL_ARGS) -var-file ../variables.tfvarsdestroy-target:cd infrastructure; terraform destroy $(ADDITIONAL_ARGS) -var-file ../variables.tfvars -target $(TARGET)refresh:cd infrastructure; terraform refresh $(ADDITIONAL_ARGS) -var-file ../variables.tfvarsinit:cd infrastructure; rm -fr .terraform; terraform initimport:cd infrastructure; terraform import $(ADDITIONAL_ARGS) -var-file ../variables.tfvars $(ARGS)lint:terraform fmt -recursive infrastructure/

Заключительные замечания

Полный код можно взять на GitHub. Им можно свободно пользоваться и менять как угодно (как обычно, ваши комментарии и замечания горячо приветствуются). Я также опубликовал переделанные образы Docker (rTorrent и Flood) с multiarch-архитектурой, поддерживающей процессоры ARM64. Я довольно часто вычищаю весь кластер и делаю сборку с нуля, используя упомянутый репозиторий, а по мере появления новых функций я буду вносить в него соответствующие изменения.

Автоматизация экономит время и нервы и надо признать! хорошо выполняет свою работу. Именно поэтому DevOps-инженеров так ценят на рынке (зарплаты с пятью нулями в месяц уже обычное дело). Если хотите так же приходите учиться, помимо современных учебных программ, у нас есть менторы из ведущих компаний Рунета, которые поделятся с вами своим практическим опытом.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Make на мыло, redo сила

02.09.2020 14:13:27 | Автор: admin
Приветствую! Хочу рассказать о главных, не всегда очевидных, недостатках системы сборки Make, делающих её часто не пригодной для использования, а также рассказать о прекрасной альтернативе и решении проблемы гениальнейшей по своей простоте, системе redo. Задумка известнейшего DJB, криптография которого где только не применяется. Лично меня, redo настолько впечатлил life-changing простотой, гибкостью и куда лучшим выполнением задач сборки, что я практически во всех своих проектах им полностью заменил Make (там где не заменил значит ещё руки не дошли), у которого я не смог найти ни одного преимущества или причины оставлять в живых.


Yet another Make?


Make много кого не устраивает, иначе не было бы десятков других систем сборки и десятков диалектов одного только Make. А redo это ещё одна очередная альтернатива? С одной стороны конечно же да только крайне простая, но способная решать абсолютно все те же задачи что и Make. С другой стороны а разве у нас есть какой-то общий для всех и единый Make?

Большинство альтернативных систем сборки рождалось потому что не хватало родных возможностей Make, не хватало гибкости. Многие системы занимаются только генерированием Makefile-ов, не производя сборку самостоятельно. Многие заточены под экосистему определённых языков программирования.

Ниже я постараюсь показать что redo является куда более заслуживающей внимания системой, не просто yet another решением.

Make всё равно всегда есть


Лично я всё равно всегда косо смотрел на всю эту альтернативу, ибо она или сложнее, или ecosystem/language-specific, или является дополнительной зависимостью которую нужно ставить и изучать как ею пользоваться. А Make это такая вещь, с которой плюс-минус все знакомы и умеют пользоваться на базовом уровне. Поэтому всегда и везде старался использовать POSIX Make, предполагая что это то, что в любом случае у каждого есть в (POSIX) системе из коробки, как например компилятор C. И задачи в Make выполнять только для которых он предназначен: распараллеливаемое выполнение целей (команд) с учётом зависимостей между ними.

В чём проблема просто писать на Make и быть уверенным что на любых системах это заработает? Ведь можно же (нужно!) писать на POSIX shell и не заставлять пользователей ставить какие-нибудь монструозные громадные GNU Bash. Проблема только в том, что работать будет только POSIX Make диалект, достаточно скудный даже для многих небольших простых проектов. Make в современных BSD системах более сложен и feature-full. Ну а с GNU Make мало с кем идёт в сравнение, хотя его возможностей по полной почти никто и не использует и не знает как ими пользоваться. Но GNU Make не поддерживает диалект современных BSD систем. А BSD системы не имеют GNU Make в своём составе (и их можно понять!).

Использовать BSD/GNU диалект значит потенциально заставлять пользователя всё равно ставить дополнительный софт, не идущий из коробки. В этом случае, возможное преимущество Make его наличие в системе, сводится на нет.

Использовать и писать на POSIX Make можно, но сложно. Лично у меня с ходу вспоминается два очень раздражающих случая:

  • Какие-то Make реализации при выполнении $(MAKE) -C переходят в директорию выполнения нового Make, а какие-то нет. Можно ли написать Makefile так, чтобы оно одинаково работало везде? Безусловно:

    tgt:    (cd subdir ; $(MAKE) -C ...)
    

    Удобно? Безусловно нет. И неприятно тем, что о подобных мелочах надо постоянно помнить.
  • В POSIX Make нет оператора выполняющего shell-вызов и его результат сохраняющий в переменную. В GNU Make до 4.x версии можно сделать:

    VAR = $(shell cat VERSION)
    

    а начиная с 4.x, а также в BSD диалектах можно выполнить:

    VAR != cat VERSION
    

    Не совсем аналогичным действием можно сделать:

    VAR = `cat VERSION`
    

    но оно буквально подставляет это выражение в ваши shell-команды описанные в целях. Этот подход применяют в suckless проектах, но это, конечно же, костыль.

Лично я в подобных местах часто писал Makefile-ы сразу под три диалекта (GNU, BSD и POSIX):

$ cat BSDmakefileGOPATH != pwdVERSION != cat VERSIONinclude common.mk$ cat GNUmakefileGOPATH = $(shell pwd)VERSION = $(shell cat VERSION)include common.mk

Удобно? Отнюдь! Хотя задачи крайне просты и распространены. Вот и выходит, что или:

  • Писать параллельно для нескольких диалектов Make. Размен времени разработчика на удобство пользователя.
  • Помня о множестве нюансов и мелочей, возможно с неэффективными подстановками (`cmd ...`), пытаться писать на POSIX Make. Лично для меня, с многолетним опытом с GNU/BSD Make, этот вариант самый трудозатратный (проще писать на нескольких диалектах).
  • Писать на одном из диалектов Make, заставляя пользователя ставить сторонний софт.

Технические проблемы Make


Но всё гораздо хуже от того, что любой Make не сказать что (хорошо) справляется с поставленными на него задачами.

  • mtime не даёт никаких гарантий, а Make оценивает свежесть целей исключительно по mtime, сравнивания его значение у выполненных целей. Если гранулярность временных штампов вашей файловой системы такая, что быстрый компьютер способен обновлять файлы быстрее, то Make будет бессилен понять изменение файлов. mtime не обязан монотонно возрастать! Обновлённый mtime также и не обязан быть ни больше, ни меньше, ни равным текущему времени! Как с mtime работают системы контроля версий по разному, но никаких гарантий об его обновлении не дают. FUSE файловые системы вообще могут отдавать mtime хоть всегда нулевого значения. mmap обновит ваш mtime когда-нибудь, или пока не вызван msync (это штатное POSIX поведение). А если у вас NFS? Всё это приводит к тому, что работать Make ожидаемо может только на системах: медленных (или хорошей гранулярностью времени на ФС), с всегда идущими вперёд часами, без FUSE/NFS/mmap/VCS.
  • Цели выполняются не атомарно. А должны? Make считает что не его забота. Но преобладающее большинство людей всё равно же захочет и будет писать цели вида:

    tgt-zstd:    zstd -d < tgt-zstd.zst > tgttgt-fetch:    fetch -o tgt-fetch SOME://URL
    

    Но внезапный сбой, перезапуск системы, убийство Make процесса с детьми, приведут к тому, что на файловой системе будут созданы целевые файлы и они не будут пересобираться, так как, с точки зрения Make, выполнены и актуальны.

    Решить эту проблему можно:

    tgt-zstd:    zstd -d < tgt-zstd.zst > tgt-zstd.tmp    fsync tgt-zstd.tmp    mv tgt-zstd.tmp tgt-zstd
    

    Но кому захочется в такие tmp/fsync/mv вызовы оборачивать каждое описание цели? Более того, в примере выше могут быть проблемы при параллельном запуске Make-ов, параллельно собирающих и записывающих в tgt.tmp.
  • Цели на зависят от своего описания. Если вы поменяли описание цели (её команды) Makefile, то будет ли Make производить пересборку этой изменённой цели? Нет. А если вы обновили какие-то переменные типа $(CFLAGS)? Тоже нет.

    Но никто же не мешает вам прописать зависимость цели от самого Makefile! Изменение описания целей будет приводить к пересборке. Но если у вас один Makefile на множество целей, то изменение только одной из них, приведёт к пересборке и всех остальных. Хотя, безусловно, уж лучше пересобрать что-то лишнее, чем недособрать что требуется.

    А что если описание целей вынести в свои отдельные Makefile и их просто:

    $ cat Makefileinclude tgt1.mkinclude tgt2.mk...
    

    Это решит проблему пересборки лишнего. Но удобно ли? Отнюдь!
  • Повезёт, если будут работать рекурсивные цели. Отличная небольшая статья Recursive Make Considered Harmful описывает простейшие случаи, когда рекурсивные Makefile-ы, где нижестоящие Makefile-ы зависят от каких-то целей описанных в сторонних, и, в зависимости от того как Make проходит граф зависимостей, могут легко возникать ситуации где всё это не будет работать корректно и пересобирать что надо. Один единственный большой Makefile на весь проект решение. Удобно? Отнюдь, иначе бы не писали рекурсивные Makefile.

    А если включить распараллеленную сборку? Тогда обход графа зависимостей снова будет нарушен, нередко и непредсказуемым образом. Например при сборке всяких FreeBSD портов, именно поэтому, по умолчанию, отключают распараллеливание, так как с ним часто может всё ломаться и вести себя непредсказуемым образом.
  • Динамически сгенерированные цели сделать нельзя. А ведь так хочется чтобы автоматически, если я написал #include tgt.h, .c файл зависел от tgt.h, ведь эту информацию можно узнать из .c выполнив какой-нибудь sed вызов.

    tgt.o: tgt.c `sed s/.../ tgt.c`
    

    сделать не выйдет. Иногда можно попытаться сгенерировать новый .mk Makefile с этими зависимостями и сделать его include. Будет ли это работать? Зависит от конкретной реализации Make, но скорее всего не так как ожидается: .mk честно пересоберётся, но проинтерпретирована будет его прошлая версия, прочитанная на этапе чтения всех Makefile-ов с include-ами.
  • Цели Makefile-ов не совсем обычный shell, а каждая строка запускается в отдельном интерпретаторе, из-за чего часто приходится или писать крайне некрасивые многострочные, но объединённые через \\$, скрипты, или выносить их в отдельные .sh файлы, вызываемые из Make. И сам Make это новый язык/формат, так и его shell это тоже не совсем привычный и удобный shell, в котором не забывать и про экранирование корректное придётся. Удобно?

Давайте честно признаемся: как часто и сколько приходилось делать make clean или пересобирать без распараллеливания, потому что что-то недособралось или не пересобралось вопреки ожиданиям? В общем случае, безусловно, это связано не с идеально корректно, правильно и полно написанными Makefile-ами, что говорит о сложности их грамотного и работоспособного написания. Инструмент должен помогать.

Требования redo


Чтобы перейти к описанию redo, я для начала расскажу что он из себя представляет как реализация и что придётся выучить его пользователю (разработчику описывающему цели и зависимости между ними).

  • redo, в общем случае, вообще не привязан к какому-либо языку для описания целей. redo не заставляет изучать новый диалект или формат файлов. Знаний POSIX shell полностью достаточно. Но можно писать все цели на Python или вообще в виде исполняемых файлов. Минимальный порог входа: ни нового формата, ни языка, ни кучи команд.
  • redo реализаций много на разных языках: POSIX shell, GNU bash, Python, Haskell, Go, C++, Inferno Shell. Средний разработчик способен написать его реализацию за день.
  • Реализация с полноценными учётом состояния и распараллеливанием сборки на чистом C занимает менее тысячи строк кода, включая полную реализацию SHA256, а результирующий исполняемый файл у меня занимает 27KB. На чистом POSIX shell можно написать в 100 строк. Это также означает и то, что вы можете просто встроить POSIX shell реализацию redo в свои tarball-ы с софтом и пользователю ничего не придётся ставить дополнительно.
  • Он не имеет ни одной описанной выше проблемы Make-а, превосходно выполняя все его задачи (с распараллеливанием).

Обычные redo цели


Правила сборки цели являются обычным POSIX shell скриптом в файле имя-цели.do. Напомню в последний раз, что это может быть и любой другой язык (если добавить shebang) или просто исполняемый бинарный файл, но по умолчанию это POSIX shell. Скрипт запускается с set -e и тремя аргументами:

  • $1 имя цели
    $2 базовое имя цели (об этом ниже)
    $3 имя файла результата

    Вот треть redo я уже и описал. Результатом выполнения цели является или весь выловленный stdout или созданный $3 файл. Почему так? Где-то удобнее не возиться с промежуточными файлами, а где-то не все программы умеют свой результат писать сразу в stdout. Например приводимые для примера цели в начале статьи в redo:

    $ cat tgt-zstd.dozstd -d < $1.zst$ cat tgt-fetch.dofetch -o $3 SOME://URL
    

    Предполагаем, что fetch не умеет писать в stdout. stdout сохраняется во временном файле, как и $3. После завершения работы скрипта, выполняется его fsync и переименование в название цели. Тем самым гарантируя атомарность её выполнения! И только при успешном выполнении, только при отработке fsync и записи результат в директории только тогда будет успешно выполнена цель.

    Некоторые цели, типа (make) clean, как правило, не генерируют никаких результатов. Большинство redo реализаций не создают пустой файл, что удобно. По умолчанию, большинство реализаций выполняют all цель.

    default цели


    Очень часто многие цели собираются одними и теми же командами. В POSIX Make можно делать такие правила сборки всех .c:

    .c:    $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<
    

    в redo для этого используются default.do файлы, а точнее default.ВОЗМОЖНО-КАКИЕ-ТО-РАСШИРЕНИЯ.do. Аналогом Make выше будет:

    $ cat default.c.do$CC $CLFAGS $LDFLAGS -o $3 $1
    

    Часто хочется узнать имя цели без расширения для этого используется $2 аргумент, совпадающий с $1 в обычных redo целях. В default-ных он будет:

    a.b.c.do       -> $2=a.b.cdefault.do     -> $2=a.b.cdefault.c.do   -> $2=a.bdefault.b.c.do -> $2=a
    

    Цели можно указывать спокойно и в любых директориях, что уровнем ниже, что выше. Вместо cd dir; redo tgt можно писать redo dir/tgt. Цель всегда будет выполняться в той же директории что и .do файл. Поэтому использование относительных путей будет надёжно работать, раз всегда известно где будет рабочая директория во время выполнения цели.

    Если файла имя-цели.do не найдено, то ищется default.do файл. А с учётом возможных расширений, поиск .do файла для цели ../a/b/xtarget.y будет такой:

    ./../a/b/xtarget.y.do./../a/b/default.y.do./../a/b/default.do./../a/default.y.do./../a/default.do./../default.y.do./../default.do
    

    Вот уже 2/3 всей redo системы описано.

    Зависимости


    Зависимости для цели задаются путём вызова в ней redo-ifchange команды:

    $ cat hello-world.doredo-ifchange hello-world.o ../config. ../config$CC $CFLAGS -o $3 hello-world.o$ cat hello-world.o.doredo-ifchange hw.c hw.h ../config. ../config$CC $CFLAGS -c -o $3 hw.c$ cat ../configCC=ccCFLAGS=-g$ cat ../all.do# этот файл в корне проекта для красоты, чтобы, набрав <em>redo</em>, он собрал# hw/hello-world программу по умолчаниюredo-ifchange hw/hello-world# Очистка проекта для красоты$ cat ../clean.doredo hw/clean$ cat clean.dorm -f *.o hello-world
    

    Именно тут коренное отличие redo и кроется: у него есть state. Для каждой цели в нём сохраняются зависимости и информация чтобы понять их свежесть. redo-ifchange запишет, что при выполнении такой-то цели, ей требовались такие-то зависимости, а также проверит не изменились ли они, все ли они свежи, а если нет, то запустить их сборку. Зависимость от .do файла автоматическая. В примере выше, изменение config файла приведёт к пересборке всего что касается hello-world программы.

    Где хранится state? Зависит от реализации. Кто-то хранит в виде TSV-like файла имя-цели.do.state, кто-то аналогично, но в .redo директории, кто-то в SQLite3 СУБД .redo директории.

    stderr целей кем-то не перехватывается, а кем-то сохраняется в state, чтобы пользователь мог дать команду покажи ка мне лог сборки такой-то цели.

    Что сохраняется в state? Автор redo предлагает вообще хранить криптографический хэш от зависимости: если хоть бит изменится, независимо от FUSE/mmap/NFS/VCS, то гарантированно это изменение будет обнаружено. Некоторые реализации хранят набор из ctime, inode number, размера и кучи всего другого с хэшом тогда не придётся его пересчитывать, если мы всё равно увидели что размер обновился.

    Наличие state на файловой системе позволяет иметь и lock-и и поэтому проблем аналогичных рекурсивному Make нет. Есть общий для всех (в пределах проекта) state с lock-ами и всеми известными зависимостями. Это также безопасно позволяет запускать сколько угодно параллельных процессов сборки.

    Динамика


    Может показаться, что redo-ifchange это как-раз и есть такой новый формат, который всё же придётся иметь в виду. Но нет это обычный вызов команды. redo-ifchange можно вызвать внутри скрипта абсолютно когда угодно, хоть пост фактум после сборки цели:

    redo-ifchange $2.cgcc -o $3 -c $2.c -MMD -MF $2.depsread deps < $2.depsredo-ifchange ${deps#*:}
    

    В него можно подставлять любые пути, хоть автоматически сгенерированные на основе include-ов:

    $ cat default.o.dodeps=`sed -n 's/^#include "\(.*\)"$/\1/p' < $2.c`redo-ifchange ../config $deps[...]
    

    Зависеть от всех собранных *.c?

    for f in *.c ; do echo ${f%.c}.o ; done | xargs redo-ifchange
    

    Без проблем можно и сгенерировать .do (....do.do цель) файл на лету и иметь от него зависимость. А чтобы не писать в куче .do файлах одни и те же $CC $CFLAGS..., то можно это сохранить в отдельном файле компиляции кода:

    $ cat tgt.doredo-ifchange $1.c cc./cc $3 $1.c$ cat cc.doredo-ifchange ../config. ../configcat > $3 <<EOF#!/bin/sh -e$CC $CFLAGS $LDFLAGS -o \$1 \$@ $LDLIBSEOFchmod +x $3
    

    Хочется сгенерировать compile_flags.txt для интеграции с Clang LSP демоном?

    $ cat compile_flags.txt.doredo-ifchange ../config. ../configecho "$PCSC_CFLAGS $TASN1_CFLAGS $CRYPTO_CFLAGS $WHATEVER_FLAGS $CFLAGS" |    tr " " "\n" | sed "/^$/d" | sort | uniq
    

    А как получить все эти $PCSC_CFLAGS, $TASN1_CFLAGS? Конечно же, используя pkg-config, без громоздких и медленных autotools!

    $ cat config.docat <<EOF[...]PKG_CONFIG="${PKG_CONFIG:-pkgconf}"PCSC_CFLAGS="${PCSC_CFLAGS:-`$PKG_CONFIG --cflags libpcsclite`}"PCSC_LDFLAGS="${PCSC_LDFLAGS:-`$PKG_CONFIG --libs-only-L libpcsclite`}"PCSC_LDLIBS="${PCSC_LDLIBS:-`$PKG_CONFIG --libs-only-l libpcsclite`}"TASN1_CFLAGS="${TASN1_CFLAGS:-`$PKG_CONFIG --cflags libtasn1`}"TASN1_LDFLAGS="${TASN1_LDFLAGS:-`$PKG_CONFIG --libs-only-L libtasn1`}"TASN1_LDLIBS="${TASN1_LDLIBS:-`$PKG_CONFIG --libs-only-l libtasn1`}"[...]EOF
    

    Если кому-то всё же любо видеть единственный .do файл, аналогичный такому Makefile:

    foo: bar baz    hello world.c:    $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<
    

    то это легко сделать:

    $ cat default.docase $1 infoo)    redo-ifchange bar baz    hello world    ;;*.c)    $CC $CFLAGS $LDFLAGS -o $3 $1    ;;esac
    

    но при этом не забывать, что изменение default.do приведёт к пересборке всех целей им созданных. А если надо ровно для одного .o файла сделать особые команды или дополнительные зависимости? Ну так и написать для него special.o.do, а остальные будут fallback делать до default.o.do и default.do правил.

    Заключение


    Про redo я слышал наверное ещё лет десять назад, но не придал ему значения и даже повёл носом от того, что мне что, придётся каждую цель в отдельном файле описывать!? (про default я не знал). Но решил попробовать, опять же, без уверенности что переезд будет безболезненным и, тем более, стоящим. А я большой любитель минималистичных и suckless подходов (уж извините, но CMake, собирающийся дольше чем многие GCC, с документацией более ёмкой чем pure-C реализация redo это перебор).

    • Гора ручной работы убрана из-за возможности автоматизации и динамического создания зависимостей.
    • Полностью решённые проблема с совместимостью на разношёрстных системах (*BSD vs GNU) POSIX shell работает везде одинаково, совершенно разные (Python, C, shell) реализации redo вели себя одинаково.
    • Минус целый язык/формат Makefile-ов.
    • Гарантированно работающее распараллеливание сборок.
    • Небывалая и невиданная точность задания зависимостей (потому что легко и просто) и, соответственно, система сборки честно пересобирает только то что связано и изменилось.
    • Каждая цель в своём файле оказалось очень удобным, так как можно узнать все доступные цели для выполнения, сделав l **.do.

    О чём я пожалел и есть ли всё же помехи/недостатки?

    • Жалею только об огромном количестве потраченных часов на борьбу с Make за все эти годы, абсолютно ничем не компенсирующиеся.
    • Мне потребовался не один месяц чтобы отучиться от рефлекса делать redo clean, так как уже привычка после Make, что что-нибудь обязательно да не (пере)соберётся.

    Рекомендую документацию apenwarr/redo реализации, с огромным количеством примеров и пояснений.

    Сергей Матвеев, шифропанк, Python/Go/C-разработчик, главный специалист ФГУП НТЦ Атлас.
Подробнее..

Финальный релиз этого года CLion 2020.3! С новыми функциями в отладчике, проверками MISRA и улучшениями для Qt

08.12.2020 00:17:38 | Автор: admin
Привет, Хабр!

Год подходит к концу, и мы в JetBrains выпускаем традиционный паровоз релизов для наших десктопных инструментов. Про некоторые из них (WebStorm, DataGrip) мы уже писали тут на Хабре. А сегодня пришло время рассказать про обновление нашей кроссплатформенной среды разработки на C и C++ CLion 2020.3!

CLion 2020.3 release

Коротко о самом главном:
  • В отладчике появились возможности исследовать дампы памяти и запускать конфигурацию с привилегиями администратора. А еще теперь можно перемещать точку исполнения по коду в любом направлении прямо во время сеанса отладки.
  • Для модульного тестирования мы добавили поддержку CTest инструмента запуска тестов, идущего в комплекте с проектной моделью CMake.
  • Для проектов на Qt добавлены шаблоны для создания новых Qt-проектов и новых классов UI, функция автодополнения адаптирована для работы с сигналами и слотами, а автоимпорт следует принятому в Qt стилю.
  • Существенно расширилось множество проектов, использующих Makefile, которые можно успешно открывать в CLion. А для проектов CMake появилась возможность отключать временно неиспользуемые профили CMake.
  • Для разработчиков встроенных систем мы включили начальную поддержку стандартов MISRA C 2012 и MISRA C ++ 2008.
  • А также множество других улучшений для конфигураций запуска и отладки, в поддержке систем контроля версий и для режима удаленной разработки. Разнообразные приятные улучшения в UI. И долгожданное превью нового сервиса для совместной разработки и парного программирования.


Новую версию можно скачать с сайта и попробовать бесплатно в течение 30 дней.Если у вас есть активная подписка на CLion или All Products Pack, просто обновите версию на 2020.3. Напоминаем, что при покупке годовой подписки на любой продукт предоставляется резервная бессрочная лицензия.

А теперь поговорим о нововведениях и улучшения версии 2020.3 подробнее.

Отладчик


Релиз 2020.3 стал большой вехой в развитии интеграций с отладчиками в CLion. И это неспроста, ведь эту интеграцию использует не один, а сразу несколько продуктов компании! CLion, плагин IntelliJ Rust и раннее превью среды для разработки игр Rider for Unreal Engine. Совместными усилиями мы смогли реализовать много важных новых возможностей в отладчике.

Отладка с дампами памяти


Процесс завершился нештатно, проще говоря, упал, оставив после себя дамп памяти? Новое действие Run | Open Core Dump, доступное для Linux и macOS, позволит открыть дамп памяти упавшего процесса в CLion для дальнейшей отладки:
Open Core Dump

В диалоге требуется указать путь к дампу памяти, путь к файлу с отладочными символами (подойдет, например, исполняемый файл программы, если была включена сборка в режиме с отладочной информацией), сопоставление путей (если сборка исполняемого файла производилась на другой машине). Далее CLion создаст конфигурацию для отладки этого дампа памяти, которую вы сможете запустить.

Диалог умеет показывать список уже созданных конфигураций. Конфигурации также можно создавать вручную из меню Run | Edit Configurations. Во время отладки с дампом памяти можно анализировать информацию о фреймах, изучать значения переменных, просматривать состояние памяти и код на ассемблере, вычислять выражения и запускать команды из консоли отладчика. При этом, по понятным причинам, отключена пошаговая отладка и недоступно окно вывода процесса.
Debug Core Dump

Из текущих ограничений, помимо недоступности новой функции на платформе Windows, стоит еще отметить, что в случае LLDB пока что не используется файл с отладочными символами. А возможности отладки дампов, собранных на другой машине, и удаленная отладка дампов сильно ограничены (CPP-22736, CPP-22656).

Запуск и отладка с привилегиями администратора


В конфигурациях запуска и отладки с этого релиза появился новый флажок запускать конфигурацию с привилегиями администратора. Настройка поддержана для многих типов конфигураций: CMake, Makefile, Gradle Native, Custom Build, CTest.

Чтобы использовать новую возможность было удобнее на практике, мы написали специальный демон, который предлагает вам оставаться авторизованным от имени администратора при запуске таких конфигураций в течение выбранного периода времени:
Elevation Settings

Подробнее о работе этой новой возможности читайте здесь (на английском).

Установка точки исполнения


Пользователи Visual Studio могут быть знакомы с такой полезной возможностью, как изменение точки исполнения во время сеанса отладки. Для всех остальных: представьте, что вы хотите пропустить исполнение целого цикла, поменять ветку условного оператора, вернуться на несколько шагов назад в потоке исполнения программы прямо во время сеанса отладки. Теперь для этого в CLion есть новое действие Set Execution Point to Cursor, или можно вручную переставлять желтую стрелочку на нужную строку прямо в редакторе:
Set Execution Point

Оговоримся, что подобное действие может сломать программу (например, если вы пропустите команду инициализации переменной). Но при умелом использовании, оно позволит вам отлаживать приложение очень эффективно.

Интерактивные подсказки


Практически во всех наших IDE на основе платформы IntelliJ в версии 2020.3 появились интерактивные подсказки и встроенные watches. Еще до 2020.3 во время сеанса отладки вы могли видеть значения переменных прямо в редакторе. Теперь эти подсказки стали интерактивными переменную можно раскрыть и посмотреть значение всех ее полей:
Interactive hints

А еще можно добавить произвольное выражение для подсчета и просмотра прямо в редакторе (рядом с нужной строкой кода). Это оказывается удобнее обычных watches (которые постоянно показываются в окне отладчика) тогда, когда используемые в выражении переменные имеют смысл только в конкретном месте кода (например, локальные переменные).

Хотите увидеть новые возможности в действии? Попробуйте сами или посмотрите это видео от нашего девелопер-адвоката:


Модульное тестирование


Что касается фреймворков для модульного тестирования, CLion поддерживает Google Test, Catch(2), Boost.Test и doctest. По результатам нашего исследования экосистемы разработки 2020 года 31% C++-разработчиков используют Google Test. Поэтому мы потратили время, чтобы улучшить представление результатов запуска Google Test в CLion:
  • Научились строить дерево тестов сразу, еще до выполнения всех тестов.
  • Начали отображать значения параметров тестов (для параметризованных тестов).
  • Ввели специальный значок для пометки отключенных (DISABLED) тестов в дереве.
  • А главное, существенно ускорили процесс поиска тестов в проекте.

GT run

Наши пользователи (в частности, здесь на Хабре) активно просили добавить поддержку CTest инструмента запуска тестов, идущего в комплекте с проектной моделью CMake. Это было не очень просто, так как CTest это сам по себе инструмент запуска, а не очередной фреймворк, и поэтому в существующий API укладывался плохо. Но мы наконец сделали это! CLion теперь автоматически находит тесты CTest в проекте, создает для них конфигурации запуска и отладки, строит дерево с отображением результатов тестов. Вы даже можете отлаживать тесты при этом CLion запустит именно тест, а не процесс ctest.

Что касается ограничений, стоит отметить, что CLion поддерживает версии CTest 3.14 и выше. К тому же, если в качестве теста CTest запускается блок тестов из уже поддерживаемого фреймворка (Google Test, Catch(2), Boost.Test и doctest), такой блок будет иметь одну запись в дереве результатов (то есть соответствовать одному тесту).

Традиционное видео от Фила Нэша, посвященное улучшениям для модульного тестирования:


Проекты Qt


Мы видим, что в CLion часто пишут проекты с использованием фреймворка Qt. И, хотя делать свой дизайнер интерфейсов или поддерживать проектную модель qmake мы пока не планируем, кое-что для улучшения пользовательского опыта в CLion в этом направлении мы все же решили сделать.

Шаблоны проектов и классов UI


Диалог создания нового проекта теперь предлагает два новых типа Qt Console Executable и Qt Widgets Executable:
New Qt project

Указываете необходимые значения CLion создает стартовый CMake-проект выбранного типа, и вот уже простейший виджет или консольное приложение у вас в кармане! Кстати, говоря о CMake и Qt, Qt6 предлагает устанавливать некоторые библиотеки через Conan (у CLion также есть соответствующий плагин) и собирать их через CMake и Ninja. Чем не повод попробовать CLion для разработки с использованием Qt?

Кстати, при добавлении нового класса Qt UI, CLion умеет создавать сразу три файла (.ui и файлы класса). Для этого выберите действие QT UI Class в меню добавления нового файла.

Работа с кодом Qt


Код Qt это, в целом, обычный код на C++, но есть и свои особенности. Так, в Qt принят определенный стиль импортов заголовочных файлов. Например, можно корректно включить QtCore/qurl.h, но будет более правильным использовать QUrl. Стиль теперь поддержан при автоимпорте в CLion:
Qt autoimport

Функция дополнения кода теперь понимает, когда необходимо предлагать варианты только сигналов или только слотов для основных сущностей Qt:
Qt completion

Это удобно, так как в выпадающем списке показываются только подходящие по смыслу варианты, а не все те, которые совпадают по набранным символам.

А какой возможности для Qt лично вам не хватает в CLion? Пишите в комментариях!

Проектные модели в CLion


Мы постепенно включаем в CLion поддержку все большего числа видов проектов с использованием Makefile (список протестированных проектов мы ведем тут). В релизе 2020.3 мы поддержали проекты, которые использую инструменты ccache, libtool, dolt, slibtool и jlibtool. Это проекты ZFS, PHP, Mono, Curl и другие. Конфигурации запуска и отладки для целей из файла Makefile верхнего уровня создаются автоматически. Кроме того, для проектов Makefile появилась возможность перекомпиляции отдельного файла без необходимости пересобирать весь проект (в раскладке по умолчанию Ctrl+Shift+F9 на Windows/Linux и F9 на macOS).

Для проектов CMake теперь можно отключить временно неиспользуемые профили CMake. Это особенно полезно, если в проекте настроено сразу несколько профилей для удаленной разработки (часть из них может быть временно отключена) или для разнообразных тулчейнов (тоже не всегда используемых все вместе). При отключении таких профилей время загрузки проекта заметно сокращается. Раньше для этого профили надо было удалять, а теперь вы просто можете их выключить. Сделать это можно из настроек профилей в Settings/Preferences | Build, Execution, Deployment | CMake либо из диалога загрузки CMake:
Disable/enable CMake profiles

Чтобы узнать подробнее об улучшениях для проектов на Qt и в проектных моделях, смотрите выступление нашего девелопера-адвоката.

Стандарты MISRA


Среди наших клиентов много разработчиков встроенных систем и представителей автомобильной отрасли. Для них важной частью процесса разработки является сертификация кода по стандартам MISRA. С самой сертификацией помочь мы пока не можем, а вот указать на возможные несоответствия стандартам MISRA на раннем этапе разработки нам вполне по силам! В версии CLion 2020.3 мы начали добавлять проверки стандартов MISRA C 2012 и MISRA C++ 2008 во встроенный статический анализатор кода:
MISRA checks

Поддержано еще не все; полный список можно найти на этой странице. Настроить инспекции довольно просто:
MISRA settings

В будущем мы добавим больше проверок MISRA. Возможно, также появятся проверки AUTOSAR.

Code With Me новый инструмент для совместной разработки


Практикует ли ваша команда парное программирование? Как насчет инструмента для совместного изучения кода? Совсем недавно мы запустили программу раннего доступа для нашего нового инструмента совместной разработки Code With Me. По сути, это плагин, доступный почти для всех наших десктопных IDE (пока что за исключением JetBrains Rider и DataGrip). С помощью него вы и ваша команда можете совместно работать над проектами прямо из своих IDE. Чтобы попробовать Code With Me, установите плагин в Preferences / Settings | Plugins.
Code With Me

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

И многое другое


Из других важных улучшений хочется отметить:
  • Теперь, чтобы настроить окружение, в котором будет запускаться конфигурация запуска/отладки, можно использовать скрипт. К тому же, редактировать конфигурации запуска/отладки теперь можно прямо во время индексирования проекта.
  • Мы расширили возможности автодополнения сниппетами Clang. Они помогут поправить не только ключевые слова, но и синтаксис целых выражений:
    Clang snippets
  • Цветовую схему CLion теперь можно синхронизировать с темой вашей операционной системы. Чтобы включить эту возможность, зайдите в меню Settings / Preferences | Appearance & Behavior | Appearance и выберите опцию Sync with OS.
  • Чтобы разделить окно на несколько частей и иметь перед глазами сразу несколько открытых вкладок, теперь можно использовать простейшее перетаскивание файлов (Drag&Drop). То же самое можно сделать при помощи действия Open in Right Split, выбрав нужные файлы в окне Project, Recent Files или Search Everywhere.
  • В интеграции с системой контроля версий появилась поддержка Git-стейджинга. Меню VCS теперь учитывает то, какую VCS вы используете в проекте (например, Git). А в диалоге Search Everywhere появилась вкладка Git. Теперь там можно искать хэши, сообщения коммита, теги и ветки.


На этом у нас все. Мы рады, что вы дочитали до конца. Спасибо за внимание! Оставляйте свои вопросы, мысли и предложения в комментариях нам интересно, что вы думаете, и мы всегда рады помочь!

Команда CLion
The Drive to Develop
Подробнее..

Отладка Makefile часть 1

21.12.2020 02:23:17 | Автор: admin

Отладка makefile это что-то из черной магии. К несчастью, не существует такой вещи как makefile отладчик, чтобы изучить ход выполнения конкретного правила или как разворачивается переменная. Большую часть отладки можно выполнить с помощью обычных printов и проверкой makefile. Конечно, GNU make немного помогает своими встроенными методами и опциями командной строки. Один из лучших методов отладки makefile это добавить отладочные перехваты (hooks) и использовать техники безопасного программирования, на которые можно будет опереться, когда дела пойдут совсем плохо. Далее представлено несколько основных техник отладки и практик безопасного программирования, которые будут, на мой взгляд, наиболее полезными.

Отладочные возможности make

Очень полезная для отладки не работающего makefile функция warning . Так как функция warningразворачивается в пустую строку, ее можно использовать везде в makefile: на верхнем уровне, в имени цели, в списке зависимостей и в командных скриптах. Это позволяет печатать значения переменных там, где это наиболее подходит для их проверки. Например:

$(warning A top-level warning)FOO := $(warning Right-hand side of a simple variable)barBAZ = $(warning Right-hand side of a recursive variable)boo$(warning A target)target: $(warning In a prerequisite list)makefile $(BAZ)   $(warning In a command script)   ls$(BAZ):

Дает вывод:

$ makemakefile:1: A top-level warningmakefile:2: Right-hand side of a simple variablemakefile:5: A targetmakefile:5: In a prerequisite listmakefile:5: Right-hand side of a recursive variablemakefile:8: Right-hand side of a recursive variablemakefile:6: In a command scriptlsmakefile

Заметим, что выполнение функции warning следует нормальному ходу алгоритма make для немедленных и отложенных вычислений. Также, присваивание к BAZ содержит warning и сообщение не печатается, пока BAZ не будет развернут в списке зависимостей.

Возможность вставки warning вызов куда угодно делает его очень полезным инструментом отладки.

Опции командной строки

Есть три очень полезные опции командной строки для отладки: --just-print (-n), --print-data-base (-p) и --warn-undefined-variables.

--just-print

Первое испытание новой цели в makefile это вызвать make с опцией --just-print (-n). Будучи вызванным с этой опцией make прочитает makefile и напечатает каждую команду, которую в обычном режиме он бы выполнил для обновления цели. Для удобства, GNU make также выведет команды помеченные собачкой (@) - заглушающим модификатором.

Опция предназначена для полного подавления выполнения всех команд. Однако, есть исключения. В то время как make не выполняет командные скрипты целей, он выполнит вызовы shell функции, которые будут в немедленном контексте выполнения. Для примера:

REQUIRED_DIRS = ..._MKDIRS := $(shell for d in $(REQUIRED_DIRS); \             do                               \                [[ -d $$d ]] || mkdir -p $$d; \             done)$(objects) : $(sources)

Смысл переменной _MKDIRS в инициировании создания нужных директорий. Если выполнить это с --just-print опцией, команда оболочки будет выполнена как обычно в момент чтения makefile. Затем, make выведет (без исполнения) каждую команду компиляции необходимую для обновления списка файлов $(objects).

--print-data-base

Еще одна опция, которую нужно использовать почаще. С ней, после обычного "прогона" makefile, make выдаст в конце его внутреннюю базу данных. Данные сгруппированы по группам: переменные, директории, неявные правила, переменные шаблонов, файлы (явные правила) и vpath путь поиска. Давайте остановимся на этих секциях подробнее.

Секция Variables выводит список переменных с описательным комментарием:

# automatic<D = $(patsubst %/,%,$(dir $<))# environmentEMACS_DIR = C:/usr/emacs-21.3.50.7# defaultCWEAVE = cweave# makefile (from `../mp3_player/makefile', line 35)CPPFLAGS = $(addprefix -I ,$(include_dirs))# makefile (from `../ch07-separate-binaries/makefile', line 44)RM := rm -f# makefile (from `../mp3_player/makefile', line 14)define make-library libraries += $1 sources += $2 $1: $(call source-to-object,$2) $(AR) $(ARFLAGS) $$@ $$^endef

Авто-переменные не выводятся, но выводятся другие, зависящие от них, полезные переменные, такие как $(<D). В комментарии пишется вывод функции origin (см. make manual). Если переменная определена в файле, будет выведено имя файла и номер строки объявления. Простые и рекурсивные переменные определяются по оператору присваивания. Вычисленное значение простых переменных также печатается в правой части выражения.

Следующий раздел Directories более полезен разработчикам make, а не пользователям make. Представляет собой список папок просмотренных make, включая SCCS и RCS под-папки, которые могут быть, но обычно отсутствуют. Для каждой папки выводится детали реализации: номер устройства, inode и статистика по совпадениям шаблонов файлов.

Секция Implicit rules содержит все встроенные и пользовательские шаблоны в базе данных make. Опять же, для тех правил, которые определены в файле, выводится имя файла и строка определения:

%.c %.h: %.y# commands to execute (from `../mp3_player/makefile', line 73):   $(YACC.y) --defines $<   $(MV) y.tab.c $*.c   $(MV) y.tab.h $*.h%: %.c# commands to execute (built-in):   $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@%.o: %.c# commands to execute (built-in):   $(COMPILE.c) $(OUTPUT_OPTION) $<

Изучение этой секции даст быстрое понимание разнообразия и структуры встроенных правил make. Конечно, не все встроенные правила реализованы как шаблонные. Если нужное правило не находится, его еще можно поискать в разделе Files, где выводятся суффиксные правила в старом стиле.

Следующая секция это каталог зависящих от конкретного шаблона переменных определенных в makefile. Описание этих переменных это их значения, которые будут подставлены во время выполнения при выполнении соответствующих им правил. Например, для переменной определенной как:

%.c %.h: YYLEXFLAG := -d%.c %.h: %.y $(YACC.y) --defines $< $(MV) y.tab.c $*.c $(MV) y.tab.h $*.h

будет выведено:

# Pattern-specific variable values%.c :# makefile (from `Makefile', line 1)# YYLEXFLAG := -d# variable set hash-table stats:# Load=1/16=6%, Rehash=0, Collisions=0/1=0%%.h :# makefile (from `Makefile', line 1)# YYLEXFLAG := -d# variable set hash-table stats:# Load=1/16=6%, Rehash=0, Collisions=0/1=0%# 2 pattern-specific variable values

Далее следует Files секция, которая выводит все явные и суффикс- правила, которые относятся к конкретным файлам:

# Not a target:.p.o:# Implicit rule search has not been done.# Modification time never checked.# File has not been updated.# commands to execute (built-in): $(COMPILE.p) $(OUTPUT_OPTION) $<lib/ui/libui.a: lib/ui/ui.o# Implicit rule search has not been done.# Last modified 2004-04-01 22:04:09.515625# File has been updated.# Successfully updated.# commands to execute (from `../mp3_player/lib/ui/module.mk', line 3): ar rv $@ $^lib/codec/codec.o: ../mp3_player/lib/codec/codec.c ../mp3_player/lib/codec/codec.c ../mp3_player/include/codec/codec.h# Implicit rule search has been done.# Implicit/static pattern stem: `lib/codec/codec'# Last modified 2004-04-01 22:04:08.40625# File has been updated.# Successfully updated.# commands to execute (built-in): $(COMPILE.c) $(OUTPUT_OPTION) $<

Промежуточные файлы и суффикс-правила обозначены как "Not a target"; остальное цели. Каждый файл включает комментарии, показывающие как make будет обрабатывать это правило. У файлов, которые найдены через обычный vpath поиск, показан найденный путь до них.

Последняя секция называется VPATH Search Paths и перечисляет значение VPATH и все vpath шаблоны.

Для makefile'ов, которые обильно используют пользовательские функции и eval для создания сложных переменных и правил, исследование этого вывода - единственный путь проверить, что разворачивание макросов дает ожидаемые значения.

--warn-undefined-variables

Эта опция заставляет make выводить предупреждение при вычислении неопределенной переменной. Так как неопределенные переменные вычисляются в пустую строку, зачастую, опечатки остаются необнаруженными долгое время. Проблема с этим ключом в том, что многие встроенные правила используют неопределенные переменные, которые нужны для перехвата пользовательских значений. Поэтому запуск make с этой опцией неизбежно выведет много предупреждений, которые не являются ошибками и не связаны с makefile'ом пользователя, Например:

$ make --warn-undefined-variables -nmakefile:35: warning: undefined variable MAKECMDGOALSmakefile:45: warning: undefined variable CFLAGSmakefile:45: warning: undefined variable TARGET_ARCH...makefile:35: warning: undefined variable MAKECMDGOALSmake: warning: undefined variable CFLAGSmake: warning: undefined variable TARGET_ARCHmake: warning: undefined variable CFLAGSmake: warning: undefined variable TARGET_ARCH...make: warning: undefined variable LDFLAGSmake: warning: undefined variable TARGET_ARCHmake: warning: undefined variable LOADLIBESmake: warning: undefined variable LDLIBS

Тем не менее, эта опция может быть крайне полезна в поиске ошибок такого типа.

--debug опция

Когда нужно узнать как make анализирует твой граф зависимостей, используй --debug опцию. Она предоставляет самую детальную доступную информацию без запуска отладчика. Есть пять опций вывода отладки и один модификатор: basic, verbose, implicit, jobs, all, и makefile, соответственно.

Если опция указана в форме --debug, используется basic - краткий вывод для отладки. Если опция указана в виде -d, используется all. Для выбора других комбинаций можно использовать список разделенный запятыми: --debug=option1,option2, где option может быть одно из следующих значений (на самом деле, make смотрит только на первую букву):

  • basic
    Наименьшая детализированность. Когда включена, make выводит каждую цель, которая нуждается в обновлении и статус действия обновления.

  • verbose
    Эта опция включает basic вывод и дополнительную информацию о проанализированных файлах и о зависимостях, которые не нуждаются в обновлении.

  • implicit
    Эта опция включает basic вывод и дополнительную информацию о неявных правилах, просмотренных в поиске нужного для каждой выполняемой цели.

  • jobs
    Эта опция выводит детали о запущенных make'ом подпроцессах. Она не включает basic вывод.

  • all
    Включает все выше перечисленное и является значением по умолчанию для -d опции.

  • makefile
    Обычно, отладочная информация включается после того, как все makefileы будут обновлены. Обновление включает в себя и обновление всех импортированных файлов, таких как файлы со списками зависимостей. С этим модификатором make выведет выбранную опциями информацию в ходе обновления makefile'ов и импортированных файлов. Он включает basic информацию, а также включается при указании all опции.

Пишем код, удобный для отладки

Как мы видим, средств отладки для makefile не так уж и много, вывод внутреннего состояния и несколько функций отчетов. Когда дела обстоят именно так, мы сами вынуждены делать наши makefileы такими, чтобы и минимизировать шанс ошибок и иметь инструментарий для отладки этих ошибок.

Правила в этом разделе, в несколько произвольном виде, основаны на практиках кодирования, защитном программировании и техник отладки. Тогда как определенные правила, такие как проверка статуса выхода команд, можно поместить как в хорошие практики кодирования так и в защитное программирование, эти три категории отражают общую шкалу. Кодируй хорошо свои makefile, но без излишеств в срезании углов. Используй защитное программирование, чтобы защитить makefile от неожиданных событий и изменчивого внешнего окружения. В конце концов, когда баги все-таки будут возникать, используй каждый приём, доступный тебе, чтобы их размозжить.

Принцип KISS залог всех хороших систем. makefile очень быстро могут становится сложными, даже для повседневных задач, таких как генерация графа зависимостей. Борись с тенденцией включать все больше и больше функционала в твою систему сборки. Ибо ты проиграешь, но не так плохо, если будешь при каждом случае добавлять функционал.

Хорошие практики кодирования

По моему опыту, большая часть программистов не рассматривает написание makefile'ов как программирование, и, из-за этого, не уделяют такое же внимание, как при разработке на C++ или Java. Но ведь язык make это полный по Тьюрингу декларативный язык! И если надежность и легкость сопровождения твоей системы сборки важна, пиши ее тщательно и используй все доступные тебе лучшие практики.

Один из важнейших аспектов в программировании безотказных makefile это проверка статуса выхода команд. Конечно, make проверяет для простых команд это дело сам, но makefile часто содержит составные команды, которые могут не сработать молча:

do:   cd i-dont-exist; \   echo *.c

При выполнении этот makefile не прервется с ошибкой, тогда как ошибка несомненно произойдет:

$ makecd i-dont-exist; \echo *.c/bin/sh: line 1: cd: i-dont-exist: No such file or directory*.c

И далее, выражение подстановки не найдет никаких файлов .c, и молча вернет выражение подстановки. Ой. Способ по-лучше, это использовать возможности командной оболочки по проверке и предотвращению ошибок:

SHELL = /bin/bashdo:   cd i-dont-exist && \   shopt -s nullglob &&   echo *.c

Теперь ошибка cd правильно передастся make, команда echo никогда не исполнится и make прервётся со статусом ошибки. В дополнение, после установки nullglob опции bashа подстановка вернет пустую строку если не будут найдены такие файлы. (Конечно, в твоем конкретном случае могут быть другие предпочтения.)

$ makecd i-dont-exist && \echo *.c/bin/sh: line 1: cd: i-dont-exist: No such file or directorymake: *** [do] Error 1

Другой хорошей практикой является форматирование своего кода для максимальной читабельности. Большая часть makefile'ов плохо отформатированы, и как следствие, их трудно читать. Что тут кажется легче прочитать?

_MKDIRS := $(shell for d in $(REQUIRED_DIRS); do [[ -d $$d \]] || mkdir -p $$d; done)

или:

_MKDIRS := $(shell                           \             for d in $(REQUIRED_DIRS);      \             do                              \               [[ -d $$d ]] || mkdir -p $$d; \             done)

Если ты как все люди, то тебе будет труднее прочитать первое, точку с запятой сложно найти, и количество операндов сосчитать еще сложнее. И это не шутки. Огромный процент синтаксических ошибок, которые встречаются в командных скриптах будут из-за пропущенных точек с запятой, символов экранирования, других разделителей, таких как разделитель программного конвейера или логические операторы.

Замечательно, что не все пропущенные разделители дадут ошибку. Для справки, ни одна из следующих ошибок не даст синтаксической ошибки оболочки окружения:

TAGS:        cd src \        ctags --recursedisk_free:        echo "Checking free disk space..." \        df . | awk '{ print $$4 }'

С форматированием команд для читабельности такого рода ошибки станет легко ловить. Делай отступы при работе в пользовательских функциях. Если же пробелы станут проблемой при работе макроса, форматирование можно обернуть через вызов strip функции. Форматируй длинные списки значений по одному на строку. Добавь комментарий перед каждой целью, дай краткое пояснение, и задокументируй параметры.

Следующей хорошей практикой является использование переменных для хранения общих значений. Как и в любой программе, неограниченное использование буквальных значений создает дублицирование кода и приводит к проблемам поддержки и багам. Еще одно большое преимущество переменных make умеет их выводить в ходе работы в целях отладки. Ниже в разделе способы отладки будет приведена полезная команда.

Защитное программирование

Защитный код это код, который выполняется когда одно из твоих предположений или ожиданий неверно if проверяет то, что всегда истинно, assert функция никогда не упадет, и конечно, значимость такого кода в том, что внезапно (обычно когда меньше всего этого ждешь), он выполняется и дает предупреждение или ошибку, или ты включаешь трассировку кода, чтобы увидеть внутреннюю работу make.

Валидация это отличный пример защитного программирования. Следующий код проверяет что текущая запущенная версия make 3.80:

NEED_VERSION := 3.80$(if $(filter $(NEED_VERSION),$(MAKE_VERSION)),,             \ $(error You must be running make version $(NEED_VERSION).))

Для приложений Java полезно включить проверку файлов в CLASSPATH.

Код валидации также может просто проверять что что-то истинно.

Другой отличный пример защитного программирования это использование assert функций, например таких:

# $(call assert,condition,message)define assert   $(if $1,,$(error Assertion failed: $2))endef# $(call assert-file-exists,wildcard-pattern)define assert-file-exists   $(call assert,$(wildcard $1),$1 does not exist)endef# $(call assert-not-null,make-variable)define assert-not-null   $(call assert,$($1),The variable "$1" is null)endef

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

Также можно определить пару функций для трассировки разворачивания пользовательских функций:

# $(debug-enter)debug-enter = $(if $(debug_trace),\                $(warning Entering $0($(echo-args))))# $(debug-leave)debug-leave = $(if $(debug_trace),$(warning Leaving $0))comma := ,echo-args = $(subst ' ','$(comma) ',\              $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)'))

Можно добавить вызовы этих макросов в свою функцию и оставить их выключенными до тех пор, пока они не потребуются для отладки. Для включения их нужно установить debug_trace в любое не пустое значение:

$ make debug_trace=1

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

QUIET := @target:   $(QUIET) some command

Используя эту технику можно увидеть ход выполнения заглушенных команд просто переопределив переменную в командной строке:

$ make QUIET=
Подробнее..

Перевод Отладка Makefile часть 2

21.12.2020 22:14:07 | Автор: admin

Методы отладки


В этой части поговорим об общих методах и проблемах отладки. В конечном итоге, отладка это солянка из всего, что работает в данной конкретной ситуации. Эти методы работают для меня и мне приходится полагаться на них даже в случае проблем с простейшими makefile. Может быть, они помогут и тебе тоже.


Отладка Makefile /часть 1/


Один из очень раздражающих багов в make 3.80 был в сообщении об ошибке в makefile, где make указывал номер строки, и обычно этот номер строки был неверный. Я не удосужился исследовать из-за чего эта проблема возникает: из-за импортируемых файлов, присваиваний многострочных переменных или из-за пользовательских макросов. Обычно, make дает номер строки больше чем должен был бы. В сложных makefile бывает что номер не совпадает на 20 строк.


Часто наиболее простой путь увидеть значение переменной это напечатать его в ходе выполнения цели. И хотя распечатать легко можно с помощью warning, в долгой перспективе поможет сэкономить много времени немного усилий на добавление общей цели debug для вывода переменных. Вот примерный код цели debug:


debug:       $(for v,$(V), \         $(warning $v = $($v)))

Для того чтобы использовать её, нужно перечислить имена переменных которые надо распечатать в командной строке и собрать debug цель:


$ make V="USERNAME SHELL" debugmakefile:2: USERNAME = Ownermakefile:2: SHELL = /bin/sh.exemake: debug is up to date.

Если уж совсем делать всё волшебно, то можно использовать MAKECMDGOALS переменную, чтобы избежать присвоения переменной V:


debug:       $(for v,$(V) $(MAKECMDGOALS), \         $(if $(filter debug,$v),,$(warning $v = $($v))))

Теперь можно выводить переменные просто перечислив их в командной строке. Однако, я не рекомендую этот способ, так как предупреждения make о невозможности обновлении переменных (так как они указаны как цели) могут сбить с толку:


$ make debug PATH SHELLmakefile:2: USERNAME = Ownermakefile:2: SHELL = /bin/sh.exemake: debug is up to date.make: *** No rule to make target USERNAME. Stop.

В то время как make выводит команды из сценариев цели до их выполнения, он не выводит команды выполняющиеся в shell функции. Часто эти команды сложные и неуловимые в том плане, что могут выполняться как незамедлительно, так и в отложенной манере, если они были вызваны в значении рекурсивной переменной. Один из способов увидеть эти команды включить отладку в самой оболочке:


DATE := $(shell date +%F)OUTPUT_DIR = out-$(DATE)make-directories := $(shell [ -d $(OUTPUT_DIR) ] || mkdir -p $(OUTPUT_DIR))all: ;

Если это запустить с опцией отладки sh, мы увидим:


$ make SHELL="sh -x"+ date +%F+ '[' -d out-2004-05-11 ']'+ mkdir -p out-2004-05-11

Можно заметить, что также выводятся значения всех переменных и выражений.


Часто встречаются сильно вложенные выражения, например, для оперирования с именами файлов:


FIND_TOOL = $(firstword $(wildcard $(addsuffix /$(1).exe,$(TOOLPATH))))

Ничего хорошего в отладке таких выражений нет. Один из разумных подходов будет их разворот и печать каждого подвыражения:


$(warning $(TOOLPATH))$(warning $(addsuffix /$(1).exe,$(TOOLPATH)))$(warning $(wildcard $(addsuffix /$(1).exe,$(TOOLPATH))))

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


Общие сообщения об ошибках


В руководстве make есть замечательный раздел со списком сообщений об ошибках make и их причин. Мы рассмотрим немного из наиболее часто встречаемых. Некоторые описанные проблемы, строго говоря, не являются ошибками make, такие как синтаксические в командных сценариях, но все же обычными проблемами для разработчиков. Полный список смотри в руководстве.


Сообщение make об ошибке имеет стандартный формат:


    makefile:n: *** message. Stop

или:


    make:n: *** message. Stop.

где makefile строка это имя файла или импортированного файла в котором произошла ошибка. Следующая часть номер строки, в которой произошла ошибка, далее следуют три звездочки, и, наконец, само сообщение.


Заметим, что это задача make запускать другие программы и таким образом, если при этом возникают ошибки, скорее всего проблемы в твоём makefile вызвали ошибки в этих других программах. Для примера, ошибки оболочки могут быть из-за плохо сформированных командных сценариев, или ошибок компилятора из-за некорректных аргументов командной строки. Выяснение того, какая программа выдала сообщение об ошибке первоочередная задача при решении проблемы. К счастью, сообщения make довольно очевидны.


Синтаксические ошибки


Обычно это типографические ошибки: пропущенные скобки, пробелы после запятых в параметрах функции, и так далее.


Одна из наиболее частых ошибок для новых пользователей make это опускание скобок вокруг имен переменных:


foo:     for f in $SOURCES; \     do                 \                       \     done

Скорее всего, make развернёт переменную $S в ничего, и оболочка выполнит цикл только раз со значением OURCES в f. В зависимости от того, что ты собрался делать с f, можно получить забавные сообщения оболочки:


    OURCES: No such file or directory

но можно и не получить сообщения вовсе. Помни имена переменных обрамляются скобками.


missing separator


Сообщение:


    makefile:2:missing separator. Stop.

или (в GNU make пер.):


    makefile:2:missing separator (did you mean TAB instead of 8 spaces?). Stop.

обычно означает make искал разделитель, такой как :, =, или табуляцию и не нашел ни одного. Вместо этого, он нашел что-то что он не понял.


commands commence before first target


Эта ошибка появляется в основном в середине makefile, когда строка вне командного сценария начинается с отступа (пробелы или символ табуляции). make сделает все возможное, чтобы устранить неоднозначность этой ситуации, но если строка не может быть идентифицирована как присваивание значения, условное выражение или многострочное определение макроса, make решит что это неправильно размещенная команда.


unterminated variable reference


Это простая, но распространённая ошибка. Она означает, что ты забыл закрыть имя переменной или вызов функции правильным количеством скобок. С сильно вложенными вызовами функций и именами переменных make файлы становятся похожими на Lisp! Избежать этого поможет хороший редактор, который умеет сопоставлять скобки, такой как Emacs.


Ошибки в командных сценариях


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


Мы обсуждали пропущенные точки с запятыми в разделе "лучшие практики", поэтому не будем на этом останавливаться здесь.


Классическое сообщение:


    bash: foo: command not found

выводится, когда оболочка не смогла найти команду foo. Так, оболочка поискала в каждой папке из переменной PATH исполняемый файл и не нашла совпадений. Чтобы исправить такую ошибку, нужно обновить PATH переменную, обычно в .profile (Bourne shell), .bashrc (bash) или .cshrc (C shell). Конечно, можно также установить PATH в самом makefile, и экспортировать PATH из make.


Если же команда завершилась с ошибкой, она выходит с ненулевым статусом выхода. В этом случае, make отчитается об ошибке со следующим сообщением:


$ maketouch /foo/bartouch: creating /foo/bar: No such file or directorymake: *** [all] Error 1

Здесь touch команда не сработала, что напечатало своё собственное сообщение объясняющее сбой. Следующая строка это итоговая ошибка make. Упавшая цель в makefile указана в квадратных скобках, а затем статус выхода упавшей программы. Если программа вышла по сигналу, а не с ненулевым статусом выхода, то make напечатает более подробное сообщение.


Заметим также, что команды под знаком @ также могут упасть. В этом случае сообщение об ошибке может возникнуть как будто оно из ниоткуда.


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


No Rule to Make Target


Это сообщение имеет две формы:


    make: *** No rule to make target XXX. Stop.

и:


    make: *** No rule to make target XXX, needed by YYY. Stop.

Это означает, что make решил обновить файл XXX, но make не смог найти ни одного правила для выполнения работы. make ищет во всех явных и неявных правилах в его базе данных прежде чем сдаться и вывести это сообщение.


Есть три причины для этой ошибки:


  • В твоем makefile отсутствует необходимое правило для обновления файла. В этом случае тебе необходимо добавить правило с описанием как построить цель.
  • В makefile опечатка. Или make ищет неверный файл или в правиле построения этого файла указан неверный файл. Если в makefile используются переменные, то опечатки становится еще труднее отыскать. Иногда единственный путь быть точно уверенным в значении сложного имени файла это напечатать его или печатая переменную напрямую или исследуя внутреннюю базу данных make.
  • Файл должен быть, но make не находит его или из-за того, что его нет, или make не знает где его искать. Конечно, иногда make абсолютно прав. Файла нет похоже мы забыли его скачать из VCS. Еще чаще, make не может найти файл из-за того, что исходник расположен где-то еще. Иногда исходник в другом дереве исходников, или может файл генерируется другой программой и создался в папке артефактов сборки.


    Overriding Commands for Target


    make позволяет только один командный сценарий для цели (за исключением :: правил, которые редко используются). Если встретится больше чем один командный сценарий для одной цели, make выведет предупреждение:


    makefile:5: warning: overriding commands for target foo
    

    Также он может вывести сообщение:


    makefile:2: warning: ignoring old commands for target foo
    

    Первое предупреждение показывает строку, на которой был найден второй сценарий команд; тогда как второе предупреждение указывает на позицию исходного переопределённого командного сценария.



В сложных makefile цели часто определены несколько раз, каждый раз добавляя еще собственные требования. Одно из этих определений цели обычно содержит командный сценарий, но в ходе разработки или отладки очень легко добавить еще один и не заметить, что существующий набор команд уже переопределен.


Например, мы могли бы определить общную цель во включаемом файле:


# Create a jar file.$(jar_file):        $(JAR) $(JARFLAGS) -f $@ $^

и позволим нескольким отдельным makefile добавить свои собственные требования. Мы могли бы записать в makefile:


# Set the target for creating the jar and add prerequisitesjar_file = parser.jar$(jar_file): $(class_files)

Если непреднамеренно добавить командный сценарий в такой makefile, make выдаст предупреждение переопределения.

Подробнее..

MacOS и мистический minOS

24.12.2020 02:20:22 | Автор: admin

После трёхлетнего перерыва актуальная версия sView стала снова доступна на macOS. Релиз sView 20.08 обещал поддержку macOS 10.10+, но что-то пошло не так и несколько пользователей обратились со странной проблемой - системы macOS 10.13 и 10.14 отказались запускать приложение с сообщением о необходимости обновиться до macOS 10.15

Сказать, что ошибка меня озадачила - сильно преуменьшить степень моего негодования, ведь магическая цифра 10.15 нигде не фигурировала ни в скриптах сборки, ни в ресурсах sView! Более того, приложение лично было проверено на более старой версии системы, а именно - на macOS 10.10.

Немного предыстории. В далёком 2011 году вышла первая сборка sView для OS X 10.6 Snow Leopard, и шесть лет именно эта версия системы оставалась минимальным требованием для запуска sView. Поддержка относительно старых версий операционных систем даёт максимальный охват потенциальных пользователей, но требует дополнительных усилий.

Практика разработки Windows, Linux, Android и macOS приложений показывает, что предположения о том, что собранное приложение "вроде должно работать" на всех версиях систем периодически дают сбой, и проблемы совместимости всплывают самым неожиданным образом. В таких случаях возможность проверить работоспособность приложения на разных (в том числе самых старых, формально поддерживаемых) системах становится жизненно необходимой.

Однако старая версия OS X требует такого же старого устройства, так как установить систему на устройство, выпущенное позднее самой системы, зачастую не представляется возможным. Проблему могли бы решить средства виртуализации, однако в случае с macOS дела с ними обстоят не лучшим образом.

Также понадобится и подходящий сборочный инструментарий. В прошлом, сборка приложения для нужной версии OS X требовала наличия нужной версии SDK в XCode. Однако упаковка нескольких SDK в XCode существенно увеличивала размер установки и старые версии SDK быстро исключались из новых версий XCode, осложняя сборку приложений для старых систем.

Для обеспечения совместимости с OS X 10.6 Snow Leopard, приложение sView долгое время собиралось на OS X той же версии, предустановленной на старом MacBook. При этом несколько версий OS X было установлено на внешний жёсткий диск для тестирования.

К счастью, со временем разработчики Apple существенно улучшили инструментарий, внедрив версионизацию на уровне заголовочных файлов, опций компилятора и линковщика. Теперь, XCode поставляется всего с одной версией macOS SDK - с самой последней, - но приложение можно собрать с совместимостью с более старыми версиями macOS посредством:

  • переменной окружения MACOSX_DEPLOYMENT_TARGET
    (т.е., export MACOSX_DEPLOYMENT_TARGET=10.0);

  • или флага компилятора -mmacosx-version-min
    (т.е., EXTRA_CXXFLAGS += -mmacosx-version-min=10.0).

В случае CMake соответствующий параметр называется CMAKE_OSX_DEPLOYMENT, а у qmake - QMAKE_MACOSX_DEPLOYMENT_TARGET.

Настройки проекта в XCode 11 позволяют выбрать минимальной платформой даже OS X 10.6, но данный выбор приводит только к ошибкам при сборке и Hello World удалось собрать только при выборе 10.7 или версия новее. Впрочем, OS X 10.6 Snow Leopard вышла в далёком 2009 году - то есть одиннадцать лет назад, - и едва ли имеет активных пользователей. Какую же версию выбрать в качестве минимальной?

OS X 10.10 Yosemite была выпущена около 6 лет назад и на 6 релизов "старее" самой актуальной на данный момент macOS 11.0 Big Sur. Трудно представить пользователей более старой OS X с учётом агрессивной политики обновлений Apple. Помимо прочего, OS X 10.10 уже была установлена на моём старом MacBook - слишком старым для разработки, но ещё живом для проверки работоспособности собранного приложения.

В попытке обновить старичка mid-2010 MacBook выяснилось, что свежие версии macOS более не поддерживают такие устройства , а последней совместимой версией оказалась macOS 10.13 High Sierra выпущенная в 2017 году.
Таким образом, Apple лишила свой продукт программных обновлений спустя 7 лет! При этом магазин приложений Apple более не позволяет загрузить старые версии macOS - то есть и обновить OS X 10.10 до macOS 10.13 не получится обычным способом.

Для сборки sView на свежем инструментарии в Makefile проекта была прописана версия 10.10, а в Info.plist был указан параметр LSMinimumSystemVersion=10.0. Сама сборка была осуществлена на macOS 10.15, установленной на относительно свежем Mac mini 2018, и протестирована на макбуке с OS X 10.10 - приложение заработало и было опубликовано на сайте!

и тут, как снег на голову, пришли сообщения пользователей об ошибках запуска sView на версиях macOS, новеепротестированной. Вздор! Откуда система вообще могла взять цифру 10.15, если LSMinimumSystemVersion указывает на 10.10 - а это единственный ранее известный мне источник для подобных сообщений macOS об ошибках?

В слепую локализовать проблему не удавалось - поиски 10.15 в архиве с приложением и в сборочных скриптах ни к чему не привели. Поэтому было найдено временное подопытное устройство с macOS 10.13, выводящее такое же сообщение об ошибке. Удивительно, но запуск исполнительного файла sView из терминала происходил без всяких проблем и ошибок!

Эксперименты показали, что что-то не так непосредственно с исполнительным файлом sView, и в конце концов, утилита otool -l выявила источник проблемы:

Load command 9        cmd LC_BUILD_VERSION    cmdsize 32   platform macos        sdk 10.15      minos 10.15     ntools 1       tool ld    version 450.3

Информации о загадочном minos нашлось не много в интернете, но удалось выяснить, что данное поле появилось в заголовке бинарный файлов macOS относительно недавно. Но этого факта оказалось достаточно, чтобы ответить на первый вопрос - как так получилось, что более старая версия OS X 10.10 запускала sView без проблем, а новые macOS 10.13-10.14 выдавали ошибки? Да просто OS X 10.10 ничего не знает о существовании нового поля minos!

Оставался последний вопрос - где в процессе сборки приложения закралась ошибка? Изучение пакета sView выявило, что поле minos присутствовало только библиотеках и исполняемом файле самого проекта, но не в библиотеках FFmpeg, собранных схожим образом. То есть проблема была явно в Makefile проекта. Как оказалось,флаг -mmacosx-version-min передавался компилятору через переменную EXTRA_CXXFLAGS, но не передавался линковщику. Добавление флага в переменную EXTRA_LDFLAGS наконец-то решило проблему:

TARGET_OS_VERSION = 10.10EXTRA_CFLAGS   += -mmacosx-version-min=$(TARGET_OS_VERSION)EXTRA_CXXFLAGS += -mmacosx-version-min=$(TARGET_OS_VERSION)EXTRA_LDFLAGS  += -mmacosx-version-min=$(TARGET_OS_VERSION)

Оригинальная публикация на английском может быть найдена здесь.

Подробнее..

Публикация Go приложения в GitHub

15.02.2021 10:17:19 | Автор: admin

Пост представляет собой контрольный список (checklist) и его реализацию при публикации Go приложения на Github'е.

TLDR:

  • Makefile как входная точка для выполнения основных действий

  • Линтеры и тесты как инструменты повышающие качество кода

  • Dockerfile как способ распространения приложения

  • Github Actions как возможность автоматической сборки и выкладки приложения при новых изменениях

  • Goreleaser как инструмент для публикации релизов и пакетов

Результатом должно быть опубликованное приложение с настроенными инструментами которые позволяют легко сопровождать приложение в процессе его существования. В качестве реального примера я буду рассматривать утилиту pgcenter.


Disclaimer: Приведенный список не являются абсолютной истинной и является лишь субъективным списком вещей к которым я пришел в процессе публикации приложении. Список может дополняться и изменяться. Все это одно большое ИМХО, если у вас есть альтернативный взгляд или вы уверены/знаете что какие-то вещи можно сделать еще лучше, обязательно дайте знать в комментах.

Будем двигаться от простого к сложному. В случае встречи имен файлов, все они взяты относительно корня проекта где находится код приложения.

Makefile

Makefile хранит в себе служебные сценарии которые приходится часто выполнять при разработке приложения:

  • выполнение линтеров и тестов

  • сборка приложения

  • запуск приложения

  • сборка артефактов типа пакетов, docker образов и т.п.

  • публикация артефактов в сторонние репозитории

  • выполнение операций относительно внешних систем, например SQL миграции если речь идет о корпоративных приложениях В Makefile складываются рутинные операции выполнять которые приходится регулярно. Основная цель Makefile это помочь вам не держать в голове все нужные команды, параметры и аргументы, а собрать и их в одном месте и при необходимости выполнить их и получить результат. Позже Makefile также будет основным сценарием для запуска этих же рутинных процедур в CI/CD. Минимальная версия Makefile может выглядеть так:

PROGRAM_NAME = pgcenterCOMMIT=$(shell git rev-parse --short HEAD)BRANCH=$(shell git rev-parse --abbrev-ref HEAD)TAG=$(shell git describe --tags |cut -d- -f1)LDFLAGS = -ldflags "-X main.gitTag=${TAG} -X main.gitCommit=${COMMIT} -X main.gitBranch=${BRANCH}".PHONY: help clean dep build install uninstall .DEFAULT_GOAL := helphelp: ## Display this help screen.@echo "Makefile available targets:"@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "  * \033[36m%-15s\033[0m %s\n", $$1, $$2}'dep: ## Download the dependencies.go mod downloadbuild: dep ## Build pgcenter executable.mkdir -p ./binCGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} go build ${LDFLAGS} -o bin/${PROGRAM_NAME} ./cmdclean: ## Clean build directory.rm -f ./bin/${PROGRAM_NAME}rmdir ./bin

Давайте разберем важные моменты в приведенном Makefile.

  • В начале файла указываем служебные переменные которые потребуются дальше. Переменных немного, называние программы и информация из Git которая используется при сборке. В частности коммит, тег и ветка которые через сборщика пробрасываются в код приложения и используются для формирования версии программы. При таком подходе версия всегда берется из Git и не нужно хранить строку с версией в коде приложения, помнить о ней, своевременно обновлять. Отмечу что проброс переменных не автоматическая магия, предполагается что в Git используются теги и также есть соответствующие правки в коде для приема Git переменных. Например 1 и 2.

  • Следующий интересный момент это пункт help, он реализует справку для нашего Makefile - обратите внимание на комментарии начинающиеся с двойной решетки после названий пунктов. Эти комментарии как раз и используются в качестве справки если вызвать make без аргументов или явно make help.

  • Следующие пункты являются базовыми для жизненного цикла Go приложения: загрузка зависимостей, сборка и очистка каталога где хранится собранная программа.

При желании список пунктов можно дополнить, что я и буду делать дальше по ходу текста.

Линтеры и тесты

Следующий шаг добавление линтеров. Основная задача линтеров проверять код на предмет "странных" конструкций которые не соответствуют принятым стилям программирования или даже хуже, могут быть неоптимальными и приводить к каким-либо ошибкам. Наличие линтеров позволяют поддерживать хорошее качество кода, ориентироваться на правильный стиль написания кода принятый в языке. Использование линтеров необязательно, но крайне желательно. В go есть масса разных линтеров, я пришел пока к использованию golangci-lint и gosec. Первый включает в себя большой набор разных линтеров (включены при этом не все), второй является линтером с уклоном в соблюдение правил информационной безопасности.

Выполнение линтеров также регулярная задача, поэтому также добавляем их в Makefile

lint: dep ## Lint the source filesgolangci-lint run --timeout 5m -E golintgosec -quiet ./...

В приведенном случае для выполнения golangci-lint выставлен таймаут, через флаг -E включаются дополнительные линтеры. Выполнение gosec особо ничем не примечательно, просто рекурсивный обход каталогов.

Очевидно что golangci-lint и gosec должны быть установлены в системе. Их установка проста, описывать тут не буду.

Также код сопровождается тестами, добавим и их выполнение тоже.

test: dep ## Run testsgo test -race -p 1 -timeout 300s -coverprofile=.test_coverage.txt ./... && \    go tool cover -func=.test_coverage.txt | tail -n1 | awk '{print "Total test coverage: " $$3}'@rm .test_coverage.txt

Команда запускает тесты, формирует файл с покрытием кода этими тестами и печатает результат покрытия.

Dockerfile

Следующим этапом является Dockerfile, который позволит собирать Docker образы нашего приложения.

# stage 1: buildFROM golang:1.15 as buildLABEL stage=intermediateWORKDIR /appCOPY . .RUN make build# stage 2: scratchFROM scratch as scratchCOPY --from=build /app/bin/pgcenter /bin/pgcenterCMD ["pgcenter"]

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

Помимо сборки Docker образа потребуется место откуда другие пользователи смогут его забрать, например этим местом может быть Docker Hub. Для размещения там образа потребуется аккаунт и реквизиты (логин/пароль).

Сборка и публикация образа также регулярная операция, поэтому добавляем команды в Makefile.

docker-build: ## Build docker imagedocker build -t lesovsky/pgcenter:${TAG} .docker image prune --force --filter label=stage=intermediatedocker-push: ## Push docker image to registrydocker push lesovsky/pgcenter:${TAG}

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

Github Actions

Теперь когда у нас есть способ для сборки приложения (Makefile) и публикации (Dockerfile), нам нужен механизм который поможет автоматически выполнять сборку и публикацию обновлений приложения. Здесь нам поможет Github Actions.

Однако в этом месте мы начинаем соприкасаться с организацией процесса, как новые изменения становятся частью существующего кода. Тема довольно обширная, об этом написано много постов, выработаны целые подходы со своими правилами. Поэтому те кто в теме уже и так все знают, а кто не в теме пусть отправляется на поиски чтива Git Flow, Github Flow, Gitlab Flow и их сравнение друг с другом.

В нашем случае все просто. Все изменения будут вливаться в master ветку. От ветки master мы создадим release ветку которая и будет источником релизов. Когда мы захотим сделать релиз, просто создадим новый тег и подтянем все изменения из master в release.

Теперь когда у нас есть понимание того как вносить изменения и делать релизы можно перейти к настройке workflow в Github Actions. Кто знает нормальный однословный русский перевод слову wokflow дайте знать в комментариях.

Будет два workflow:

  • Default (.github/workflows/default.yml) - это flow по-умолчанию, выполняет тесты при поступлении новых изменений.

  • Release (.github/workflows/release.yml) - это релизный flow делает тесты, сборку Docker образов и пакетов для пакетных менеджеров.

Этот workflow запускается при наступлении push и pull request событий в ветке master. Здесь всего одна задача (job), запуск линтеров и тестов. Github Actions имеет хорошие возможности для кастомизации и позволяют описывать очень сложные сценарии. В нашем случае таким примером кастомизации является запуск и выполнение в специально подготовленном контейнере где есть все необходимое для осуществления тестов.

Второй workflow.

---name: Releaseon:  push:    branches: [ release ]jobs:  test:    runs-on: ubuntu-latest    container: lesovsky/pgcenter-testing:v0.0.1    steps:      - name: Checkout code        uses: actions/checkout@v2      - name: Prepare test environment        run: prepare-test-environment.sh      - name: Run lint        run: make lint      - name: Run test        run: make test  build:    runs-on: ubuntu-latest    needs: test    steps:      - name: Checkout code        uses: actions/checkout@v2        with:          fetch-depth: 0      - name: Build image        run: make docker-build      - name: Log in to Docker Hub        run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}      - name: Push image to Docker Hub        run: make docker-push  goreleaser:    runs-on: ubuntu-latest    needs: [ test, build ]    steps:      - uses: actions/checkout@v2        with:          fetch-depth: 0      - uses: actions/setup-go@v2        with:          go-version: 1.15      - uses: goreleaser/goreleaser-action@v2        with:          version: latest          args: release --rm-dist        env:          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Релизный workflow чуть больше, и запускается он при push событиях в release ветке. Этот workflow также включает в себя выполнение линтеров и тестов. Но также тут есть еще две задачи - build и goreleaser.

В задаче build выполняется сборка и публикация Docker образа. Обратите внимание, что используются секреты, которые предварительно нужно указать в разделе Secrets, в настройках Github репозитория.

В задаче goreleaser выполняется публикация релиза в разделе Releases репозитория. Настройки goreleaser определим позже. Здесь также используется секрет GITHUB_TOKEN его указывать нигде не нужно, он создается автоматически для нужд workflow.

Goreleaser

Последний шаг это публикация релиза и дополнительная сборка пакетов. Кроме Docker образов существуют способы распространения с помощью пакетных менеджеров. Наиболее распространенные это deb и rpm пакеты. Есть и другие варианты, но для меня они экзотичны
и их я рассматривать не буду. Для всего этого нам потребуется goreleaser который и сделает всю работу по сборке. Настройки определяются в .goreleaser.yml

before:  hooks:  - make depbuilds:  - binary: pgcenter    main: ./cmd    goarch:      - amd64    goos:      - linux    env:      - CGO_ENABLED=0    ldflags:      - -a -installsuffix cgo      - -X main.gitTag={{.Tag}} -X main.gitCommit={{.Commit}} -X main.gitBranch={{.Branch}}archives:  - builds: [pgcenter]changelog:  sort: ascnfpms:  - vendor: pgcenter    homepage: https://github.com/lesovsky/pgcenter    maintainer: Alexey Lesovsky    description: Command-line admin tool for observing and troubleshooting Postgres.    license: BSD-3    formats: [ deb, rpm ]

Важным моментом является то что goreleaser не имеет интеграции с Makefile и сборку нужно описывать отдельно в формате goreleaser правил. Поэтому важно описать сборку точно так же как это осуществляется в Makefile, т.е. указать все те же флаги, переменные окружения и т.п. В секции nfpms описываем метаданные пакета и указываем необходимые форматы пакетов.

Собственно на этом все. Можно фиксировать изменения, пушить в репозиторий, перейти в Actions и наблюдать за тем как выполняются workflow. При успешном выполнении, можем создать новый тег, и влить изменения в релизную ветвь и также понаблюдать за прогрессом в Actions.

Спасибо за внимание, если у кого есть дополнения, замечания, вопросы, пожелания - Go в комменты.

Ссылки

Подробнее..
Категории: Github , Github actions , Go , Makefile , Goreleaser , Golangci-lint , Gosec

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru