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

Microsoft.Build 15.1

Я активно участвую в разработке C# анализатора PVS-Studio. Помимо прочего, при работе он использует сборки 'Microsoft.Build.dll', 'Microsoft.Build.Framework.dll' и т. д. Эти библиотеки помогают получать различную информацию из проектного файла, что позволяет выполнять более глубокий анализ.

В какой-то момент разработчики из Microsoft решили, что версией всех подобных сборок всегда будет 15.1. Неважно, меняется ли публичный интерфейс, появляются ли новые типы, меняются ли старые — версия всегда одна и та же. У соответствующих NuGet-пакетов версии меняются, а вот у сборок — нет. На вопрос "почему так?" мне ответил maintainer репозитория msbuild:

This is intentional, and allows API client applications to work with multiple versions of MSBuild.

Ну вроде бы и ладно — это их дело, и никому такой подход не мешает.

Или мешает?

Der Typ "Microsoft.Build.Framework.Traits" konnte nicht geladen werden

Примерно с таким незамысловатым сообщением о падении анализатора к нам обратился клиент. Мы обратились к переводчику и узнали, что сообщение говорит об отсутствии необходимого типа Microsoft.Build.Framework.Traits в одной из наших зависимостей — 'Microsoft.Build.Framework.dll'.

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

Не так давно мы как раз обновляли все 'Microsoft.Build.*' зависимости ради поддержки анализа проектов под .NET 7. Взглянув на предыдущие версии анализатора, мы увидели, что в используемой им библиотеке 'Microsoft.Build.Framework.dll' действительно нет нужного типа. Возможно, у клиента как-то некорректно обновился анализатор?

Ничего подобного! По нашей просьбе клиент скинул нам нужный dll-файл, и он оказался вполне корректным — нужный тип там присутствовал. Следующим шагом мы скинули пользователю специальную версию анализатора, которая бы логировала пути, по которым на этапе исполнения подгружаются сборки. И оказалось, что на этапе выполнения подгружалась не библиотека, которая лежала рядом с исполняемым файлом, а её 'аналог' из глобального кеша сборок (global assembly cache, GAC).

Тут-то пазл и собрался!

Итак, имеем:

  • анализатор зависит от нового выпуска сборки Microsoft.Build.Framework 15.1;

  • в GAC лежит старый выпуск этой сборки, но её версия тоже 15.1;

  • на этапе выполнения анализатор просит у среды сборку Microsoft.Build.Framework версии 15.1;

  • CLR великодушно предоставляет ему сборку "нужной" версии из GAC, игнорируя ту, что лежит рядом с exe-файлом;

  • в итоге загружена старая сборка, и при обращении к ней оказывается, что там не хватает Microsoft.Build.Framework.Traits.

Проверить данную гипотезу оказалось легко: достаточно было просто добавить в GAC сборку старого выпуска, и падение воспроизвелось. Но что было ещё интереснее — перестала работать Visual Studio 2022. При открытии появляется весёлое сообщение про то, что всё плохо:

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

Ну опять же, я человек простой — вижу "unloaded", значит нажимаю "Reload":

И тут же получаю знакомое сообщение (хотя уже не на немецком):

Да-да, это то же самое исключение, что возникало у нашего клиента.

Воспроизводится это всё легко:

  • скачиваем пакет Microsoft.Build.Framework 17.0;

  • находим в нём сборку для net472;

  • добавляем её в GAC одним из описанных способов (например, через команду gacutil);

  • поздравляю, вы сломали себе Visual Studio 2022, а в частности — используемый ей MSBuild.

Конечно, у нашего клиента не было цели сломать себе Visual Studio 2022. У него её попросту не было, а потому и заметить её падения он не мог. Вместо этого весь удар на себя принял анализатор, завязанный на новые MSBuild-библиотеки и потому таскающий их с собой.

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

Сначала я писал на эту тему в Twitter и на Stack Overflow, но никакой реакции не получил. Куда более действенным оказалось создание issue в репозитории MSBuild.

В этом issue сам maintainer MSBuild, Rainer Sigwald, уделил моим вопросам немного времени, за что ему большое спасибо. Если обобщить, то он сказал мне 2 вещи:

  • версии у сборок одинаковые специально, потому что для некоторых целей это удобно;

  • вы не должны устанавливать сборки Microsoft.Build.* в GAC.

Никакой возможности обойти GAC при подгрузке сборок на этапе исполнения найти не удалось :(.

Что в итоге

Во-первых, повторю — не стоит добавлять сборки Microsoft.Build.* версии 15.1 в GAC, так как это может сломать MSBuild (ну и что не менее важно — это может сломать PVS-Studio).

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

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

А у меня на этом всё, желаю удачи!

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Lipilin. Why change an assembly version when making a new assembly release, or how to break Visual Studio with a single command.

Комментарии (8)


  1. hssergey
    00.00.0000 00:00
    +8

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


    1. Sazonov
      00.00.0000 00:00
      +2

      Майкрософт вообще любит костыли. Вспомнился старый пост про х64… Логика там конечно примерно такая же: https://habr.com/ru/post/102179/


      1. Firensis Автор
        00.00.0000 00:00
        +1

        Забавно)))


    1. Firensis Автор
      00.00.0000 00:00

      Я тоже так подумал) Но maintainer MSBuild-а вроде достаточно уверено говорил, что это удобно и всё такое.


      1. Mingun
        00.00.0000 00:00
        +1

        Нормальные люди придумали SemVer, чтобы было удобно и не нужно было зависеть от конкретной версии


      1. bonArt0
        00.00.0000 00:00

        Любой программист будет на публике хвалить свой продукт. Уверен, в кулуарах они сами плюются с того, как задолбало это легаси решение. :)


  1. rg_software
    00.00.0000 00:00
    +3

    Как говорилось в одном классическом тексте,

    Let’s be very, very clear about one thing: .NET will eliminate DLL Hell. 


    1. Firensis Автор
      00.00.0000 00:00

      (((