Сказ о .NET Core и ошибке его package downgrade

vimdiffЗа последние шесть или около того недель Майкрософт, конечно, отличились: выпустили целую кучу версий .NET Core 2.1 SDK (Preview 2, Release Candidate 1, Early Access, RTM), которые мы все и попробовали. Так как всё происходило в спешке, к выходу финальной версии мой кластер CI серверов выглядел как зоопарк. Там были RC1 сервера, выглядевшие как Early Access. EA сервера, пытающиеся быть похожими на RTM. Ну как и не упомянуть тот единственный RTM сервер, который старался быть похожим на всех. Ну а что, бывает.

Проблемы начались тогда, когда я попытался разгрести этот бардак и поудалять пререлизные машины, поубирать P2, RC1 и EA SDK теги с релизных веток, ну и выкатить свежие билд-сервера с новейшим и стабильным .NET Core SDK 2.1 на них. Ну и ничего, естественно, на них не скомпилировалось.

Собственно, проблема

Компилятор ругался на следующее: Detected package downgrade: Microsoft.NETCore.App from 2.1.1 to 2.1.0. И это даже не компиляция была, а подготовка к ней — восстановление NuGet пакетов.

ошибка package downgrade

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

Дебаггинг

Осматриваемся вокруг

Что интересно, локально проект компилился и собирался нормально. И ведь код тот же, Убунта — тоже одинаковая. Даже SDK, и тот идентичный натуральному… Или нет? Быстрый dotnet --version на обоих машинах показал, что на лаптопе стоит версия 2.1.300, а на билд сервере — аж 2.1.301. То есть что, Microsoft уже успела патч выкатить? Хорошо, устанавливаем новый SDK на рабочей машине и.. проект теперь не собирается и тут. Неплохо для начала.

Я проверил проектные файлы, но ничего хоть отдалённо выглядящего подозрительным там не было. Но как-то не сразу я заметил, что, собственно, сборка проекта через dotnet build вполне себе работает. Это только паблишинг с dotnet publish -r ubuntu-x64 вываливается с ошибкой. Хм.

Лезем в логи

Так как умных идей больше не было никаких, оставались только крайние меры: включить диагностику для build и publish команд и посмотреть, чем они отличаются.

Если вы никогда не пользовались dotnet флагом -v diag, то вот хорошая причина придерживаться этой стратегии и впредь. -v diag делает много текста. Реально много. Для нашего солюшена с примерно 90 проектами внутри, логи билда — это десятки и десятки мегабайт неструктурированного текста. Но если уж что-то найдётся, то скорее всего именно там.

Итак, берём с одной стороны dotnet build -c Debug -v diag > works.txt для рабочего билда, затем dotnet publish -c Debug -v diag -r ubuntu-x64 > fails.txt для падающего, и смотрим разницу при помощи vim и :diffthis.

vimdiff логов

Ну а что, разноцветненько. Так как publish билд упал ещё до компиляции, то его размер раз в двадцать меньше, чем логи успешного билда. Это, кстати, очень хорошо, потому что компиляция как раз таки нас и не интересует, так что удалив всё после строк Done executing task 'RestoreTask', мы избавим себя от необходимости продираться через большую часть текста.

Смотрим на отличия

Ошибка ругалась на версии пакетов 2.1.1 и 2.1.0. Стоит, наверное, начать поиск с них.

ищем 2.1.1

Через десяток с лишним совпадения находится очень интересная строка, в которой  RuntimeFrameworkVersion свойство начинает отличаться: 2.1.1 в проблемном билде и 2.1.0 в успешном. Интересно это тем, что предыдущий установленный на лаптопе SDK — 2.1.300 — шёл именно с рантаймом 2.1.0, а свежайший — 2.1.301 — уже с 2.1.1. Это даже проверить можно:

