Привет, Хабр! Программирую на C++ / Qt / QML в среде разработки QtCreator уже 6-ой год. У меня есть определенные пересечения мыслей с мозгом груга и еще мне постоянно хочется избавиться от глупой и рутинной работы, которая есть на разных этапах разработки. Одна из таких работ — возня с IDE и рабочим окружением, особенно в мире C++ разработки. В статье постараюсь раскрыть проблему и описать свой текущий подход к решению.
Проблема
Мы пишем кросс-платформенный десктоп C++ клиент-серверный продукт, и у наших разработчиков довольно развесистое рабочее окружение. В системе должно быть многообразие библиотек, некоторые переменные окружения. Иногда это добро расширяется. Плюс есть множество полезных настроек в самой IDE, многими плюшками с моей подачи пользуются коллеги: настроенные форматтеры кода определенных версий, некоторые удобные сниппеты, шаблоны создаваемых библиотек и файлов, конфиг статического анализатора и многие другие, тут на целую статью наберется.
-
Если разработка идет на нескольких компьютерах (домашний, рабочий, ноутбук), то нужно везде поддерживать одни настройки, версии библиотек и прочие детали окружения. Где-то я обновил систему, где-то не обновлял, что-то случайно затерлось. В итоге на всех рабочих компьютерах рабочее окружение отличается, это раздражает
-
Если в команду приходит новый человек, то на первоначальную настройку среды и сборку проекта уходит от одного до пары рабочих дней. Любой крупный проект обрастает нюансами, в которых нужно разобраться
-
Иногда проект изменяется таким образом, что у всех разработчиков внезапно ломается рабочее окружение, нужно что-то поменять. Добавилась необходимая переменная окружения, библиотечная системная зависимость и тд. И всю следующую неделю рабочий чатик разрывается одним и тем же вопросом «а шо делать», несмотря на то, что этот вопрос задавался 2-мя экранами выше. Справедливо, мы ведь работаем, а не чатики читаем 🙂
-
Если нужно на рабочем компьютере переустановить операционную систему. Редко, но случается, особенно под Linux. Тогда даже опытный разработчик возвращается к проблеме номер 2 и кучу времени тратит чтобы восстановить свое рабочее окружение и продолжить работать, ведь последний раз он с нуля все настраивал с годик назад
-
Иногда хочется поэкспериментировать с рабочим окружением, но это может привести к его окирпичиванию. На рефлекторном уровне гасится желание ковырять это окружение для улучшений, особенно после неприятных историй, которые приводят к п.4. Работает — не трожь!
Наступил день, когда эти проблемы меня доконали и я решил что-то с этим сделать.
Требования
Сформулировал требования к решению
-
Достаточно поддержки одной операционной системы — Linux
Продукт кросс-платформенный, но конкретный разработчик чаще всего сидит на одной системе. Распределение примерно 50 на 50 (Linux, Windows), зависит от предпочтения. Мой выбор продиктован личным предпочтением + пониманием, что автоматизировать разворачивание Linux среды будет сильно проще.
-
Быстрое развертывание (пара минут)
-
Поддержка нескольких версий
-
Быстрая и удобная доставка обновлений до пользователя
-
Коробочное решение — открыл и работаешь
-
Плавность работы, как у нативного приложения
После недолгих раздумий я пришел к Docker.
Решение
DockerFile
Это был мой первый опыт написания Dockerfile. После пары первых попыток столкнулся с тем, что при билде контейнера система просит указать таймзону в интерактивном режиме (для каких-то зависимостей это нужно), на этом ожидаемо валится билд.
p.s. почему-то не вижу в настройках вставки кода язык «Dockerfile», пускай будет «C#» : — )
FROM ubuntu:20.04 ENV TZ=Asia/Yekaterinburg ENV DEBIAN_FRONTEND=noninteractive
Пакеты. Я несколько раз полностью менял стратегию того, как я скачиваю пакеты. По итогу пришел к тому, что update и install должны всегда быть в рамках одного шага, потому что их разделение сработает только в первый билд, а если потом поменять зависимости, то update не отработает и будет проблемес, если он успел устареть. А еще после каждого скачивания в этом же шаге подчищать за собой, это я подсмотрел на форумах.
Итоговый шаблон для скачивания пакетов:
RUN apt update -y && apt install \ lib1 lib2 ... \ -y && apt clean -y && apt autoremove --purge -y
Зависимости, которые понадобились для запуска QtCreator в докере, собраны опытным путем. Выставлял переменную окружения (вроде QT_DEBUG_PLUGINS=1) и это давало расширенный лог ошибки запуска. Там было видно какой библиотеки не хватает. Так было проделано N раз, по количеству недостающих библиотек
RUN apt update -y && apt install \ libgl1 libxkbcommon-dev libegl1 libfontconfig-dev libgssapi-krb5-2 \ libdbus-1-3 libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \ libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-cursor-dev \ -y && apt clean -y && apt autoremove --purge -y
Опущу длинный и неинтересный набор зависимостей для сборки нашего проекта, этот список у каждого будет индивидуальным, но смысл такой же, скачиваем нужные пакеты
Чтобы внутри контейнера работал отладчик, добавил это, решение взял отсюда
RUN echo 0 > /etc/sysctl.d/10-ptrace.conf
Эту команду также нужно выполнить на хосте.
Локали. Видел разные решения на форумах, работали с переменным успехом, в итоге получилось что-то такое
RUN apt install locales -y && \ locale-gen en_US.UTF-8 ru_RU.UTF-8 && \ update-locale LANG=ru_RU.UTF-8 ENV LC_ALL=ru_RU.UTF-8 ENV LANG=en_US.UTF-8
Свою задачу выполняет и ладно
Создание юзера. Достаточно важный пункт, т.к. по умолчанию контейнер стартует под рутом. В дальнейшем мы будем прокидывать директории хоста в контейнер, например исходники проектов, над которыми идет работа. Любые изменения в файлах/директориях со стороны контейнера приведут к изменению прав на root, и с хоста они будут по умолчанию недоступны без sudo. Неудобненько.
Удивительно, но на этот пункт было потрачено много усилий, почти никакие не-интерактивные способы с интернетов не работали нормально.
ENV USER_NAME=uzver RUN adduser $USER_NAME; usermod -aG sudo $USER_NAME; echo "$USER_NAME:123" | chpasswd USER $USER_NAME ENV HOME=/home/$USER_NAME WORKDIR $HOME
После этих изменений, качать пакеты в Dockerfile не получится, т.к. у обычного юзера нет прав, поэтому все зависимости желательно качать до того как установим пользователя. С другой стороны, именно от лица пользователя нужно копировать в образ все что нам нужно (среду разработки, утилиты, сборочный комплект, настройки и тд), потому что пользоваться контейнером будем от имени созданного пользователя
Примерно так добавляем саму среду разработки. Я брал архив тут
COPY --chown=$USER_NAME qtcreator14 $HOME/qtcreator ENV PATH=$PATH:$HOME/qtcreator/bin
При копировании указываем принадлежность файлов юзеру. И добавляем пути в PATH если им там нужно быть. Для всего остального что нужно копировать принцип такой же.
Многие конфиги, настройки профилей сборки, компиляторов, отладчиков. Я заранее настраивал это в контейнере и копировал конфиги на хост, чтобы они всегда были одинаковыми, с одинаковыми UUID различных сущностей и тд, чтобы была строгая детерминированность при различных запусках
Запуск контейнера с хоста
Чтобы открывались окошки, перед запуском контейнера необходимо сказать своему x-server, чтобы он давал к себе подключиться. Сначала сделал так
xhost +
Потом подумал, что «access control disabled, clients can connect from any host» наверное не хочу иметь подключения от any host. Можно свести команду к
xhost +local:$USER
Уже приятнее. Эту команду необходимо выполнять каждый раз перед запуском контейнера
Также перед запуском хочется иметь на хосте директории, которые мы пробрасываем через volume. Например, для утилиты ccache директорию ~/.ccache. Соберем всё в запускаемый скрипт my-ide.bash
#!/bin/bash xhost +local:$USER mkdir -p $HOME/.ccache docker start -i CONTAINER_NAME
Точка входа
Сначала я думал сделать точку входа команду qtcreator, которая запускает среду разработки. Тогда при запуске контейнера сразу запускается окошко с IDE. Проблема в том, что не было прямого доступа к терминалу контейнера, а при останове самого qtcreator, контейнер завершал свое выполнение. Иногда очень нужно иметь доступ к терминалу внутри контейнера
Вариант 2 — стартовать bash, тогда пользователь при запуске оказывается внутри оболочки bash и может открывать и закрывать программы, оставаясь в этой оболочке. Но тогда пользователь всегда при запуске контейнера с IDE будет вынужден прописывать эту команду qtcreator руками.
Нашел вариант побороть проблему. Итоговый Entrypoint:
/bin/bash --init-file /home/uzver/utils/init_script.bash
Самое простое (к сложному чуть позже) возможное содержимое этого файла, в моем случае такое:
#!/bin/bash qtcreator&
Таким образом окошко IDE запускается в фоновом режиме и пользователь сразу имеет приглашение ко вводу в терминале контейнера, идеально.
Я также использовал возможности такого способа для кастомизации IDE по некоторым параметрам. Например, некоторые коллеги используют автоформатирование, а некоторые нет. Для некоторых проектов нужен qbs одной версии, для других другой.
Эти параметры задаются переменными среды на хосте, чтобы при обновлении контейнера, при его начальном запуске, эти параметры подтянулись и донастроили систему. Получилось примерно так
#!/bin/bash if [ "$CONTAINERIDE_ALREADY_CONFIGURED" != "1" ]; then export CONTAINERIDE_ALREADY_CONFIGURED=1 if [ "$CONTAINERIDE_AUTOCLANGFORMAT" == "0" ]; then crudini --set "$HOME/.config/QtProject/QtCreator.ini" Beautifier "General\\autoFormatOnSave" false else crudini --set "$HOME/.config/QtProject/QtCreator.ini" Beautifier "General\\autoFormatOnSave" true fi if [[ "$CONTAINERIDE_QBSVERSION" == 1.20* ]]; then export PATH=$PATH:$HOME/qbs-1.20/bin else export PATH=$PATH:$HOME/qbs-2.3.0/bin fi fi qtcreator&
утилита crudini отлично выполняет свою роль — протолкнуть значение по имени секции + по имени ключа в конфигурационный файл .ini
Создание контейнера из образа
Эту процедуру я вынес в скрипт my-ide-update.bash
#!/bin/bash # Аргумент запуска - tag, по умолчанию latest DEFAULT_TAG="latest" TAG="${1:-$DEFAULT_TAG}" # Имя образа IMAGE_NAME=MY_IMAGE_NAME # Имя контейнера CONTAINER_NAME=MY_CONTAINER_NAME # Программа, которая будет запущена PROGRAMM="/bin/bash --init-file /home/uzver/utils/init_script.bash" docker pull $IMAGE_NAME:$TAG docker rm $CONTAINER_NAME docker create \ -ti \ --cap-add=SYS_PTRACE \ --name=$CONTAINER_NAME \ --net=host \ -e DISPLAY \ -e CONTAINERIDE_AUTOCLANGFORMAT \ -e CONTAINERIDE_QBSVERSION \ -v $CONTAINERIDE_PROJECTS:/home/uzver/projects \ -v $HOME/.ccache:/home/uzver/.ccache \ -v /dev/dri:/dev/dri \ $IMAGE_NAME:$TAG $PROGRAMM
Некоторые пояснения:
#Чтобы иметь прямой доступ к терминалу системы внутри контейнера -ti #Чтобы работал отладчик --cap-add=SYS_PTRACE #ENV CONTAINERIDE_PROJECTS пользователь определяет на своем хосте, эта директория монтируется в контейнер как директория для проектов по умолчанию -v $CONTAINERIDE_PROJECTS:/home/uzver/projects #ENV CONTAINERIDE_AUTOCLANGFORMAT, CONTAINERIDE_QBSVERSION пользователь определяет на своем хосте, они помогут в донастройке системы под эти параметры -e CONTAINERIDE_AUTOCLANGFORMAT -e CONTAINERIDE_QBSVERSION
Раньше эта процедура была связана с запуском контейнера и там был примерно такой код, опускаю лишние детали
DOCKER_PS=$(docker ps -a) # это чтобы записать вывод команды pull в файл для дальнейшего анализа docker pull $IMAGE_NAME | tee OUT_FILE OUT_STRING=$(cat OUT_FILE) COMMAND="docker create ..." xhost + mkdir -p $HOME/.ccache #Если контейнер на текущий момент не создан if [[ $DOCKER_PS != $CONTAINER_NAME ]]; then echo "Создаем контейнер на основе свежего образа" $COMMAND #Иначе, если контейнер создан и мы обновили образ, удалим контейнер и создадим новый elif [[ $OUT_STRING != "Image is up to date for" ]]; then echo "Образ обновлен. Удалим текущий контейнер и создадим новый" docker rm $CONTAINER_NAME $COMMAND else echo "Текущий контейнер использует свежий образ" fi rm OUT_FILE docker start -i $CONTAINER_NAME
Таким я видел рабочий контейнер изначально, чтобы бесшовно доставлять обновления пользователю IDE, все происходит через один скрипт. Этот скрипт проверяет обновления и скачивает их, либо запускает текущий контейнер, если обновлений нет.
Мне думалось, что если я быстро решу ново-возникшую проблему с рабочим окружением, пользователь даже не заметит проблему. Заметили. Такой подход приводил к ряду проблем:
-
Никто не любит внезапных автоматических обновлений
-
Если крупно поменялись слои образа, пользователь может попасть на 5-10 минутное ожидание, вместо одной секунды, за которую обычно запускается IDE.
-
Я бы хотел иметь команду навроде docker check IMAGE_NAME latest, которая проверяла бы соответствие локального образа с образом на сервере по определенному тэгу, но ничего подобного не нашел (с кавалерийского наскока, а глубже не разбирался). Тогда можно было бы при запуске проверять версию и ненавязчиво предлагать обновиться, если есть желание.
Вместо этого я искал в стандартном выводе команды docker pull некоторые слова, как например \*»Image is up to date for»\*, чтобы делать вывод о наличии обновления. Проблема в том, что этот вывод я получу уже после фактического обновления, а еще в том, что красивый форматированный консольный вывод от команды docker pull, сильно ломается если прогонять его через те костыли, которыми я все это подпер
Я пришел к тому, что обновление должно быть сознательным процессом. Я пришел к этому форсированно, т.к. узнал что для одних это стало поводом запускать IDE напрямую через докер команды (минуя мой скрипт), для других это стало поводом стучаться в личку и «а шо так долго не запускается, в прошлый раз запускалось быстро, наверное сломалось». Поэтому обновление и запуск разделены на 2 скрипта
Что можно улучшить
-
Кэшировать больше пользовательских данных
Сессии, сохранение положения окон, кэш поисковых запросов, паттернов файлов для поиска, … Сейчас при обновлении контейнера многие вещи затираются, т.к. они вперемешку с настройками, которые я хочу выставлять принудительно. Можно делать это только через crudini, сохраняя большинство пользовательской информации. А сами конфиги через volume использовать хостовые. Но это снижает независимость контейнера и в общем пока эту тему не трогаю -
Прокинуть драйвер звука
Возникла необходимость в разрабатываемом продукте запустить некий звук. С учетом что там дергаются драйверы, поведение отличалось от тестирования на обычном хосте. С кавалерийского наскока не разобрался как это сделать, быстрее было поставить окружение на хост, чтобы решить задачу. Но осадочек остался -
Добавить простенький браузер, файловый менеджер
Когда в коде есть ссылка или нужно открыть директорию с файлами, приходится возвращаться на хост из уютного контейнера -
Расширить линейку поддерживаемых версий сред разработки и различного инструментария
С учетом ограниченности ресурсов на текущий момент я выпускаю новые версии, когда наберется некоторое количество улучшений, которые уже недостаточно поддерживать только в локальном контейнере и хочется зафиксировать их. Старый релиз остается только в хранилище для тех кто им еще пользуется, но я не вношу туда изменения, даже если то окружение уже не соответствует действительности разработки. Жива только та версия, на которой я активно работаю, потому что я могу быстро заметить проблему. Жизнеспособность других версий под вопросом -
Сделать гигачад IDE С++ из VSCode
Я рассматривал этот вопрос, но уперся во многие вещи, которые в qtcreator есть из коробки, а в VSCode нет даже в формате расширений. Значит пришлось бы писать и поддерживать эти расширения. Если бы у меня было больше ресурса на эту задачу, я бы сделал контейнер на базе VSCode, как ультимативную C++ среду разработки. Ряд фич я подсмотрел у Clion, «можем повторить». И как игра в долгую VSCode как-то интуитивно больше нравится. Но это лишь ощущенения
Итого
Я в таком формате работаю уже больше года, и это реально доставляет мне удовольствие. Коллеги активно пользуются. Кто хоть раз вкусил свободу от мыслей про рабочее окружение, не остался равнодушным
ссылка на оригинал статьи https://habr.com/ru/articles/849432/
Добавить комментарий