Start Debugging

В чём разница между dotnet build и dotnet publish?

dotnet build компилирует проект для внутреннего цикла разработки и по умолчанию использует Debug. dotnet publish запускает target Publish в MSBuild, по умолчанию использует Release для net8.0 и более поздних версий и упаковывает готовую к развёртыванию папку с уже обработанными веб-ресурсами, автономными средами выполнения, одним файлом, обрезкой и AOT. Здесь ровно то, что производит каждая команда и когда её применять.

dotnet build компилирует проект и его зависимости в IL-сборки для внутреннего цикла разработки и по умолчанию использует конфигурацию Debug. dotnet publish запускает другой target MSBuild, который производит автономную папку, готовую к копированию на сервер, по умолчанию использует конфигурацию Release для любого проекта с target net8.0 или новее и является единственным официально поддерживаемым способом подготовить приложение к развёртыванию. Коротко: build вы запускаете, пока пишете код, publish вы запускаете, когда закончили и хотите получить то, что можно поставлять. Различия, которые действительно кусаются, это смена конфигурации по умолчанию и всё то, что делает target Publish и не делает Build: обработка веб-ресурсов, упаковка среды выполнения для автономных развёртываний и применение одного файла, обрезки и AOT.

Всё, что описано ниже, использует SDK .NET 11 (11.0.100) с <TargetFramework>net11.0</TargetFramework> и C# 14. Описанное поведение действует начиная с SDK .NET 8, если не указана конкретная версия, потому что самое значимое различие, конфигурация по умолчанию, изменилось в .NET 8.

Два разных target MSBuild, а не два варианта одного и того же

Обе команды это тонкие обёртки над MSBuild, но они вызывают разные target, и один этот факт объясняет почти каждое различие, которое вы заметите.

dotnet build запускает target Build по умолчанию. Согласно справочнику по dotnet build, его запуск эквивалентен dotnet msbuild -restore с другой подробностью вывода по умолчанию. Он компилирует ваш код вместе с его зависимостями в набор двоичных файлов и на этом останавливается.

dotnet publish запускает target Publish. Справочник по dotnet publish прямо говорит, что он “компилирует приложение, прочитывает его зависимости, указанные в файле проекта, и публикует полученный набор файлов в каталог”, и что его вывод “готов к развёртыванию в систему размещения”. Target Publish зависит от Build, поэтому publish всегда сначала собирает, а затем выполняет дополнительную работу поверх.

Эта “дополнительная работа поверх” и есть вся суть. Когда вы понимаете, что добавляет target Publish, вы понимаете, когда вывода build достаточно, а когда он молча откажет в продакшене.

Конфигурация по умолчанию это ловушка, в которую большинство попадает первой

Это различие обходится людям в целый день, поэтому идёт первым.

dotnet build по умолчанию использует Debug. dotnet publish по умолчанию использует Release, но только для проектов с target net8.0 или новее. Оба значения по умолчанию можно переопределить через -c|--configuration.

# .NET 11 SDK 11.0.100, project targeting net11.0.
dotnet build      # builds Debug   -> bin/Debug/net11.0/
dotnet publish    # publishes Release -> bin/Release/net11.0/publish/

До .NET 8 dotnet publish тоже по умолчанию использовал Debug, и множество мышечной памяти и CI-скриптов всё ещё это предполагают. Microsoft изменила это намеренно, чтобы команда, задача которой производить артефакты развёртывания, по умолчанию производила оптимизированные. Изменение задокументировано в разделе ‘dotnet publish’ использует конфигурацию Release.

Практическое следствие: если вы делаете dotnet build, а затем копируете bin/Debug/... на сервер, вы поставляете неоптимизированный двоичный файл с отключёнными оптимизациями JIT и включёнными средствами отладки. Если ваш CI-конвейер всё ещё передаёт -c Release в dotnet publish “на всякий случай”, теперь это избыточно, но безвредно. Если он по привычке передаёт -c Debug, вы переопределяете разумное значение по умолчанию и поставляете Debug. Прочитайте свой конвейер один раз и удалите карго-культ.

Что каждая команда на самом деле пишет на диск

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

