Пару недель назад вышел .NET Core 2.1 RC1. Это первая версия SDK, где есть фича под названием "Глобальные утилиты .NET Core" (".NET Core Global Tools"). Она дает простой способ создания кросс-платформенных консольных утилит.



Мы познакомимся с основами использования .NET Core Global Tools и кратко посмотрим, что внутри. А еще вы можете скачать .NET Core 2.1 SDK и попробовать написать собственный пример.


Основы


.NET Core global tool — это специальный пакет NuGet, в котором находится консольное приложение. Когда вы устанавливаете его, .NET Core CLI скачивает пакет и делает его доступным в виде новой глобальной консольной команды.


Пользователи могут устанавливать утилиты с помощью команды dotnet tool install:


dotnet tool install -g <nuget package name>

После установки консольные утилиты, находящиеся в пакете, будут глобально доступны по имени:


<command name>

dotnet tool имеет и другие команды. Например:


dotnet tool list -g
dotnet tool uninstall -g <nuget package name>
dotnet tool update -g <nuget package name>

Под капотом


NuGet пакет с консольной утилитой содержит все файлы, полученные в результате выполнения команды dotnet publish, а также несколько дополнительных файлов с мета-информацией.


Когда вы запускаете dotnet tool install --global, происходит следующее:


  1. Запускается dotnet restore со специальными параметрами, чтобы скачать пакет.
  2. Файлы распаковываются в папку $HOME/.dotnet/.store/<package id>/<version>.
  3. Генерируется запускаемый файл в папке $HOME/.dotnet/tools.

Сгенерированный запускаемый файл — это небольшое консольное приложение (написанное на C++), которое знает, где находится ваш .NET Core DLL файл и автоматически запускает его.


Вы также можете запустить dotnet tool install с аргументом --tool-path $installDir. Эта команда делает всё то же самое, но устанавливает консольное приложение в папку $installDir, а не в $HOME/.dotnet/tools.


Как создать свой пакет


Для создания глобальных консольных утилит вам нужен .NET Core SDK версии 2.1. В этой версии добавлено несколько дополнительных настроек проектов для управления неймингом и содержимым пакетов с глобальными консольными утилитами.


Минимальные необходимые параметры проекта:


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <PackAsTool>true</PackAsTool>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

</Project>

Дополнительные (не обязательные) параметры, управляющие сборкой пакета:


  • AssemblyName — задает название .dll файла вашего консольного приложения.
  • ToolCommandName — название команды, по которому пользователь будет запускать вашу консольную утилиту. По умолчанию оно совпадает с названием .dll файла (которое задано в параметре AssemblyName).
    • Название команды не обязательно должно начинаться с dotnet-. Можно использовать любое название без пробелов.
    • Если название начинается с dotnet-, то утилиту можно будет запускать как команду утилиты dotnet (убедитесь, что команды с таким именем еще нет). Например, утилита dotnet-say-moo может быть вызвана и как dotnet-say-moo, и как dotnet say-moo.
  • PackageId — идентификатор NuGet пакета. По умолчанию совпадает с названием .csproj файла. Этот идентификатор нужно указывать при установке. При этом он может отличаться от названия команды (ToolCommandName) и названия .dll файла (AssemblyName).
  • PackageVersion — версия NuGet пакета (по умолчанию 1.0.0). Также вместо PackageVersion можно использовать VersionPrefix и VersionSuffix.

Пример использования этих параметров:


<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <PackAsTool>true</PackAsTool>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    <ToolCommandName>pineapple</ToolCommandName>
    <PackageId>dole-cli</PackageId>
    <PackageVersion>1.0.0-alpha-$(BuildNumber)</PackageVersion>
    <AssemblyName>Dole.Cli</AssemblyName>
  </PropertyGroup>
</Project>

Сборка и установка пакета


Сборка пакета происходит как обычно — при помощи команды dotnet pack. SDK увидит, что установлен параметр PackAsTool=true и автоматически сгенерирует нужные дополнительные файлы.


dotnet pack --output ./packages

С помощью параметра --source-feed вы можете установить пакет, который еще не опубликован в репозитории пакетов NuGet. Это может быть полезно для проверки, что всё сделано правильно. Также, если версия пакета — не релизная (например, 3.0.0-alpha — содержит что-то, кроме трех чисел), нужно при установке явно указать её.


Например:


dotnet tool install -g my-package-name --version 3.0.0-alpha --source-feed ./packages/

Что внутри пакета


