blog post image
Andrew Lock avatar

Andrew Lock

~7 min read

New in .NET Core 3.0: local tools

Exploring ASP.NET Core 3.0 - Part 7

In this post I explore the new local tools feature introduced in .NET Core 3.0. I show how to install and run local tools using the dotnet-tools manifest, describe how to work with multiple manifests, and describe how the tools are installed.

Global tools, local-ish tools, and finally, Local tools

.NET Core 2.1 introduced the concept of global tools that are CLI tools (console apps really) that you can install using the .NET Core SDK. These tools are available globally on your machine, so can be used for a wide variety of things.

I've recently migrated to using the Cake global tool for new builds, by installing the global tool into the project folder using the tools-path option, and running the tools from there. Installing into the project folder in this way means you can use a different version of the global tool for each project, rather than being forced to update all your tools at once.

However this "local" use of global tools always felt a bit clumsy, and in .NET Core 3.0 we now have explicit support for "project-specific" local tools. Stuart Lang wrote a nice introductory post on the feature here. This post is very similar, but with a couple of extra details.🙂

Local tools in .NET Core 3.0

In .NET Core 3.0 you can now specify global tools that are required for a specific project by creating a dotnet-tools manifest. This is a JSON file which lives in your repository and is checked-in to source control. You can create a new tool-manifest by running the following in the root of your repository:

dotnet new tool-manifest

By default, this creates the following manifest JSON file dotnet-tools.json inside the .config folder of your repository:

{
  "version": 1,
  "isRoot": true,
  "tools": { }
}

The initial manifest doesn't include any tools, but you can install new ones by running dotnet tool install (i.e. without the -g or --tool-path flag required in .NET Core 2.x). So you can require the Cake global tool for your project by running:

> dotnet tool install Cake.Tool

You can invoke the tool from this directory using the following commands: 'dotnet tool run dotnet-cake' or 'dotnet dotnet-cake'.
Tool 'cake.tool' (version '0.35.0') was successfully installed. Entry is added to the manifest file C:\repos\test\.config\dotnet-tools.json.

This updates the manifest by adding the cake.tool reference to the tools section, including the version required (the current latest version - you can update the version manually as required), and the command you need to run to execute the tool (dotnet-cake):

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "cake.tool": {
      "version": "0.35.0",
      "commands": [
        "dotnet-cake"
      ]
    }
  }
}

When a colleague clones the repository and wants to run the Cake tool, they can run the following commands to first restore the tool, and then run it:

# Restore the tool NuGet packages
dotnet tool restore
# Execute the tool associated with command "dotnet-cake" and pass the arguments: --version
dotnet tool run dotnet-cake --version
# or you can use:
dotnet dotnet-cake --version
# or even shorter:
dotnet cake --version

For build tools like Cake, where you might want or need to have different versions installed for different projects, the .NET Core 3 local tools are great. The "global tools with local tool-path" approach I showed in my earlier post was OK, but you had to do some manual work to ensure the correct version was installed. That all gets simpler with .NET Core 3, as I'll show in a later post.

How are .NET Core local tools implemented?

In this section, I'll dig into a couple of questions I had after giving local tools a try. Namely, where are local tools installed, what are the other properties in the manifest file, and can I put the manifest file somewhere else?

At the time of writing, there's no official documentation for .NET Core local tools, so most of the information below is from this issue, plus my experimentation!

The dotnet-tools.json manifest

When you create a new manifest using dotnet new tool-manifest you get a JSON file like the following:

{
  "version": 1,
  "isRoot": true,
  "tools": { }
}

The version property is specifying the version of the dotnet-tools schema. It's not the file version, it's the schema version, so you'll need to leave that it set to 1. Later versions of the .NET Core SDK may update the schema to add extra/different functionality, and the version number can be used to determine which version of the schema to use.