dotnet build пишет в bin/<configuration>/<framework>/, например bin/Debug/net11.0/. Он выдаёт вашу IL-.dll, исполняемый файл для её запуска в проектах Exe, файлы символов .pdb, файл .deps.json со списком зависимостей, файл .runtimeconfig.json с именем общей среды выполнения и копии DLL ваших зависимостей. Для исполняемых проектов с target .NET Core 3.0 или новее в документации отмечено, что “если нет другой логики, специфичной для публикации (как у Web-проектов), вывод сборки должен быть пригоден к развёртыванию”.

dotnet publish пишет в подпапку publish: bin/<configuration>/<framework>/publish/ для сборки, зависимой от фреймворка, или bin/<configuration>/<framework>/<runtime>/publish/, когда вы собираете автономно для конкретной среды выполнения. Он выдаёт те же базовые файлы (IL-.dll, .deps.json, .runtimeconfig.json, зависимости) плюс то, что требует форма развёртывания.

Обратите внимание, что вывод publish попадает во вложенный каталог publish именно для того, чтобы не столкнуться с обычным выводом сборки. Если вы направите -o на папку прямо под вашим проектом в Web-проекте, последовательные публикации могут вложиться в publish/publish, известный острый угол, отмеченный в документации.

Почему “вывод сборки пригоден к развёртыванию” это полуправда

В документации сказано, что вывод сборки “должен быть пригоден к развёртыванию” для простого исполняемого файла, и для простого консольного приложения это действительно так. Вы можете сделать dotnet build -c Release, упаковать bin/Release/net11.0/, положить на машину с установленной подходящей средой выполнения .NET 11 и запустить.

Проблема в том, что оговорка “если нет другой логики, специфичной для публикации” охватывает множество реальных приложений. Target Publish выполняет работу, которую Build никогда не запускает:

Итак, точное правило: для библиотеки или тривиального консольного приложения вывод build и вывод publish это почти один и тот же набор файлов. Для веб-приложения, автономного развёртывания или всего, что использует преобразования момента публикации, только publish производит нечто корректное. Когда люди сообщают об ошибке “не удалось загрузить файл или сборку” в опубликованном приложении, корневая причина часто в том, что они развернули папку build или её частичную копию вместо настоящего вывода publish.

Зависимость от фреймворка vs автономность: решение исключительно publish

dotnet build собирает против общего фреймворка и предполагает, что среда выполнения присутствует во время выполнения. У него нет понятия упаковки среды выполнения.

dotnet publish позволяет выбрать модель развёртывания. Зависимость от фреймворка это значение по умолчанию, оно держит вывод небольшим, требуя подходящую среду выполнения .NET на целевой машине:

# .NET 11 SDK 11.0.100. Framework-dependent, cross-platform. Target needs .NET 11 installed.
dotnet publish -c Release

Автономность упаковывает среду выполнения, чтобы целевой машине ничего не требовалось предустанавливать, ценой намного большего вывода и сборки для каждой среды выполнения:

# .NET 11 SDK 11.0.100. Self-contained: the runtime ships inside the output folder.
dotnet publish -c Release -r linux-x64 --self-contained

-r (идентификатор среды выполнения) делает вывод специфичным для платформы: publish linux-x64 не запустится на win-x64. Поэтому путь автономного вывода включает сегмент среды выполнения (bin/Release/net11.0/linux-x64/publish/). Если вы поставляете на несколько платформ, вы запускаете один publish на каждый RID. Этот компромисс переносимости тот же, что вы взвешиваете, решая, стоит ли прибегать к Native AOT и во что он вам обойдётся.

Преобразования момента публикации, у которых нет эквивалента в build

Четыре свойства MSBuild меняют то, что производит publish, и ни одно из них не делает ничего значимого во время голого build. Именно поэтому publish существует как отдельная команда.

PublishReadyToRun компилирует ваши сборки в формат ReadyToRun (R2R), форму опережающей компиляции, которая заранее JIT-компилирует ваш код, чтобы сократить время запуска, сохраняя JIT доступным на потом. Это шаг момента публикации; посмотрите, как он выглядит на фоне альтернатив, в Native AOT vs ReadyToRun vs JIT.

PublishSingleFile упаковывает приложение в один исполняемый файл, специфичный для платформы. Его установка неявно включает PublishSelfContained.

PublishTrimmed удаляет код библиотек, достижимость которого обрезчик не может доказать, уменьшая автономное развёртывание. Оно применимо только к автономным публикациям.

PublishAot запускает компилятор ILC, чтобы произвести один нативный двоичный файл вообще без JIT.

<!-- .NET 11, C# 14. These belong in the .csproj, not on the build command line. -->
<PropertyGroup>
  <PublishSingleFile>true</PublishSingleFile>
  <PublishTrimmed>true</PublishTrimmed>
  <PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
# .NET 11 SDK 11.0.100. Only 'publish' honors these; 'build' ignores them.
dotnet publish -c Release -r win-x64

Запустите dotnet build в проекте с этими заданными свойствами, и ничего не произойдёт: обрезчик не запустится, ни один файл не будет собран, ILC никогда не вызовется. Преобразования зашиты в граф зависимостей target Publish, а не Build. Это конкретная, механическая причина, по которой документация называет publish “единственным официально поддерживаемым способом подготовить приложение к развёртыванию”.

Пропуск повторной сборки с —no-build

Поскольку publish зависит от Build, publish по умолчанию перекомпилирует. В CI-конвейере, где вы уже запускали dotnet build (чтобы прогнать анализаторы или тесты против тех самых двоичных файлов), перекомпиляция во время publish тратит время и, что хуже, может произвести тонко отличающийся вывод. Передайте --no-build, чтобы переиспользовать существующий вывод сборки:

# .NET 11 SDK 11.0.100. Build once with the config you want, then publish that.
dotnet build -c Release
dotnet publish -c Release --no-build

Два момента, за которыми стоит следить. Во-первых, --no-build неявно устанавливает --no-restore, поэтому restore уже должен был произойти. Во-вторых, конфигурации должны совпадать: если вы делаете dotnet build -c Release, а затем dotnet publish --no-build без -c Release, publish ищет вывод Debug (своё значение по умолчанию), не находит подходящей сборки и падает. Держите значение -c идентичным в обеих командах. Если вы используете раскладку вывода артефактов (--artifacts-path), в документации отмечено, что вы должны передать этот же путь обеим командам.

dotnet run и dotnet test стоят рядом с этими

Полезно поставить эти две команды в более широкий CLI. dotnet run собирает (Debug по умолчанию) и сразу запускает приложение; это удобная обёртка внутреннего цикла, а не инструмент развёртывания. dotnet test собирает и запускает ваш тестовый проект. dotnet pack производит пакет NuGet, а не пригодное к развёртыванию приложение, и, как publish, по умолчанию использует Release для net8.0 и более поздних версий. Все они сначала запускают неявный dotnet restore, если вы не передадите --no-restore. Если ваш CI даже не находит SDK, чтобы запустить любую из них, это отдельная проблема окружения, разобранная в команда dotnet не найдена в CI.

Какую запускать, решается одной строкой для каждой

Используйте dotnet build, пока разрабатываете: чтобы скомпилировать, вывести на свет предупреждения и диагностику анализатора и произвести двоичные файлы для ваших тестов. Он быстрый, по умолчанию использует Debug, и его вывод предназначен для вашей машины.

Используйте dotnet publish, когда хотите артефакт для поставки: он по умолчанию использует Release, выполняет шаги, специфичные для развёртывания, и это поддерживаемый путь к папке, которую вы можете передать серверу, образу контейнера или двоичному файлу Native AOT. Когда вы нарезаете release-сборку в CI, publish (при желании после одной общей build с --no-build) это команда, которая производит то, что вы развёртываете. Если вы вдобавок переходите между мажорными версиями среды выполнения одновременно, сочетайте это с чек-листом миграции с .NET 8 на .NET 11, чтобы вывод publish был нацелен на ту среду выполнения, на которой вы действительно намерены запускаться.

Проверка в одном предложении: если вывод для вас, build; если вывод для машины, которая не ваша, publish.

Похожее

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад