.Net, Azure and occasionally gamedev

Global tools in .Net Core (and some gotchas)

2018/09/22

A cool new feature that was implemented with .Net Core 2.1 is the global tool.

Any .Net Core project can now be packaged as a global tool and distributed via NuGet.

Once installed on a users pc it will be available anywhere on the commandline (thanks to .Net Core it works on Windows, Mac and Linux!).

Given an existing sample package "my-tool" on NuGet you just:

dotnet tool install --global my-tool

on the commandline and after a short while it is installed and can be called from anywhere via "my-tool".

Getting started

In order to create such a package your project has to fulfill a few requirements.

First requirement is obviously that it must be a .Net Core project targeting .Net Core 2.1 or above.

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

  <PropertyGroup>
    <PackAsTool>true</PackAsTool>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>
  
  <PropertyGroup>
    <authors>MarcStan</authors>
    <owners>MarcStan</owners>
    <!-- add any other nuget metadata here -->
  </PropertyGroup>
</Project>

The second requirement is the "PackAsTool" property.

PackAsTool will tell nuget to format the nupkg file correctly (specifically it will include all your dependencies into the NuGet package).

Another helpful (but optional) bit is the ToolCommandName. It allows you to specify a custom command name.

if you don't set it, the csproj file name will be used by default. However if your project is prefixed with your company name (e.g. Company.AwesomeUtil) you might want to set ToolCommandName to "awesomeutil" or "awesome-util" so your users don't always have to type "Company.awesomeutil".

<PropertyGroup>
    <ToolCommandName>my-tool</ToolCommandName>
</PropertyGroup>

After you've set those bits, all that's left is to dotnet pack:

dotnet pack MyTool.csproj -c Release

To test locally you can run this command:

dotnet tool install --global my-tool --add-source ./

where ./ is the path to the directory containing the final nupkg file.

Your tool should now be available as an executable that you can run from anywhere.

If you're happy with it, you can publish it to NuGet:

dotnet nuget push *.nupkg

Internals

The tool is globally available because the tools directory (%userprofile%.dotnet\tools) is added to the path and for each installed tool a new exe is created in there (exe on Windows, ELF binary on Linux).

The only purpose of this executable is to call "dotnet %toolpath%/my-tool.dll" where %toolpath% is "%userprofile%.dotnet\tools.store%your package and version%".

This means it's now just as simple to distribute globally available dotnet tools as it has been with node for a while!

Pitfalls

Global really means user

