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. А заодно разберёмся, как автоматизировать сборку и доставку, чтобы не делать всё это руками при каждом обновлении.