Так вот, ошибка утверждает, что Microsoft.NETCore.App 2.1.1 — рантайм то бишь — начал конфликтовать со своей предыдущей версией — 2.1.0. Но почему? Я не помню, чтобы мы хоть раз ссылались на рантайм в принципе, не говоря уже о его версиии. Нужно, наверное, глянуть, откуда вообще RuntimeFrameworkVersion получает своё значение.

Судя по логам билда, прямо из космоса. По крайней мере ни одного присваивания я не нашёл. Но ОК, сам SDK — это всего лишь компилятор плюс.props и.targets XML файлы, так что скорее всего можно что-то найти среди них. Идём в папку с SDK и начинаем искать.

А вот это очень интересно. Если в переменной билда TargetLatestRuntimePatch стоит true, то в RuntimeFrameworkVersion пойдёт LatestNetCorePatchVersion, который, я так полагаю, равен 2.1.1 и я почти уверен, что именно это и происходит в нашем случае. И среди логов мгновенно находится подтверждение:

TargetLatestRuntimePatch

LatestNetCorePatchVersion

Отлично, а почему тогда TargetLatestRuntimePatch стал true? В логах опять ничего нет, но есть что-то в SDK:

Думательная часть

В принципе, становится более или менее понятно, что происходит. Когда я собираю проект через dotnet build,  то SelfContained свойство будет false (это же не publish), и тогда через цепочку TargetLatestRuntimePatch и RuntimeFrameworkVersion весь проект получает базовый рантайм — 2.1.0. Но в dotnet publish всё меняется. SelfContained становится true, TargetLatestRuntimePatch  тоже и итоговый рантайм становится 2.1.1. Но почему-то не для всего проекта. Либо какой-то внутренний косяк, либо устаревший пакет продолжает требовать 2.1.0, и получается конфликт. В SDK 2.1.300 проблемы не было, потому что это же первый официальный SDK для .NET Core 2.1, и для него существовал только один рантайм. Но в патче 2.1.301 появился второй рантайм, и TargetLatestRuntimePatch реально стал указывать на другую версию.

Ну и что теперь с этим делать? На самом деле есть аж три варианта. Один нормальный, и два с запахом.

  1. Можно дойти до корня проблемы и-таки найти тот пакет или настройку, которая дефолтит один из проектов солюшена на старый рантайм.
  2. В проблемном проекте можно явно задать TargetLatestRuntimePatch — true и таким образом заставить его пользоваться самым свежим рантаймом.
  3. Или можно наоборот — поставить в TargetLatestRuntimePatch false для всех остальных проектов.

В конце концов мы пошли по четвёртому пути: делаем мы build или publish — без разницы: ставим везде TargetLatestRuntimePatch — true и всё тут. Кому вообще были нужен этот старый рантайм, если есть пропатченый?

Мораль

Хотя я совсем не любитель лазить в дебри дотнэта и MSBuild в частности, какое-то удовольствие в этом всё же есть. Давным давно мне приходилось проводить много времени с XSLT, который хотя и был в душе обычным XML, но при этом являлся вполне себе функциональным языком программирования с паттернами, чистыми функциями, рекурсией и вообще. Писать на нём что-то сложное было… интересно. Так вот, всякие там .csproj, .targets и .props файлы на самом деле тоже XML, в них тоже реализована логика — условия, присваивания, вызов функциеподобных блоков, и это немного прикольно. Сильно извращенски, конечно, но прикольно. Эх, были времена…

Вторая часть морали — хотя я и очень любитель .NET Core и особенно поддержки им Линукса, но с каждой новой версией Майкрософт находит способ меня удивить. То они придумали новый формат для проектных файлов, то откатили назад. То вели различные номера версий для SDK и самого .NET Core, то теперь вроде одну, но не совсем. .NET Core до версии 2.1 всегда использовал базовую версию рантайма, а теперь в некоторых случаях пытается использовать новую. Почему NuGet пакет перестал собираться в .NET Core 2.0 мы ещё не знаем. Дебаггинг на линуксе, завершающийся ошибкой дебаггера — это вполне нормально. Ребята, блин, вы уже два года дотнэт делаете, ну сколько ж можно…

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *