.NET Core Global Tools and Gotchas
Getting started with using or creating a .NET Core global tool package, and how to deal with the gotchas
As announced recently in the .NET Core 2.1 Roadmap, the .NET Core 2.1.300 SDK will add a feature called “.NET Core Global Tools”. This announcement contains a brief snippet of how the tools will work. As this feature is new, there are some rough edges. In this post, I’ll go over the basic design of how global CLI tool should work, some of the gotchas, and how to make it all owrk.
For those who want to get started on code right away, checkout the project templates in https://github.com/natemcmaster/DotNetGlobalTool. You will need to download a pre-release build of the .NET Core CLI that supports these features.
At the time of writing this post, this feature is still in progress and is subject to change.
Tip: For a real-world example of creating global tools, see https://github.com/aspnet/DotNetTools/, which contains the source code for a handful of .NET Core global tools created by the ASP.NET Core team.
A .NET Core global tool is a special NuGet package that contains a console application. When installing a tool, .NET Core CLI will download the package, extract it to disk, and make your console tool available as a new command in your shell by adding to the PATH environment variable.
Users install tools by executing “dotnet install tool”:
> dotnet install tool -g awesome-tool
Once installed, the command contained in the “awesome-tool” package are on PATH.
Creating your own package
To simplify getting started, I’ve created a project templates.
Install the templates package
dotnet new --install "McMaster.DotNet.GlobalTool.Templates::2.1.300-preview1"
Create a new project
dotnet new global-tool --command-name awesome-tool
Once you have this project, you can create your tool package like this:
dotnet pack --output ./
This creates a file named
You can install your package like this:
dotnet install tool -g awesome-tool --source ./
When you are ready to share with world, upload the package to https://nuget.org.
Under the hood
The NuGet package that contains a global tool must completely contain the application. Unlike project tools (aka
DotNetCliToolReference), you produce a package by executing
dotnet publish, and putting everything that is
in the publish output into the NuGet package.
dotnet install tool executes, it…
dotnet restorewith special parameters to fetch the package.
- extracts the package into
- Generates an executable shim from the extract packge into
The executable shim generation is controlled by a file named
DotnetToolSettings.xml. It’s basic format looks like this.
<?xml version="1.0" encoding="utf-8" ?> <DotNetCliTool> <Commands> <Command Name="my-command-name" EntryPoint="MyApp.dll" Runner="dotnet" /> </Commands> </DotNetCliTool>
At the time of writing this post, this feature has some restrictions and unexpected behaviors. These may change as the feature evolves.
Gotcha 1 - there is no uninstall (yet)
There is no uninstall after
dotnet install tool.
I’m willing to bet this will change by RTM. But for previews, you can manually uninstall tools by deleting this files:
(Windows) %USERPROFILE%\.dotnet\tools\awesome-tool.exe %USERPROFILE%\.dotnet\tools\awesome-tool.exe.config %USERPROFILE%\.dotnet\toolspkgs\awesome-tool\ (macOS/Linux) $HOME/.dotnet/tools/awesome-tool $HOME/.dotnet/toolspkgs/awesome-tool/
Gotcha 2 - PATH
> dotnet install tool -g awesome-tool > awesome-tool awesome-tool: command not found awesome-tool : The term 'awesome-tool' is not recognized as the name of a cmdlet, function, script file, or operable program.
Global tools are installed to
%USERPROFILE%\.dotnet\tools (Windows) or
$HOME/.dotnet/tools (macOS/Linux). The .NET Core CLI tool makes a best effort to help you ensure this is in your PATH environment variable. However, this
can easily be broken. For instance:
- if you have set the
DOTNET_SKIP_FIRST_TIME_EXPERIENCEenvironment variable to speed up intial runs of .NET Core, then your PATH may not be updated on first use
- macOS: if you install the CLI via
.pkg, you’ll be missing the
/etc/paths.d/dotnet-cli-toolfile that configures PATH.
- Linux: you will need to edit your shell environment file. e.g.
(May require restarting your shell.)
setx PATH "$env:PATH;$env:USERPROFILE/.dotnet/tools"
echo "export PATH=\"\$PATH:\$HOME/.dotnet/tools\"" >> ~/.bash_profile
Gotcha 3 - tools are user-specific, not machine global
The .NET Core CLI installs global tools to
$HOME/.dotnet/tools (Linux/macOS) or
This means you cannot install a global tool for the entire machine using
dotnet install tool --global.
Installed tools are only available to the user who installed them.
Package authoring and SDK
The best experience for authoring a global tool requires a .NET Core SDK version 2.1.300-preview1-008000 or newer. This SDK provides a few simple settings that adjust package layout to match the requirements for .NET Core global tools. You only need to add two properties to enable packing the project as a global tool.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <ToolCommandName>awesome-tool</ToolCommandName> <PackAsTool>true</PackAsTool> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp2.0</TargetFramework> </PropertyGroup> </Project>
Tip: Until .NET Core 2.1 is released, you may see build warnings when calling
dotnet pack on this project. To workaround this, add the following:
<PropertyGroup> <NETCorePlatformsImplicitPackageVersion>2.0.1</NETCorePlatformsImplicitPackageVersion> </PropertyGroup>
I recommend this for now as it will also prevent an install error for users that would look like this:
error NU1102: Unable to find package Microsoft.NETCore.Platforms with version (>= 2.1.0-preview1-26202-03)
Deep-dive: package requirements
There are some very specific requirements for CLI global tools. The SDK takes care of most of these for you when you specify PackAsTool=true.
If you cannot yet upgrade to a nightly build of the .NET Core SDK but want to try this out, you can workaround these restrictions. The templates package McMaster.DotNet.GlobalTool.Templates, version 2.1.300-preview-build7 contains a template that workarounds this for older SDKs.
dotnet new --install "McMaster.DotNet.GlobalTool.Templates::2.1.300-preview1-build7"
Publish output into pack
As mentioned above, the tools package must contain all your apps dependencies.
This can be collected into one place by using
dotnet pack only contains the output of
This output does not normally contain third-party assemblies, static files, and the
which is why you need to publish, not just build.
Early version of the SDK don’t support packing global tools. You can workaround this by chaining publish before dotnet-pack, and using a .nuspec file.
<!-- In .csproj file --> <PropertyGroup> <NuspecFile>globaltool.nuspec</NuspecFile> </PropertyGroup> <ItemGroup> <Content Include="DotnetToolSettings.xml" CopyToPublishDirectory="PreserveNewest" /> </ItemGroup> <Target Name="PackGlobalTool" BeforeTargets="GenerateNuspec" DependsOnTargets="Publish"> <PropertyGroup> <NuspecProperties> publishDir=$(PublishDir); </NuspecProperties> </PropertyGroup> </Target>
<!-- In your .nuspec file --> <package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd"> <!-- ... --> <files> <file src="$publishdir$" target="tools/netcoreapp2.0/any/" /> </files> </package>
Error NU1202 and “Microsoft.NETCore.Platforms”
A global tool package must specify a dependency to “Microsoft.NETCore.Platforms”. This is required because dotnet-install-tool does some magic stuff. Ask me about this in the comments if you want me to explain. Without this dependency, the package will fail to install with
error NU1202: Package is not compatible with netcoreapp2.1 (.NETCoreApp,Version=v2.1) / win10-x64.
To workaround, add this to your nuspec:
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd"> <metadata> <!-- ... --> <dependencies> <dependency id="Microsoft.NETCore.Platforms" version="2.0.1" /> </dependencies> </metadata> <!-- ... --> </package>
See this GitHub issue for details.
Restrictions on DotnetToolSettings.xml
The schema for this file looks like this:
<DotNetCliTool> <Commands> <Command Name="$name" EntryPoint="$file" Runner="$runner" /> </Commands> </DotNetCliTool>
Lots of restrictions here:
The file must exist in the NuGet package under
tools/$targetframework/$runtimeidentifier/. If your application is portable to all platforms, use
You may only specify one
DotnetToolSettings.xmlfile per package.
You may only specify one
The only allowed value for
The value for
EntryPointmust be a
.dllfile that sits next to
DotnetToolSettings.xmlin the package.
Error NU1212 and package type
Installation may fail with this error
error NU1212: Invalid project-package combination for awesome-tool 1.0.0. DotnetToolReference project style can only contain references of the DotnetTool type
What this means is that dotnet-install-tool is currently restricted to only installing a .NET Core package that has specific metadata. That metadata can be defined in your nuspec file and must be set as follows:
<?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>
You must redistribute any of your dependencies in your tools package. Dependencies define in the
<dependencies> metadata of your NuGet package are not
honored by dotnet-install-tool.
This is an awesome feature. Super happy the .NET Core CLI team is working on it. Can’t wait to see what people make.