Как стать автором
Обновить
3076.76
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Итак, вы унаследовали старую кодовую базу на C++. Что дальше?

Уровень сложностиСредний
Время на прочтение21 мин
Количество просмотров16K
Автор оригинала: Philippe Gaultier

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

Теперь вы отвечаете за кодовую базу на C++. Она большая, сложная и своеобразная; достаточно слишком долго на неё посмотреть, как она начинает разваливаться разными интересными способами. Иными словами, это легаси.

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

И что делать теперь?

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

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

Буду справедливым к C++: я не ненавижу его, просто этот язык — один из тех, которые люди используют неправильно, что неизбежно приводит к ужасному хаосу. C++ здесь всего лишь жертва; не беспокойтесь, комитет по развитию C++ всё исправит в C++45, добавив в стандартную библиотеку std::cmake, и вы увидите, как всё заиграет новыми красками… Впрочем, давайте вернёмся к теме статьи.

Вот краткое описание шагов, которые нужно предпринять:
  1. Заставьте код работать локально, но внеся только минимальные изменения в код и систему сборки (в идеале ноль изменений). Пока никакого крупного рефакторинга, даже если о-о-очень хочется!
  2. Заведите бензопилу и отрежьте всё, без чего можно обойтись для обеспечения фич, которые ваша компания/опенсорсный проект рекламирует и продаёт
  3. Заставьте проект перейти в 21-й век, добавив CI, линтеры, фаззинг, автоматическое форматирование и так далее
  4. После этого можно, наконец, начинать вносить в код маленькие инкрементные изменения. Повторяйте процесс, пока не перестанете просыпаться каждую ночь от кошмаров, что коварные хакеры взломают ваше приложение за секунды
  5. Рассмотрите возможность переписывания некоторых частей на безопасном по памяти языке
В целом ваша цель заключается в том, чтобы прикладывать минимальный объём усилий для обеспечения приемлемого уровня безопасности, удобства разработки, корректности и производительности проекта. Очень важно всегда помнить об этом. Об этом, а не о «чистом коде», использовании фич нового крутого языка и так далее.

Итак, давайте приступим!

Кстати, всё изложенное в статье применимо и к кодовой базе на чистом C, и к смешанной кодовой базе на C и C++. Так что если это ваш случай, продолжайте читать!

▍ Заручитесь согласием


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

Разработка ПО должна быть устойчивым и самоподдерживающимся процессом, а не тем, что вызывает выгорание спустя несколько месяцев или лет. Мы не можем заниматься ею сверхурочно, в «марше смерти» и даже в одиночку! Нам нужно убедить людей поддержать нашу инициативу, донести до них, что и почему мы делаем. И это относится ко всем: к вашему руководителю, к коллегам, даже к нетехническим специалистам. И кто знает, возможно, вернувшись из отпуска, вы увидите, что люди продолжают вашу работу, даже когда вас нет на месте.

По сути, я имею в виду следующее: объясните проблему доходчиво, перечислив несколько простых фактов, предложите решение и график его реализации. Очень просто, правда? Например:
  • Слушайте, у предыдущего сотрудника на сборку кода на его машине уходило три месяца. Мне кажется, было бы здорово, если бы мы могли делать за несколько минут.
  • Я быстренько накидал простую систему для фаззинга, и она смогла за несколько секунд устроить вылет приложения 253 раза. Интересно, что произойдёт, если пользователи попробуют проделать такое с нашим приложением в продакшене?
  • Для устранения нескольких последних срочных багов потребовался труд множества людей и две недели для деплоя в продакшен, потому что приложение можно собирать только одним сервером сборки с древней операционной системой, не поддерживаемой уже восемь лет (FreeBSD 9, если вам любопытно), который постоянно валится. А, да, и если сервер умрёт, то мы не сможем деплоить приложения, ну вот вообще никак. Разве не будет здорово, если мы сможем собирать приложение на любом дешёвом облачном инстансе?
  • Когда в продакшене возник загадочный баг, влияющий на пользователей, нам понадобилось несколько недель, чтобы найти и устранить его. Оказалось, что он был вызван повреждающим данные неопределённым поведением, а когда я пропустил наш код через популярный линтер, он мгновенно выявил ошибку. Нам нужно пользоваться этим инструментом каждый раз, когда мы вносим изменения!
  • На носу годовой аудит, а для прохождения предыдущего нам понадобилось семь месяцев, потому что аудитор был недоволен тем, что увидел. У меня есть мысли о том, как ускорить процесс.
  • Прямо сейчас в новостях говорят об уязвимости безопасности, позволяющей расшифровывать зашифрованные данные, и красть секреты. Думаю, она могла затронуть и нас, но не уверен, потому что созданная нами (копипейстом) криптографическая библиотека содержит изменения, которые никто не проверял. Мы должны всё подчистить и построить систему алертов, автоматически уведомляющую о наличии влияющих на нас уязвимостей.
