Как обновить библиотеку и утонуть в задаче. Обновление Roslyn и PVS-Studio 7.34
Ежегодно Microsoft выпускает новую версию .NET. Это большое событие, к которому мы выпускаем версию PVS-Studio с поддержкой нововведений. Сегодня речь пойдёт про боль PVS-Studio при обновлении Roslyn — неотъемлемой части .NET.
Для удобства я поделил статью на две части: краткую (для тех, кто хочет быстро найти обоснование изменений, произошедших в PVS-Studio 7.34) и длинную (для тех, кто хочет узнать ещё и историю).
Краткая версия
В релизе PVS-Studio 7.34 была добавлена поддержка .NET 9 и всех новых функций языка C#. Для этого нам требовалось обновить платформу Roslyn, которая используется PVS-Studio для синтаксического анализа языка C#, а также обновить взаимозависимые библиотеки MSBuild, используемые для построения модели и парсинга проектов на C# и C++ (.csproj, .vcxproj).
Начиная с версии PVS-Studio 7.34, для анализа проектов C# .NET, .NET Standard и .NET Framework в стиле SDK требуется дополнительная установка .NET 9 SDK в системе, выполняющей анализ.
Классические проекты .NET Framework остаются без изменений, если в системе, выполняющей анализ, установлен экземпляр MSBuild или Visual Studio версий 2017 и выше. Если в системе установлены только MSBuild или Visual Studio версий 2015 и ниже, то для анализа классических проектов .NET Framework также потребуется наличие .NET 9 SDK.
Причиной этого изменения стала потеря обратной совместимости библиотек Roslyn, используемых для анализа проектов из Visual Studio\MSBuild версий 2015 и ниже. Теперь для этого Roslyn использует собственный серверный компонент на базе .NET SDK, который, в свою очередь, требует наличия в системе .NET SDK.
Краткая версия — всё.
Длинная версия
Обновление PVS-Studio C#
В релизе PVS-Studio 7.34 была добавлена поддержка .NET 9 и всех новых функций языка C#.
Кстати, предлагаю вам прочитать две наши статьи про нововведения:
Для поддержки .NET 9 нам потребовалось обновить библиотеки Roslyn и MSBuild. PVS-Studio и Roslyn используют MSBuild для построения модели проекта (Design-Time Build). Это достаточно сложный процесс, который требует определённых знаний от сотрудника (или его ментора). Вы могли бы подумать:, а что сложного нажать update на соответствующих NuGet пакетах из IDE? До поддержки нами Visual Studio 2017 так и было. Я бы мог начать рассказывать, что изменилось, и почему недостаточно обновить пакеты, но лучше поделюсь ссылкой на статью, где об этом написано.
Но краткую вводную всё равно придётся рассказать. Что приходится делать для поддержки нового .NET и почему? MSBuild, начиная с версии 15 (VS 2017), не умеет находить собственную установочную директорию. Предполагаю, это связано с тем, что MSBuild с версии 15 и выше не прописывается в реестре. Чтобы обойти эту проблему, было предложено решение:
Мы создаём собственный кастомный toolset. Для этого нам нужно тащить с собой изменённый BuildTools, который содержит большое количество ИЗМЕНЁННЫХ .target и .props файлов;
Ищем установочные директории MSBuild и на основе этой информации изменяем значение свойств в кастомном toolset;
Подсовываем MSBuild путь до нашего BuildTools, чтобы он считал кастомный toolset;
Profit!
Исходя из этого, для поддержки нового .NET нужно:
Обновить библиотеки Roslyn, MSBuild и необходимые им;
Справиться с некоторым Dependency hell;
Обновить наш BuildTools до последней версии, сохраняя обратную совместимость с прошлыми версиями в некоторых местах;
Исправить проблемы, которые возникнут при обновлении (а они точно возникнут, иначе не было бы этой статьи).
Это очень упрощённая схема, и лучше бы вам прочитать статью по ссылке выше. Вдобавок есть ещё статьи про поддержку Visual Studio 2019 и Visual Studio 2022. В них также описываются возникающие проблемы. Прочитав всё это вы могли подумать: мммм, вкусное legacy.
Да, legacy… Теперь уже есть решение для поиска MSBuild — MSBuildLocator. Но у него тоже есть свои ограничения: он разделён на сборки для .NET Framework и .NET Core. Версия для .NET Framework может искать и регистрировать MSBuild только для .NET Framework, версия .NET Core —только версию MSBuild для .NET Core. Предположу, что это связано с тем, что MSBuild также разделён на MSBuild для .NET Framework и на MSBuild для .NET Core.
Проблемы и решения
Теперь, когда вы немного лучше понимаете, с чем приходится иметь дело, начнём.
Вышел .NET 9 RC 1, и мы приступили к поддержке. Первым делом обновили библиотеки и наш BuildTools. Во время обновления мы заметили, что в пакете Microsoft.CodeAnalysis (Roslyn) появились две новые папки: BuildHost-net472 и BuildHost-netcore. Сначала я даже обрадовался и подумал, что теперь у нас будет меньше проблем. Но, как оказалось, это не так.
Обновление библиотек и BuildTools прошло довольно быстро, поскольку у нас уже был такой опыт. После этого этапа разработчик запускает локальный набор тестов, чтобы убедиться в корректности анализа. Эти тесты содержат большое количество Open Source проектов, на которых запускается анализатор. После их запуска возникли проблемы: в некоторых фрагментах кода анализатор начал сообщать (с помощью внутренних логов) об ошибках компиляции в полностью компилируемом коде, где-то были выброшены исключения. Мы приступили к правкам и успешно довели локальные тесты до полного прохождения.
Следующий этап — запуск обновлённой версии на тестах, запускающих анализатор на шаблонных проектах под разные версии Visual Studio и .NET. Эти тесты запускаются в контейнерах с разным окружением. И тут мы видим, что у нас сломался анализ на машинах, где установлена только VS 2010, 2012, 2013, 2015, и сломался анализ .NET Framework проектов SDK стиля c VS 2017, 2019, 2022, но без .NET SDK.
Почему предыдущие тесты прошли и не выявили проблем? Они запускались локально на машине разработчика, где установлены, по сути, все версии Visual Studio и множество версий .NET SDK.
Мы начали разбираться и нашли проблему: обновлённый Roslyn сделал не лучше, а хуже.
Раньше Roslyn ожидал, что пользователи будут использовать MSBuildLocator или что-то похожее. У нас же был свой BuildTools и свои toolset-ы, которые Roslyn использовал. Это позволяло нам поддерживать C# проекты даже для Visual Studio 2010. И в целом оставалась только сложность с поддержкой нового .NET. Ранее я писал, что Microsoft.CodeAnalysis теперь включает две дополнительные папки. Roslyn перешёл на новую архитектуру с Build серверами для самостоятельного поиска и Design-Time сборки проектов. Теперь он тянет с собой два BuildHost: для .NET Framework и для .NET Core. В зависимости от типа проекта Roslyn вызывает соответствующий процесс BuildHost для сборки проекта. Для поиска MSBuild используется MSBuildLocator. И здесь есть несколько проблемных моментов.
Первая проблема
Для работы BuildHost для .NET Core требуется минимум .NET 6 Runtime. И так как PVS-Studio использует Roslyn, пользователь должен иметь установленный .NET 6+ Runtime. В целом это можно понять и принять. Вы можете сказать:, а почему бы не публиковать BuildHost для .NET Core как self-contained приложение или даже использовать NativeAOT? К сожалению, сильно вырастет вес, который будет занимать .exe файл. Но основное, это что для регистрации MSBuild под капотом используется Assembly.Load, который просто не работает в подобных приложениях.
Вторая проблема
MSBuildLocator для .NET Framework может искать только MSBuild 15, 16, 17 (Visual Studio 2017, 2019, 2022). Соответственно, если у пользователя достаточно старый проект и он использует Visual Studio 2015, то Roslyn не может найти подходящий MSBuild и просто не будет работать. И это учитывая тот факт, что проект полностью собирается на машине. Мы создали issue на GitHub об этом. Если кратко, ответ состоит в том, что для них это не приоритет, так как VS 2015 и прошлые версии слишком старые. Но оказалось, что если у пользователя имеется .NET SDK, то Roslyn начинает использовать запасной план. Если у вас старый .NET Framework проект, то Roslyn попробует использовать BuildHost для .NET Core. Чаще всего это работает нормально, но возможны проблемы, если встречается что-то, что не поддерживается MSBuild для .NET Core. Про это вы узнаете в описании третьей проблемы.
Из всего этого мы приходим к тому, что, если мы хотим оставить поддержку старых проектов, нам нужно поставлять .NET SDK. Для целостности продукта мы решили поставлять с собой на Windows .NET 9 SDK, как уже делаем на Linux и macOS. На этих операционных системах анализатору для работы нужен .NET SDK той версии, под которую он был собран. Зачем нам .NET 9 SDK на Linux и macOS — это отдельная тема для разговора, но поверьте, он нужен.
Третья проблема
Roslyn для любых проектов SDK стиля начинает использовать BuildHost для .NET Core, и неважно, может ли MSBuild для .NET Core собрать его. MSBuild для .NET Core поддерживает не все вещи, которые поддерживает MSBuild для .NET Framework. Например, COMReference
не поддерживается в MSBuild для .NET Core. По этой проблеме тоже было создано issue. Как это решается с нашей стороны? Да в общем и целом никак. Подобные точечные ошибки выливаются в мелкие недостатки полученной от Roslyn семантической модели. И это никак не влияет на качество анализа.
Это только самые запомнившиеся проблемы. По ходу обновления мы сталкивались ещё с ворохом маленьких проблем.
Мысли
Что же мы имеем по итогу улучшений в Roslyn от Microsoft? Усложнение дистрибуции анализатора, необходимость для клиента ставить ненужные ему .NET SDK и десятки дней, потраченных на то, чтобы всё заработало.
Зачем же эта статья? Хотелось поделиться небольшой историей о том, как обновление и улучшение инструмента делает жизнь не лучше, а хуже. И на сам деле многое можно было избежать, если бы Roslyn более правильно выбирал BuildHost. А ещё хотелось иметь быстрый ответ на клиентский вопрос: «Почему так?» Для этого была написана краткая версия.
Во время чтения вам могло показаться, что я достаточно негативно высказываюсь о Roslyn. Может, так и было, но только чуть-чуть и из-за того, что мне сильно прибавили работы :) Я всё ещё считаю Roslyn отличным инструментом, который во многом облегчает разбор и анализ кода.
Также скажу, что в будущем мы бы хотели избавиться от наших BuildTools и перейти к похожей на Roslyn архитектуре. Как обычно и бывает, это достаточно сложная задача, так как много старого кода нужно переписать или выкинуть и написать заново. И при этом нужно сохранить совместимости со старыми проектами. Короче, та ещё работа.
А вам большое спасибо за прочтение:)
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Artem Rovenskii. How to update library and get swamped with this task. Roslyn and PVS-Studio 7.34 update.