The isRoot property is related to how the dotnet tool command searches for manifests. I'll get to the details of that shortly, but in summary, isRoot means "stop searching, I'm what you're looking for". It is the "root" manifest, i.e. the top-level manifest.

How does the .NET Core SDK locate local manifests?

The dotnet tool command checks in a number of locations when looking for a dotnet-tools.json manifest:

  1. The .config folder in the current directory (./.config/dotnet-tools.json)
  2. In the current directory (./dotnet-tools.json)
  3. In the parent directory (../dotnet-tools.json)
  4. In each parent directory until you reach the root

As soon as it finds a dotnet-tools.json manifest for which isRoot is true, it stops searching. The local tools available are all those listed in the root manifest, plus all those listed in manifests found while searching for the root.

You can view the local tools available in a given folder by running dotnet tool list.

For example, imagine you have a non-root manifest in the .config folder that requires the Cake global tool. You also have a non-root manifest in the current directory that installs the dotnetsay global tool, and a root manifest in the parent directory that installs the dotnet-tinify tool. Running dotnet tool list shows that all three of these tools are available:

Package Id         Version      Commands           Manifest
-----------------------------------------------------------------------------------------------------
cake.tool          0.35.0       dotnet-cake        C:\repos\test\.config\dotnet-tools.json
dotnetsay          2.1.4        dotnetsay          C:\repos\test\dotnet-tools.json
dotnet-tinify      0.2.0        dotnet-tinify      C:\repos\dotnet-tools.json

Precedence is obviously important here for knowing when to stop searching (due to isRoot), but it also handles the case where different versions of a tool are defined in more than one manifest. In that case, the first manifest found wins. So if version 1.0.0 of the dotnetsay tools was in the .config folder manifest, then dotnet tool list would output the following:

Package Id         Version      Commands           Manifest
-----------------------------------------------------------------------------------------------------
cake.tool          0.35.0       dotnet-cake        C:\repos\test\.config\dotnet-tools.json
dotnetsay          1.0.0        dotnetsay          C:\repos\test\.config\dotnet-tools.json
dotnet-tinify      0.2.0        dotnet-tinify      C:\repos\dotnet-tools.json

Note the Version and the Manifest for the dotnetsay command compared to the previous table.

Of course, you shouldn't really ever have to care about these details. The whole point of local tools is that they're local, checked in with your source control, and don't rely on things already existing (e.g. manifests in parent folders). I strongly suggest using only a single manifest, ensuring isRoot is true, and placing it either in the .config folder or the root of your project.

Where are .NET Core local tools installed?

The short answer is they're installed in the global NuGet package folder. .NET Core global/local tools are just console apps distributed as special NuGet packages. So they're downloaded to the global folder and unpacked as though they're normal NuGet packages. If you install the same version of a tool in multiple manifests, only a single copy of the NuGet package is installed.

When you run a global tool, it runs the app from the NuGet package. So if you install the dotnet-serve global tool for example:

dotnet tool install dotnet-serve

and then run it:

dotnet tool run dotnet-serve
# or you can use:
dotnet dotnet-serve
# or even shorter:
dotnet serve

then looking in process explorer you can see the tool is being run directly from the ~/.nuget/packages folder for the installed version of the tool (1.4.1 in this case)

The Windows process explorer when running 'dotnet tool run dotnet-serve'

Given the tools run from the shared NuGet cache, uninstalling a tool from a manifest (using dotnet tool uninstall <toolname> simply removes the entry from the dotnet-tools.json manifest. The NuGet package remains cached, and so can still be used by other apps. If you completely want to remove the tool from your system, you'll need to clear your NuGet cache.

Summary

In this post I described the new local tools feature introduced in .NET Core 3.0. This feature allows you to include a manifest in your project that lists the .NET Core CLI tools it requires. This allows you to have different tools (and different versions of tools) for different projects. I showed how to install and run local tools, explained the format of the manifest file, and how multiple manifest files can be used if necessary. Finally I described how local tools work, by running the tools from the global NuGet package cache.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?