А вот чего стоит полностью избегать (разумеется, все случаи полностью выдуманные и никогда-а-а со мной не происходили):
  • Мы не используем последний стандарт C++, нам следует приостановить всю работу на пару недель для апгрейда; кроме того, я понятия не имею, поломается ли что-то, потому что у нас нет тестов
  • Я изменю много чего в проекте в отдельной ветви и буду работать над ней в течение нескольких месяцев. Рано или поздно её обязательно смерджат! (голос рассказчика: этого так и не произошло)
  • Мы перепишем весь проект с нуля, это займёт максимум несколько недель
  • Мы улучшим кодовую базу, но понятия не имею, когда это будет сделано и что конкретно мы будем делать
Итак, допустим, вы заручились поддержкой всех важных для проекта людей. Перейдём к процессу:
  • Каждое изменение должно быть маленьким и инкрементным. Приложение работает до внесения изменения и после. Тесты выполняются успешно, линтеры всем довольны, для внесения изменений не приходится искать обходных путей (исключения бывают, но действительно в исключительных случаях)
  • Если нужно срочно устранить баг, то это можно сделать обычным образом, не мешая ничему другому
  • Каждое изменение — это измеримое улучшение, которое можно объяснить и продемонстрировать неспециалистам
  • Если всю инициативу нужно поставить на паузу или полностью остановить (из-за смены приоритетов, бюджета и так далее), она всё равно принесла пользу по сравнению с тем, что было до её начала (и эта польза в том или ином виде измерима)
По моему опыту, при таком подходе все будут довольны и смогут вносить изменения, которые действительно нужны.

Отлично, а теперь перейдём к делу!

▍ Составьте список поддерживаемых платформ


Это очень важно, но не во многих проектах делают это. Составьте README (у вас ведь есть README, так?). Это просто список пар <архитектура>-<операционная система>, например, x86_64-linux или aarch64-darwin, которые официально поддерживает ваша кодовая база. Крайне важно, чтобы сборка работала на каждой из них, а ещё, как мы увидим ниже, это позволяет избавляться от захламления платформами, которые вы не поддерживаете.

Если вы хотите углубиться, можно даже записать конкретные версии архитектуры, например, ARMV6, ARMv7 и так далее.

Это позволяет ответить на важные вопросы, в том числе:
  • Можем ли мы полагаться на аппаратную поддержку чисел с плавающей запятой, или SIMD, или SHA256?
  • Нам вообще важна поддержка 32-битных платформ?
  • Есть ли у нас вообще платформы big-endian? (Скорее всего, ответ будет таким: нет, никогда не было и не будет; однако если они всё-таки есть, то напишите мне письмо с подробностями, это звучит любопытно).
  • Может ли char быть 7-битным?
И ещё важный пункт: в этом списке совершенно точно должны быть рабочие станции разработчиков. И это приводит нас к следующему пункту:

▍ Заставьте сборку работать на вашей машине


Вы бы удивились, узнав, сколько есть реальных кодовых баз на C++, ставших ядром успешного продукта, зарабатывающего миллионы долларов, которые, по сути, даже не компилируются. Ну, если звёзды сойдутся правильно, то могут и скомпилироваться. Но я говорю не об этом. Я говорю о надёжной беспроблемной сборке на всех поддерживаемых платформах. Никакой возни, никаких «мне наконец-то удалось его собрать спустя три недели мучений» (здесь у меня возникают вьетнамские флэшбэки). Всё «просто работает»TM.

Небольшое отступление: раньше я очень любил заниматься карате. Ходил на 3-4 тренировки в неделю и всё такое прочее. Помню, как один из моих учителей (представьте мудрого азиатского старца… хм, хотя на самом деле мой учитель был белым и лысым… так что представьте Стива Балмера):

Пока ты не освоил этот приём. Иногда ты его делаешь, иногда нет, а значит, не освоил. Когда ешь ложкой, ты промахиваешься мимо рта один раз из пяти?

И этот принцип я взял с собой в разработку ПО. Если «новая фича работает», то она работает каждый раз. Не четыре раза из пяти. То же самое относится и к сборке.

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

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

Ещё одна проблема может заключаться в том, что коду требуется какой-то платформенный API, например, io_uring в Linux. Здесь может помочь реализация оболочки (shim) или сборка внутри виртуальной машины на вашей рабочей станции. Тоже неидеально, но лучше, чем ничего.

Я проделывал всё это в прошлом, и у меня получалось, но сборка непосредственно на своей машине — лучший из вариантов.

▍ Сделайте так, чтобы на вашей машине успешно выполнялись все тесты


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

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

▍ Запишите в README, как собирать и тестировать приложение


В идеале должна быть одна команда для сборки и ещё одна для тестирования. Поначалу этого хватит; если будет что-то более сложное, то соответствующие команды можно поместить в build.sh и test.sh, где и будет содержаться весь бедлам.

Ваша цель — сделать так, чтобы даже неспециалист в C++ мог собирать код и тестировать его, не задавая вам вопросов.

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

▍ Найдите самые простые способы ускорить сборку и тестирование


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

Вы опять-таки удивитесь, какой объём работы в типичном проекте на C++ выполняет сборка, хотя всего этого не нужно делать. Проверьте, есть ли у вас что-то подобное, и замерьте, помогает ли отключение таких поведений:
  • Сборка и выполнение тестов своих зависимостей. В проекте, использовавшем в качестве тестового фреймворка unittest++, собранный как подпроект CMake, я обнаружил, что по умолчанию собирались тесты тестового фреймворка, после чего они выполнялись, и так каждый раз! Безумие. Обычно существует переменная CMake или что-то подобное для отключения такого поведения.
  • Сборка и запуск примеров программ своих зависимостей. То же самое, что и в первом пункте. Виновником в данном случае оказалась mbedtls. Решила проблему тоже установка переменной CMake для отказа от такого поведения.
  • Сборка и выполнение тестов проекта по умолчанию, когда он добавляется в качестве подпроекта к другому родительскому проекту. Помните о стандартном поведении зависимостей, над которым мы смеялись? Оказалось, что мы делаем то же самое в других проектах! Я не специалист по CMake, но, похоже, исключить эти тесты в сборке стандартным образом нельзя. Поэтому я рекомендую добавить переменную сборки MYPROJECT_TEST, которая по умолчанию отключена, и собирать, и выполнять тесты, только когда она включена. Обычно её включают только разработчики, работающие над проектом. То же самое относится к примерам, генерации документации и так далее.
  • Сборка сторонней зависимости целиком, когда вам нужна только небольшая её часть: здесь в качестве примера на ум приходит mbedtls потому что она раскрывает множество флагов времени компиляции, позволяющих включать части, которые вам могут быть и не нужны. Остерегайтесь всего, что установлено по умолчанию, и собирайте только то, что нужно!
  • Неправильные зависимости, указанные для целевой платформы, приводят к пересборке всего, хотя это и не нужно: большинство систем сборки способно выводить граф зависимостей со своей точки зрения, и это может очень помочь в диагностировании таких проблем. Нет ничего хуже, чем ждать пересборки минутами или часами, когда знаешь, что на самом деле, должно быть, пересобрано всего несколько файлов.
  • Поэкспериментируйте с более быстрым компоновщиком: можно добавить mold, он поможет без всяких дополнительных затрат. Однако это сильно зависит от количества компонуемых библиотек, от того, является ли это узким местом и так далее
  • По возможности поэкспериментируйте с другим компилятором: я встречал проекты, в которых clang вдвое быстрее gcc, а в других между ними не было никакой разницы.
Закончив с этим, можно попробовать кое-что ещё, хотя выгода обычно гораздо меньше, а то и отрицательна:
  • Оптимизация во время компоновки (LTO): off/on/thin
  • Разбиение отладочной информации
  • Make или Ninja
  • Тип используемой файловой системы и тонкая настройка её параметров
Как только цикл итераций станет приемлемым, можно приступать к изучению кода под микроскопом. Если сборка длится годами, то пока не стоит стремиться к внесению изменений в код.

▍ Удалите весь необязательный код


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

Вот несколько советов:
  • У компилятора есть множество уведомлений -Wunused-xxx, например, -Wunused-function. Они отлавливают кое-что, но не всё. Следует разобраться с каждым из этих уведомлений. Обычно для этого достаточно удалить код, повторно собрать проект и снова провести тесты. Готово. В редких случаях это симптом вызова не той функции. Поэтому я не очень горю желанием полностью автоматизировать этот этап. Но если вы уверены в своём наборе тестов, то сделайте это.
  • Линтеры могут находить неиспользуемые функции или поля классов, например, cppcheck. По моему опыту, бывает довольно много ложноположительных срабатываний, особенно для виртуальных функций в случае наследования, но плюс здесь в том, что эти инструменты находят неиспользуемые части, которые не замечают компиляторы. Поэтому это аргумент для добавления в свой арсенал линтера, если уж не в CI (подробнее об этом ниже).
  • Я встречал и более экзотичные методики, например, компоновщику приказывали поместить каждую функцию в свой отдельный раздел и выполнять печать при каждом удалении раздела из-за того, что во время компоновки обнаружилось, что он не используется. Но это привносит так много шума, например, о том, что не используются функции стандартной библиотеки, что я не считаю это особо практичным. Другие разработчики исследуют сгенерированный ассемблерный код и сравнивают наличие в нём функций из исходного кода, но это не подходит для виртуальных функций. Может быть, что-то из этого пригодится в вашем случае?
  • Помните наш список поддерживаемых платформ? Да, настало время применить его и избавиться от всего кода для неподдерживаемых платформ. Код пытается поддерживать древние версии Solaris в проекте, работающем только в FreeBSD? Выбросим его прямиком в мусор. Код пытается реализовать собственный генератор случайных чисел, потому что, вероятно, платформа, на которой он работает, не имеет ГПСЧ (разумеется, оказалось, что такого никогда не бывает)? Тоже в мусорку. Сотни строк кода на случай отсутствия поддержки POSIX 2001, хотя приложение работает только на современных Linux и macOS? Уничтожить. Код проверяет, является ли CPU хоста big-endian и меняет порядок байтов, если это так? Чао (когда вы в последний раз выпускали код для big-endian CPU?). Этот код добавили много лет назад для гипотетической фичи, которая так и не появилась? Аста ла виста.
Плюс всего этого не только в том. что вы увеличите скорость сборки раз в пять, не привнеся ничего плохого, но и в том, что если ваш начальник немного технарь, то ему понравится, что пул-реквесты удаляют тысячи строк кода. И вашим коллегам это тоже понравится.

▍ Линтеры


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

▍ Форматирование кода


Дождитесь подходящего момента, когда ни одна из ветвей не будет активна (в противном случае у людей будут возникать ужасные конфликты слияния), произвольным образом выберите стиль оформления кода, выполните единовременное форматирование всей кодовой базы (без исключений), обычно это делают при помощи clang-format, выполните коммит конфигурации. Всё, готово. Не тратьте нервы на споры о самом форматировании кода. Оно существует только для уменьшения размеров diff и избавления от споров, так что не спорьте о нём!

▍ Санитайзеры


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

Неплохо будет начать с -fsanitize=address,undefined. У них обычно не бывают ложноположительных срабатываний, так что если что-то обнаруживается, приступайте к исправлению. Тесты выполняйте тоже с ними, чтобы выявлять проблемы и в тестах. Я даже слышал о людях, у которых с включёнными санитайзерами работал код в продакшене, поэтому, если ваш бюджет производительности это допускает, это может стать неплохой идеей.

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

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

И последнее: в идеале все сторонние зависимости тоже должны компилироваться с включёнными санитайзерами при выполнении тестов, чтобы выявлять проблемы и в них тоже.

▍ Добавьте конвейер CI


Как однажды сказал Брайан Кантрилл (цитирую по памяти), «Я убеждён, то основная часть встроенного ПО просто берётся из папки home ноутбука разработчика». Настройка CI — это быстрый и не требующий затрат процесс, автоматизирующий всё то хорошее, что мы настроили выше (линтеры, форматирование кода, тесты и так далее). И благодаря ему мы можем при каждом изменении создать двоичные файлы продакшена в чистой среде. Если вы разработчик, но всё ещё этого не делаете, то, по моему мнению, застряли в прошлом веке.

И вишенка на торте: большинство систем CI позволяют выполнять этапы для матрицы различных платформ! Поэтому вы можете действительно проверить, что список поддерживаемых платформ — это не просто теория, а реальность.

Обычно конвейер выглядит как make all test lint fmt, так что ничего особо сложного тут нет. Просто сделайте так, чтобы проблемы, о которых сообщают инструменты (линтеры, санитайзеры и так далее) приводили к прекращению работы конвейера, иначе никто их не заметит и не исправит.

▍ Инкрементные улучшения кода


Эта тема хорошо изучена, так что особо многого здесь говорить не буду. Достаточно сказать, что большой объём кода часто можно существенно упростить.

Вспоминаю, как итеративно упрощал сложный класс, который вручную распределял и (иногда) освобождал память, должен был работать с какими-то универсальными данными и так далее. Как оказалось, единственное, что делал класс — это распределял указатель, а позже проверял, является ли он нулевым. Вот и всё. По мне, так это обычный boolean. True/false, и больше ничего.

Мне кажется, что для этого этапа сложнее всего наметить график, потому что каждый раунд упрощения открывает новые возможности для дальнейшего упрощения. Воспользуйтесь здесь своей интуицией и давайте консервативные оценки. Сосредоточьтесь на осязаемых целях: безопасности, корректности и производительности; сторонитесь субъективных критериев наподобие «чистого кода».

По моему опыту, апгрейд используемого в проекте стандарта C++ может иногда помочь с упрощением кода, например, для замены вручную инкрементирующего итераторы кода на цикл for (auto x : items), но помните, что это лишь средство, а не цель. Если вам нужен лишь std::clamp, просто напишите его самостоятельно.

▍ Переписать на безопасном по памяти языке?


Прямо сейчас я занимаюсь этим по работе, и сама по себе эта тема заслуживает отдельной статьи. Тут есть множество тонкостей. Делайте это, только если есть веские причины.

▍ Заключение


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

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

▍ Дополнение: управление зависимостями


Этот раздел очень субъективен, это лишь моё личное предвзятое мнение.

Существует горячо обсуждаемая тема, которую я до этого момента тщательно обходил — управление зависимостями. Если вкратце, то в C++ его нет вообще. Большинство людей полагается на использование системного менеджера пакетов; это легко заметить, потому что их README выглядит так:

В Ubuntu 20.04: `sudo apt install [сто строк пакетов]`
 
В macOS: `brew install [сто строк пакетов с немного другими именами]`
 
Во всех остальных: ну, не повезло тебе, друг. Наверно, стоит выбрать популярную ОС и переустановить приложение ¯\_(ツ)_/¯

И так далее. Я и сам так делал. И я считаю, что это ужасная идея. Вот почему:
  • Инструкции по установке, как мы видели выше, зависят от операционной системы и дистрибутива. Хуже того, они зависят от версии дистрибутива. Помню проект, перенос которого с Ubuntu 20.04 на Ubuntu 22.04 занял несколько месяцев, потому что с ними поставлялись разные версии пакетов (если вообще поставлялись одинаковые пакеты), поэтому для апгрейда до нового дистрибутива необходимо было одновременно обновить сто зависимостей нашего проекта. Очевидно, что это очень плохая идея. В идеале следует апгрейдить по одной зависимости за раз.
  • Всегда находится сторонняя зависимость, не имеющая пакета, и её всё равно приходится собирать из исходников.
  • Пакеты никогда не собирают с нужными вам флагами. Fedora и Ubuntu годами обсуждали, нужно ли собирать пакеты с включённым указателем фреймов (они решили делать это только недавно). Помните раздел о санитайзерах? Как вы будете получать зависимости с включённым санитайзером? Это невозможно. Но существует и множество других примеров: LTO, -march, отладочная информация и так далее. Или пакеты собраны при помощи отличающейся от используемой вами версии компилятора C++ и ломают ABI C++ между ними.
  • Нужно стремиться к простому доступу к исходникам зависимости при аудите, разработке, отладке и так далее для версии, которую вы используете сейчас.
  • Вам нужна возможность быстро пропатчить зависимость в случае обнаружения бага и быстро выполнять повторную сборку без чрезмерных изменений в системе сборки
  • Вам никогда не добиться, чтобы во всех системах была одинаковая версия пакета, например, если Элис работает в macOS, Боб — в Ubuntu, а продакшен-система — в FreeBSD. Поэтому будут возникать раздражающие странные расхождения, которые невозможно воспроизвести.
  • Следствие из предыдущего пункта: вы не знаете точно, какие версии используются в разных системах, поэтому получить автоматизированным образом Bill of Material (BOM) сложно, а в некоторых отраслях это обязательно (или скоро будет обязательно? Ну, в любом случае правильно его иметь).
  • Иногда пакеты не имеют нужной вам версии библиотеки (статической или динамической)
Вы, наверно, подумаете: знаю, я буду использовать новые крутые менеджеры пакетов для C++, Conan, vcpkg и им подобные! Не торопитесь:
  • Они требуют внешних зависимостей, поэтому CI становится сложнее и медленнее (например, приходится разбираться, какая конкретно версия Python им требуется, и она точно будет отличаться от версии Python, требуемой вашему проекту)
  • В них нет всех версий пакета. Пример: Conan и mbedtls; он перескакивает с версии 2.16.12 на 2.23.0. Что произошло с версиями между ними? Они уязвимы и их не следует использовать? Кто знает! Для доступных версий всё равно не перечислены уязвимости безопасности! И, разумеется, в прошлом у меня был проект, который использовал версию 2.17
  • Они могут не поддерживать некоторые важные для вас операционные системы или архитектуры (FreeBSD, ARM и так далее)
Если у вас сложилась ситуация, в которой они подходят, то это великолепно и гораздо лучше сложившегося у меня представления об использовании пакетов. Просто я пока ещё ни разу не сталкивался с проектом, где можно было бы ими воспользоваться, всегда что-то мешало.

Так что же я рекомендую? Старые добрые подмодули (submodule) git и компиляцию из исходников. Да, это трудоёмко, но в то же время:
  • Ужасно просто
  • Лучше, чем поставка зависимостей вручную, потому что у git есть история и diff
  • Вы точно, вплоть до коммита, знаете, какая версия зависимости используется
  • Апгрейд версии одной зависимости — тривиальный процесс, достаточно выполнить git checkout
  • Работает на любой платформе
  • Вы получаете возможность сами выбирать при сборке всех зависимостей флаги компиляции, компилятор и так далее. И их можно настраивать под каждую зависимость индивидуально!
  • Разработчики сразу во всём разбираются, даже не имея опыта работы с C++
  • Получение зависимостей — безопасный процесс, а удалённые исходники находятся в git. Никто их втихаря не поменяет.
  • Это работает рекурсивно (то есть транзитивно, для зависимостей ваших зависимостей)
Для компиляции каждой зависимости в каждом подмодуле может быть достаточно простого add_subdirectory при помощи CMake, или может потребоваться ручное git submodule foreach make.

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

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

Если взглянуть на ситуацию с менеджерами пакетов для компилируемых языков (Go, Rust и так далее), то можно увидеть, что всё (известные мне) менеджеры выполняют компиляцию из исходников. Это та же технология, минус git, плюс автоматизация.

▍ Дополнение: предложения читателей


Я собрал отличные идеи и отзывы читателей (иногда это совокупность нескольких комментариев разных людей, а иногда я перефразирую по памяти, так что простите, если что-то будет не совсем точно):
  • Нужно делать больший упор на тесты (расширять набор тестов, покрытие кода и так далее), но стоит учитывать, что набор тестов в C++ оправдывает себя только при запуске из под санитайзеров, в противном случае у вас сложится ложное чувство безопасности. Согласен на 100%. Я считаю, что модифицировать сложный чужой код попросту невозможно без тестов. И да санитайзеры могут отлавливать так много проблем в тестах, что даже стоит подумать о выполнении набора тестов несколько раз в CI с применением разных санитайзеров.
  • vcpkg — хороший менеджер зависимостей для C++, решающий все ваши трудности. Мне не доводилось им пользоваться, поэтому я добавлю его в свой инструментарий для экспериментов. Если он соответствует перечисленным мной требованиям, а также обеспечивает кросс-компиляцию, то да, это совершенно точно лучше подмодулей git.
  • В качестве хорошего менеджера зависимостей для C++ может послужить Nix. Должен признать, что не смог справиться со сложностью и тормознутостью Nix. Возможно, стоит вернуться к нему через несколько лет, когда он станет качественнее?
  • Не стоит вкладывать так много времени в рефакторинг старой кодовой базы, если вы будете устранять по одному багу в год. Частично согласен, но это уже на ваше усмотрение. По моему опыту, никогда не бывает одного и только одного бага, чего бы ни говорило руководство. И все хорошие стороны (избавление от мёртвого кода, санитайзеры и так далее) всё равно будут ценны даже при редких багах, и к тому же приведут к выявлению большего количества багов. Как сформулировал один из комментаторов, если уж вы будете владеть кодовой базой, то владейте ею по-настоящему.
  • Удалять код очень рискованно, в общем случае никогда не знаешь точно, используется ли он и полагается ли кто-то на это конкретное поведение. Это правда, поэтому я выступаю за удаление кода, который никогда не вызывается при использовании инструментов статического анализа, то есть в котором мы точно уверены. Но да, если есть сомнения, то оставьте его. Лично я не люблю виртуальные методы, которые очень устойчивы к статическому анализу (потому что весь их смысл в выборе конкретного метода для вызова в среде исполнения), обычно их нельзя просто удалять. Кроме того, поговорите с людьми из отдела продаж, с менеджерами по продукту, да даже с пользователями, если это возможно. Чаще всего, если вы спросите их, используется ли конкретная фича или платформа, то услышите чёткое «да» или «нет», после чего будете знать, как действовать дальше. Мы, инженеры, иногда забываем, что пятнадцатиминутная беседа с людьми может сильно упростить техническую работу.
  • Отправьте весь свой код в LLM и начинайте задавать вопросы: я настроен против LLM, поэтому должен признать, что эта мысль никогда не приходила мне в голову. Однако я считаю, что попробовать стоит, если можно сделать это юридически безопасным образом, в идеале полностью локально, и воспринимать все ответы с долей скепсиса. Мне действительно интересно, какие ответы сможет дать модель!
  • Существуют инструменты, анализирующие код и создающие диаграммы, взаимосвязи классов и так далее, чтобы получить общую картину кода: я никогда не пользовался такими инструментами, но это хорошая идея и я точно попробую их в будущем
  • Этапом 0 должно быть добавление кода в систему управления исходным кодом, если он ещё не там: это точно. К счастью, я никогда с таким не сталкивался, но да, даже самая плохая система управления исходным кодом лучше, чем полное отсутствие такой системы. И я говорю это после того, как мне пришлось работать с Visual Source Safe, где для изменения файла необходимо получить исключающую блокировку, которую потом нужно отключать вручную.
  • Настройка CI должна быть этапом 1: справедливо, я вполне согласен с этой точкой зрения. Я быстрее работаю локально, но это справедливо.
  • Не будьте фанатиком красоты кода, просто вносите нужные вам исправления: аминь.
  • Если можно отказаться от платформы, которая мало используется, и снизить комбинаторную сложность, и если это позволяет вам серьёзно всё упростить, то сделайте это: абсолютно согласен. Поговорите с отделом продаж и стейкхолдерами, попытайтесь убедить их. В моём случае это были уже давно не поддерживаемые древние версии FreeBSD; чтобы убедить всех отказаться от них, мы сделали упор на безопасность.
  • Воспроизводимые сборки: эта тема вызывала серьёзные обсуждения. Честно говоря, я не думаю, что достичь полностью воспроизводимой сборки в типичной кодовой базе C++ возможно. Даже версии компилятора и стандартной библиотеки вызывают проблемы, потому что во входных параметрах сборки они обычно не учитываются. Однако вполне возможно добиться надёжной сборки. В этом обсуждении всплыло упоминание Docker. Лично я пользовался Docker и плевался на него с 2013 года, поэтому не думаю, что он обеспечивает такой выигрыш, на который обычно рассчитывают люди. Но опять-таки, если вы можете только добиться того, чтобы код собирался внутри Docker, то это лучше, чем вообще ничего.
  • Git можно приказать игнорировать один коммит, например, тот, что форматирует всю кодовую базу, чтобы git blame продолжал работать, а история оставалась логичной: потрясающий совет, я этого не знал, спасибо! Я точно попробую так сделать.
  • Использовать статистику VCS из истории, чтобы выяснить, какие части кодовой базы требуют больше всего пересмотра, а какие обычно изменяются вместе: никогда не пробовал так делать, это интересная идея, но то же время вижу в ней множество нюансов. Возможно, стоит попробовать?
  • Эта статья применима не только к C++, но и к старым кодовым базам на других языках: благодарю! У меня больше всего опыта работы с C++, поэтому такова была моя точка зрения. Но я рад слышать это. Просто пропускайте пункты, относящиеся только к C++, например, про санитайзеры.
  • Хорошие советы есть в книге «Working effectively with Legacy Code» («Эффективная работа с унаследованным кодом»): кажется, я не читал её полностью, так что спасибо за рекомендацию. Кажется, я пролистал её и обнаружил, что она очень объектно-ориентирована, с кучей шаблонов проектирования ООП, так что мне тогда она не особо помогла, но, возможно, память меня подводит.
  • В общем случае трогайте как можно меньше, сосредоточьтесь на там, что повышает ценность (реальную ценность, например, увеличивает выручку): в целом я согласен (см. раздел «Не будьте фанатиком красоты кода»), однако в тот момент, когда вы начинаете изучать типичную крупную кодовую базу на C++ с точки зрения безопасности, вы будете находить всё больше уязвимостей безопасности, требующих устранения. А это не преобразуется напрямую в финансовую выгоду, это снижает риски. Я считаю это крайне ценным. Впрочем, некоторые отрасли более чувствительны к безопасности, чем другие.

Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
Теги:
Хабы:
Всего голосов 52: ↑51 и ↓1+69
Комментарии26

Публикации

Информация

Сайт
ruvds.com
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
ruvds