CLI приложения #23. Установка одной командой — .NET tools

В прошлый раз мы научились собирать приложение в готовый исполняемый файл. Теперь его можно скопировать на другой компьютер и запускать без исходников и dotnet run. Но копирование файлов, создание папок, настройка PATH — всё это не очень удобно. Особенно когда приложение нужно обновить или удалить.

А что, если устанавливать нашу утилиту одной командой, как мы устанавливаем библиотеки через NuGet? Решение есть, в .NET такой механизм называется **.NET tools**. По сути это обычное консольное приложение, упакованное особым образом в NuGet-пакет. Только вместо подключения к проекту оно устанавливается как самостоятельный инструмент.

Подготовим проект#

Для начала откроем .csproj нашего приложения и добавим несколько свойств:

<PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>

    <PackAsTool>true</PackAsTool>
    <ToolCommandName>myapp</ToolCommandName>
    <PackageId>MyCompany.MyApp</PackageId>
    <Version>1.0.0</Version>
</PropertyGroup>

Здесь самое интересное:

  • PackAsTool — указывает, что проект нужно упаковать как .NET tool.
  • ToolCommandName — имя команды, которой будем запускать приложение.
  • PackageId — идентификатор пакета в NuGet.
  • Version — версия нашего инструмента.

Обратите внимание на два разных имени: MyCompany.MyApp и myapp. Первое используется для установки пакета, второе — для запуска приложения.
И вот тут мы добрались до вопроса, который до сих пор обходили стороной. Помните git, docker, dotnet? Откуда вообще берутся эти названия?
В случае обычного исполняемого файла команда обычно совпадает с его именем. Например, myapp.exe можно запустить командой myapp, если ОС знает, где искать этот файл. А для .NET tool имя команды задаётся явно через ToolCommandName. И оно совершенно не обязано совпадать ни с именем проекта, ни с именем NuGet-пакета.

Кстати, RootCommand из наших предыдущих постов к выбору имени запуска отношения не имеет. Он начинает работать уже после запуска приложения и отвечает за обработку команд и аргументов.

Собираем пакет#

Вместо dotnet publish теперь используем:

dotnet pack -c Release -o ./packages

В папке packages появится файл:

MyCompany.MyApp.1.0.0.nupkg

Это и есть наш установочный пакет. Пока никуда его публиковать не будем — сначала проверим локально.

Устанавливаем приложение#

Выполняем:

dotnet tool install --global MyCompany.MyApp --version 1.0.0 --add-source ./packages

Здесь:

  • --global — устанавливаем инструмент для текущего пользователя, чтобы он был доступен из любой папки;
  • --version — указываем нужную версию;
  • --add-source — добавляем локальную папку как источник NuGet-пакетов.

После успешной установки пробуем:

myapp --help

Работает!

Причём запускать можно из любой папки, а не только из каталога проекта. Никаких ручных копирований .exe и DLL.

Куда всё установилось?#

По умолчанию глобальные .NET tools размещаются здесь:

Windows:      %USERPROFILE%\.dotnet\tools
Linux/macOS:  ~/.dotnet/tools

При нормальной установке SDK этот каталог добавляется в PATH. Если команда myapp не находится, проверьте переменную окружения и попробуйте открыть новый терминал.
Есть ещё нюанс: слово global здесь не означает установку для всех пользователей компьютера. Инструмент устанавливается для текущего пользователя.
Посмотреть установленные инструменты можно командой:

dotnet tool list --global

Там будут идентификатор пакета, версия и имя команды запуска.

Обновление и удаление#

Предположим, мы внесли изменения в приложение и подняли версию в .csproj до 1.0.1.
Снова собираем пакет:

dotnet pack -c Release -o ./packages

А затем обновляем установленную версию:

dotnet tool update --global MyCompany.MyApp --version 1.0.1 --add-source ./packages

Проверить установленную версию можно через dotnet tool list --global.

А если инструмент больше не нужен:

dotnet tool uninstall --global MyCompany.MyApp

И всё. Никаких поисков файлов по диску и ручной очистки PATH.

А если инструмент нужен только для одного проекта?#

Есть и такой вариант — локальные .NET tools.
Например, если в команде разработчиков все должны пользоваться одной и той же версией генератора кода, нет необходимости устанавливать его глобально.

В корне проекта создаём манифест:

dotnet new tool-manifest

Затем устанавливаем инструмент:

dotnet tool install MyCompany.MyApp --version 1.0.0 --add-source ./packages

Появится файл .config/dotnet-tools.json, в котором будет записано, какой инструмент и какой версии нужен проекту.

Этот файл можно добавить в Git. Тогда остальные разработчики смогут восстановить инструменты командой:

dotnet tool restore

А запускать локальную утилиту можно так:

dotnet tool run myapp -- --help

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


Итого.#

Теперь наше приложение можно не просто собрать, но и установить, обновить или удалить стандартными командами .NET. Причём один и тот же пакет можно использовать как глобальный инструмент или привязать к конкретному проекту. Но есть важное ограничение: обычный .NET tool по-прежнему требует подходящий .NET Runtime на компьютере пользователя. Это не то же самое, что self-contained сборка из предыдущего поста.

И ещё мы пока устанавливали пакет из локальной папки. А если захотим поделиться приложением с другими разработчиками, придётся разместить пакет в общедоступном источнике, например на NuGet.org.

Тогда установка будет выглядеть совсем просто:

dotnet tool install --global MyCompany.MyApp

Разумеется, если пакет с таким идентификатором действительно опубликован.

В следующей части поговорим о других способах распространения CLI-приложений: установщиках, GitHub Releases и Native AOT. А заодно разберёмся, как автоматизировать сборку и доставку, чтобы не делать всё это руками при каждом обновлении.