While it says "--global" right on the commandline it is really "user" context, as the tool is only available for the current user (as shown in the internals it's stored in the %userprofile% folder).

For most developers this will be fine as they always stay within their single user context. However it can catch you of guard if you manually install such a tool on a build server via RDP (user context) and then have builds fail because they can't find the tool (network service context or whichever user the build agent runs as).

Authenticated feeds

As Azure DevOps makes it very easy to set up private feeds it is common for many companies to use them to host their private packages.

It took me quite a bit of digging to figure out how to download a tool from such a feed.

Installing from a custom source is easily supported:

dotnet tool install --global my-tool --add-source https://myvstsaccount.pkgs.visualstudio.com/_packaging/MyFeed/nuget/v3/index.json

Except that you are greeted by the most generic error message:

error : Unable to load the service index for source https://myvstsaccount.pkgs.visualstudio.com/_packaging/MyFeed/nuget/v3/index.json.
error :   Response status code does not indicate success: 
The tool package could not be restored.
Tool 'my-tool' failed to install. This failure may have been caused by:

* You are attempting to install a preview release and did not use the --version option to specify the version.
* A package by this name was found, but it was not a .NET Core tool.
* The required NuGet feed cannot be accessed, perhaps because of an Internet connection problem.
* You mistyped the name of the tool.

After a process of elimination (and the "401 Unauthorized" hint in the output) I was sure it was an authentication issue. I dug through dozens of issues on the NuGet repo and finally found another switch: "--configfile"

I also found a link describing how to set up authentication for the dotnet cli.

It seems the cross platform support for login is just about to be rolled out so until it works fully the workaround still applies.


Update: Early versions of the cross-platform credential provider are available already and it looks like they will work automagically starting with VS 2017 15.9+ and Nuget 4.9+.


In short you will need to create a file "NuGet.config" with content:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="your-feed-name" value="http://your-feed" />
  </packageSources>
  <packageSourceCredentials>
    <your-feed-name>
      <add key="Username" value="your-email-here" />
      <add key="ClearTextPassword" value="your-pat" />
    </your-feed-name>
  </packageSourceCredentials>
</configuration>

Entering "your-feed-name" (it's 3 times your-feed-name in the sample above, be sure to replace all), the link, your email and a PAT (Personal Access Token).

To create a PAT, go to Azure DevOps in the top right corner click your profile icon and go to "Security" -> Personal access tokens and create a new one with at least "Packaging (read)" permissions.

The PAT allows anyone who has it to impersonate you on your Azure DevOps account with the rights you gave it. So never check your PAT into sourcecontrol.

With the file in place you can now run:

dotnet tool install --global my-tool --configfile /path/to/NuGet.config

and the tool should successfully install.

If you're security conscious you can delete the NuGet.config as well as revoke the PAT in Azure DevOps (you can just create a new one if you ever need to update the tool).

Authenticated feeds round #2

If you have done everything correctly above, you might still run into authentication issues.

The most likely reason is that you have added global nuget sources that require authentication (such as your companies VSTS feed).

For those feeds nuget authentication will fail and prevent you from downloading packages as stated in the comment by Nikolche Kolev:

This is by design as NuGet hard-fails when there is a failed source because this can potentially cause inconsistent restores based on which sources are available. This even applies when the global feed has the same url as the one listed in the NuGet.config!

I personally recommend the use of NuGet.config files (feed url only, no password!) checked into source control along each solution. That way each solution loads the correct NuGet feeds from its local NuGet.config without cluttering your global system install. Especially handy when you have access to a multitude of NuGet feeds but only ever need to work with a selected few (depending on the open solution).

To check which feeds you have globally registered, you can run:

nuget sources

If you don't have nuget in your path you can either download and add it or you can open Visual Studio and go to "Tools -> Options -> NuGet Package Manager -> Package Sources".

Once there, temporarily uncheck any sources that require authentication and click ok.

Now your tool install should work with the "--configfile" switch.

Once you have installed the tool you are of course free to re-add any global nuget sources via Visual Studio or the commandline.

The tool must be a separate nuget package

This is more of a niche issue but I still ran into it:

As the tool must have the "PackAsTool" property set to true, it cannot be referenced from any other project (nuget forbids adding references to "tool" packages).

Generally when you build a tool you don't intend it to be used as a reference by other projects, however I recently built a tool that I also wanted to add as a post build step (using nuget and its msbuild integration).

While I could have used the tool directly in a post build step, the project build would fail with "my-tool executable not found" for any user who doesn't have it installed which is just a bad developer experience overall (it doesn't tell new developers what they have to do to fix the issue).

Instead I opted for a MSBuild "AfterBuild" target which you can integrate easily via a nuget package. This also means the package is correctly restored on first build for any developer and runs without any errors.

As the title says I couldn't reuse the nuget package because .Net Core tool packages cannot be install into any project (if you try it will error out with "tool package cannot be added as a reference").

Instead I had to build a second (wrapper) project "my-tool.MSBuild" that simply shipped the .Net Core tool along with a "build/netstandard/my-tool.MSBuild.targets" file.

Said target file is automatically invoked in any project that it is installed into (Naming convention: nuget package id = targets file name inside "build/%platform%" folder).

The targets file can then invoke "dotnet ../../tools/netcoreapp2.1/my-tool.dll" to achieve build integration.

tagged as .Net Core, Visual Studio Team Services, VSTS and Azure DevOps