Как я писал выше, команда dotnet pack собирает пакет особым образом, если в файле проекта указан параметр PackAsTool=true.


Зависимости


В пакет попадают не только файлы, полученные через dotnet build, но и все другие зависимости вашего проекта (подключенные сторонние пакеты NuGet). Все файлы, необходимые для работы вашей консольной утилиты, должны быть включены в NuGet пакет. Команда dotnet-tool-install не устанавливает зависимости, указанные в резделе <dependencies> вашего .nuspec файла.


DotnetToolSettings.xml


Генерируется специальный файл DotnetToolSettings.xml, который содержит информацию о вашем консольном приложении. Если этого файла по какой-то причине не окажется в пакете (например, пытаетесь установить произвольный пакет как консольную утилиту), то при установке вы получите ошибку:


The settings file in the tool’s NuGet package is invalid: Settings file ‘DotnetToolSettings.xml’ was not found in the package.

Пример содержимого файла:


<DotNetCliTool Version="1">
  <Commands>
    <Command Name="my-command-name" EntryPoint="my-file.dll" Runner="dotnet" />
  </Commands>
</DotNetCliTool>

Сейчас есть следующие требования к файлу DotnetToolSettings.xml:


  • Файл DotnetToolSettings.xml должен находиться в папке tools/$targetframework/any/. Например: tools/netcoreapp2.1/any/DotnetToolSettings.xml.
  • Пакет должен содержать только один файл DotnetToolSettings.xml.
  • В файле DotnetToolSettings.xml должна быть описана только одна секция <Command>.
  • Значение атрибута Runner должно быть "dotnet".
  • В качестве значения атрибута EntryPoint должно быть указано название .dll файла, который лежит в одной папке с файлом DotnetToolSettings.xml.

<packageType name="DotnetTool" />


В .nuspec файл автоматически добавляется параметр <packageType name="DotnetTool" />. Например:


<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <!-- Следующий фрагмент обязательно должен быть! -->
    <packageTypes>
      <packageType name="DotnetTool" />
    </packageTypes>
    <!-- ... -->
  </metadata>
</package>

Если параметр packageType[name] не указан как DotnetTool, то при установке получите ошибку:


error NU1212: Invalid project-package combination for awesome-tool 1.0.0. DotnetToolReference project style can only contain references of the DotnetTool type

Конечно, это не очень понятное сообщение об ошибке. На Github есть issue, чтобы это улучшить.


Что может пойти не так


Глобальные утилиты — глобальные для пользователя, а не для компьютера


.NET Core CLI по умолчанию устанавливает глобальные утилиты в папку $HOME/.dotnet/tools (на Linux/macOS) или в папку %USERPROFILE%\.dotnet\tools (на Windows). Это значит, что вы не можете установить пакет глобально для всех пользователей компьютера с помощью dotnet tool install --global. Установленные утилиты доступны только для пользователя, который их установил.


Не хватает пути в переменной PATH


Обычно .NET Core автоматически добавляет путь к папке с установленными утилитами в переменную окружения PATH, чтобы они были доступны по имени, без указания полного пути. Но иногда это может не сработать. Например:


  • если вы добавили переменную окружения DOTNET_SKIP_FIRST_TIME_EXPERIENCE (например, чтобы ускорить первый запуск .NET Core), то значение переменной PATH может быть не установлено при первом использовании
  • macOS: если вы установили CLI из .tar.gz файла (а не из .pkg файла), то у вас может не быть файла /etc/paths.d/dotnet-cli-tool который настраивает переменную PATH.
  • Linux: вам нужно вручную отредактировать свой shell environment file, т.е ~/.bash_profile или ~/.zshrc

В этом случае при запуске своей консолььной утилиты вы получите ошибку. Например:


bash: my-command-name: command not found

Чтобы всё заработало, нужно добавить в переменную PATH путь к папке с утилитами. Например так (после добавления нужно перезапустить терминал):


cat << \EOF >> ~/.bash_profile
# Add .NET Core SDK tools
export PATH="$PATH:/Users/<user-name>/.dotnet/tools"
EOF

Или вот так можно добавить для текущей сессии:


export PATH="$PATH:/Users/<user-name>/.dotnet/tools"

Примеры выше — для MacOS. Для других систем всё аналогично. Кроме того, при установке вашей глобальной консольной утилиты команда dotnet tool install проверит, что переменная PATH правильно настроена и предложит варианты решения, если это не так.


.NET Core CLI установлен не в папку по умолчанию


Если вы скачали .NET Core CLI как .zip/.tar.gz архив и распаковали его в папку, которая отличается от папки по умолчанию, то при запуске своей консольной утилиты вы можете получить ошибку:


  • Windows: A fatal error occurred, the required library hostfxr.dll could not be found
  • Linux: A fatal error occurred, the required library libhostfxr.so could not be found
  • macOS: A fatal error occurred, the required library libhostfxr.dylib could not be found

В сообщении об ошибке будет также дополнительная информация:


If this is a self-contained application, that library should exist in [some path here].
If this is a framework-dependent application, install the runtime in the default location [default location] or use the DOTNET_ROOT environment variable to specify the runtime location.

Причина в том, что запускаемы файл, который генерируется командой dotnet tool install при установке пакета, ищет .NET Core в папке по умолчанию. Вы можете переопределить пути по умолчанию, установив переменную окружения DOTNET_ROOT. Например:


# Windows
set DOTNET_ROOT=C:\Users\username\dotnet

# MacOS/Linux
export DOTNET_ROOT=/Users/username/Downloads/dotnet

Подробности в issue на GitHub.


Заключение


Мы познакомились с глобальными консольными утилитами в .NET Core. На мой взгляд, это очень крутая штука. Я крайне счастлив, что команда .NET Core её запилила. Не могу дождаться, когда все начнут её использовать :)

Комментарии (13)


  1. dmitry_dvm
    21.05.2018 12:56

    Это чтобы клепать консольные приложения, которые легко распространять (через нагет) и деплоить?


    1. dima117 Автор
      21.05.2018 13:04

      Ага, как в npm.


      1. alexs0ff
        21.05.2018 13:13

        Проблема в том, что npm дает заглянуть в исходники, чтобы понять что делает та или иная тулза, а тут предлагают распространять «черный ящик». ИМХО но не секюрно как-то.


        1. dima117 Автор
          21.05.2018 13:46

          Это относится к любым пакетным менеджерам и магазинам приложений)


          1. alexs0ff
            21.05.2018 13:51

            любым пакетным менеджерам

            npm распространяется с исходниками же?


            1. imanushin
              21.05.2018 17:08

              dima117,


              1. У nuget и maven есть пакеты-дублеры с исходниками. Т.е. вдобавок к пакету с dll файлами (или class файлами в случае Java) можно публиковать пакет с исходниками. См. для nuget и для maven.
              2. Уже давным давно известна атака, когда публикуется новая версия популярной библиотеки, а все, кто на неё ссылается (пусть даже косвенно) получают зараженный код. То есть если ваш проект ссылается на пакет A, а он ссылается на пакет B, то заразу пихают в новую версию пакета B, а в пакете A делают обновление в стиле "update dependencies" + пара исправлений. Вы обновляете пакет A (где нет ничего страшного — там всего лишь обновили зависимости и добавили пару фиксов), а с пакетом B к вам прилетает подарок
              3. А как вы изучаете изменения в проектах типа Angular? Там же немало кода, и он не всегда самый простой...


            1. taliban
              21.05.2018 20:18
              +1

              При желании никто не мешает обфусцировать исходный код, либо сделать прокси на бинарную библиотеку с аргументом «надо для скорости». При желании можно все что угодно.


        1. qw1
          21.05.2018 17:44

          В .NET-сборках слишком много метаданных.
          Если их специально не обфусцировали, можно считать, что декомпиляцией получим те же исходники (за исключением оригинального форматирования кода, комментариев и имён локальных переменных).


          1. QuAzI
            22.05.2018 00:11

            А есть что-то живое из обфускаторов для .NET Core? Что-то что можно запустить например в Ubuntu или после чего можно обфусцированный софт в Ubuntu крутить?


            1. qw1
              22.05.2018 00:13

              «Core» относится к библиотеке классов. Формат у исполняемых файлов и байткод одинаковый. Берите любой обфускатор.


        1. Flaksirus
          21.05.2018 18:45
          +1

          Эм, это же дотнет, тут посмотреть исходник всего на шажок сложнее чем в npm. (Само собой в основном и редко бывает иначе)


          1. SanSYS
            21.05.2018 20:19

            Вдруг кто не знает — посмотреть исходник dll под .net можно через dotPeek


            1. Evengard
              22.05.2018 11:26

              Предпочитаю Telerik-овый. Бывали случаи, когда dotPeek не мог прожевать файл, а телериковский справлялся без запинок.