За последние шесть или около того недель Майкрософт, конечно, отличились: выпустили целую кучу версий .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 пакетов.
Конечно, был маленький шанс, что что-то где-то случайно глюкнуло, и ошибка случилась в первый и последний раз. Но нет. Перезапустив билд аж два раза и дважды увидев одну и ту же ошибку, я понял, что в этот раз придётся действительно думать. Возможно, мозгами.
Дебаггинг
Осматриваемся вокруг
Что интересно, локально проект компилился и собирался нормально. И ведь код тот же, Убунта — тоже одинаковая. Даже 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
.
Ну а что, разноцветненько. Так как publish
билд упал ещё до компиляции, то его размер раз в двадцать меньше, чем логи успешного билда. Это, кстати, очень хорошо, потому что компиляция как раз таки нас и не интересует, так что удалив всё после строк Done executing task 'RestoreTask'
, мы избавим себя от необходимости продираться через большую часть текста.
Смотрим на отличия
Ошибка ругалась на версии пакетов 2.1.1
и 2.1.0
. Стоит, наверное, начать поиск с них.
Через десяток с лишним совпадения находится очень интересная строка, в которой RuntimeFrameworkVersion
свойство начинает отличаться: 2.1.1 в проблемном билде и 2.1.0 в успешном. Интересно это тем, что предыдущий установленный на лаптопе SDK — 2.1.300 — шёл именно с рантаймом 2.1.0, а свежайший — 2.1.301 — уже с 2.1.1. Это даже проверить можно:
1 2 3 4 |
dotnet --list-runtimes # Microsoft.AspNetCore.All 2.1.1 [/usr/share/dotnet/shared/Microsoft.AspNetCore.All] # Microsoft.AspNetCore.App 2.1.1 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App] # Microsoft.NETCore.App 2.1.1 [/usr/share/dotnet/shared/Microsoft.NETCore.App] |
Так вот, ошибка утверждает, что Microsoft.NETCore.App
2.1.1
— рантайм то бишь — начал конфликтовать со своей предыдущей версией — 2.1.0
. Но почему? Я не помню, чтобы мы хоть раз ссылались на рантайм в принципе, не говоря уже о его версиии. Нужно, наверное, глянуть, откуда вообще RuntimeFrameworkVersion
получает своё значение.
Судя по логам билда, прямо из космоса. По крайней мере ни одного присваивания я не нашёл. Но ОК, сам SDK — это всего лишь компилятор плюс.props
и.targets
XML файлы, так что скорее всего можно что-то найти среди них. Идём в папку с SDK и начинаем искать.
1 2 3 4 5 |
cd /usr/share/dotnet/sdk grep -ir RuntimeFrameworkVersion ... #<RuntimeFrameworkVersion Condition="'$(TargetLatestRuntimePatch)' == 'true' ">$(LatestNetCorePatchVersion)</RuntimeFrameworkVersion> #<RuntimeFrameworkVersion Condition="'$(TargetLatestRuntimePatch)' != 'true' ">$(DefaultNetCorePatchVersion)</RuntimeFrameworkVersion> |
А вот это очень интересно. Если в переменной билда TargetLatestRuntimePatch
стоит true, то в RuntimeFrameworkVersion
пойдёт LatestNetCorePatchVersion
, который, я так полагаю, равен 2.1.1 и я почти уверен, что именно это и происходит в нашем случае. И среди логов мгновенно находится подтверждение:
Отлично, а почему тогда TargetLatestRuntimePatch
стал true? В логах опять ничего нет, но есть что-то в SDK:
1 2 3 |
grep -ir TargetLatestRuntimePatch ... #..<TargetLatestRuntimePatch Condition="'$(SelfContained)' == 'true' ">true</TargetLatestRuntimePatch> |
Думательная часть
В принципе, становится более или менее понятно, что происходит. Когда я собираю проект через 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
реально стал указывать на другую версию.
Ну и что теперь с этим делать? На самом деле есть аж три варианта. Один нормальный, и два с запахом.
- Можно дойти до корня проблемы и-таки найти тот пакет или настройку, которая дефолтит один из проектов солюшена на старый рантайм.
- В проблемном проекте можно явно задать
TargetLatestRuntimePatch
— true и таким образом заставить его пользоваться самым свежим рантаймом. - Или можно наоборот — поставить в
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 мы ещё не знаем. Дебаггинг на линуксе, завершающийся ошибкой дебаггера — это вполне нормально. Ребята, блин, вы уже два года дотнэт делаете, ну сколько ж можно…
Увлекательное приключение) Спасибо за пост