How to Create a NuGet Package that Runs your Console App as an MSBuild Task

Tutorial using the Build Task NuGet Package Template with GitHub or `dotnet new`

Introduction

The Build Task NuGet Package Template sets up a cross-platform msbuild task that runs at build.

Background

I started looking for a way to run a task at build when I started work on NuGetDefense. I needed to run a console app and emit warnings/errors to MSBuild as part of the Build. I found several articles detailing the pitfalls of trying to write a cross-platform task and settled instead on an MSBuild ExecTask run using a nuget Targets file.

Using the Code

Using the repository is as simple as installing the template and then writing your app.

dotnet new --install BuildTaskNuGetPackage.Template::\*
dotnet new nugetbuildtask -n ProjectName -A "FirstName LastName"

OR:

If you prefer to use GitHub, you can start with the template repository.

  1. Click the Use This Template button
  2. Fill out the page including your repository name. DO NOT check the Include all branches this would include the branch used to create the dotnet new template as well.

Regardless of which method you use, you will need to write your console app and update the nuspec to include any dependencies that are needed for your app to run.

This is how the files are included for NuGetDefense.

 <files>
    <!-- <file src="Src/bin/Release/net461/NuGetDefense.exe" 
               target="tools/net461/NuGetDefense.exe" /> -->
    <file src="build/nugetdefense.targets" target="build/nugetdefense.targets" />
    <file src="bin/Release/netcoreapp3.1/NuGetDefense.deps.json" 
          target="tools/netcoreapp3.1/NuGetDefense.deps.json" />
    <file src="bin/Release/netcoreapp3.1/NuGetDefense.dll" 
          target="tools/netcoreapp3.1/NuGetDefense.dll" />
    <file src="bin/Release/netcoreapp3.1/NuGetDefense.Core.dll" 
          target="tools/netcoreapp3.1/NuGetDefense.Core.dll" />
    <file src="bin/Release/netcoreapp3.1/NuGetDefense.NVD.dll" 
          target="tools/netcoreapp3.1/NuGetDefense.NVD.dll" />
    <file src="bin/Release/netcoreapp3.1/NuGetDefense.OSSIndex.dll" 
          target="tools/netcoreapp3.1/NuGetDefense.OSSIndex.dll" />
    <file src="bin/Release/netcoreapp3.1/NuGetDefense.runtimeconfig.json" 
          target="tools/netcoreapp3.1/NuGetDefense.runtimeconfig.json" />
    <file src="bin/Release/netcoreapp3.1/MessagePack.dll" 
          target="tools/netcoreapp3.1/MessagePack.dll" />
    <file src="bin/Release/netcoreapp3.1/MessagePack.Annotations.dll" 
          target="tools/netcoreapp3.1/MessagePack.Annotations.dll" />
    <file src="bin/Release/netcoreapp3.1/Microsoft.Bcl.AsyncInterfaces.dll" 
          target="tools/netcoreapp3.1/Microsoft.Bcl.AsyncInterfaces.dll" />
    <file src="bin/Release/netcoreapp3.1/NuGet.Versioning.dll" 
          target="tools/netcoreapp3.1/NuGet.Versioning.dll" />
    <file src="bin/Release/netcoreapp3.1/System.Text.Json.dll" 
          target="tools/netcoreapp3.1/System.Text.Json.dll" />
    <file src="bin/Release/netcoreapp3.1/VulnerabilityData.bin" 
          target="tools/netcoreapp3.1/VulnerabilityData.bin" />
    <file src="lib/net461/\_.\_" target="lib/net461/\_.\_" />
    <file src="lib/netcoreapp3.1/\_.\_" target="lib/netcoreapp3.1/\_.\_" />
    <file src="lib/netstandard2.0/\_.\_" target="lib/netstandard2.0/\_.\_" />
  </files>

The Hard Way

So you want to do it the hard way? Maybe you like a challenge or want a deeper understanding of the code you're using.

Get the Console App

This should be fairly basic. You can use Visual Studio or the dotnet cli to create a new console app or use one that already exists. For the sake of this tutorial, I'm going to assume it's written in .NET Core 3.1 (so it is cross-platform) but with a few tweaks, you can use anything. Make sure to build the project so you have a list of all the files and dependencies required to run the app.

Create the Nuspec

The .nuspec file is the file that tells nuget how to put the package together. You can review the documentation for this file to see more of what it can do but we are only going to look at a few of the potential fields.

Create the file `yourProjectName.nuspec` somewhere in your repository. I generally put it right beside the project file, but you could potentially have it anywhere in your project/solution. Edit the contents to match the following:

<?xml version="1.0"?>
<package>
  <metadata>
    <id>mybuildtask</id>
    <title>MyBuildTask</title>
    <version>0.0.0.1</version>
    <authors>Owner Name(s)</authors>
    <owners>Owner Name(s)</owners>
    <requireLicenseAcceptance>true</requireLicenseAcceptance>
    <description>A Description of my Package</description>
    <license type="expression">MIT</license>
  </metadata>
</package>

You will want to replace the appropriate fields with values that match your project/organization. And you will want to keep the id as a lowercase value (some NuGet sources may be case-sensitive) to avoid edge-case issues.

Now you need to look at the bin/Release/ folder and decide which files need to be included. Typically, this will be every item in that folder. Once you have a list, Add them to a files collection at the bottom of the Nuspec:

<?xml version="1.0"?>
<package>
  <metadata>
    <id>mybuildtask</id>
    <title>MyBuildTask</title>
    <version>0.0.0.1</version>
    <authors>Owner Name(s)</authors>
    <owners>Owner Name(s)</owners>
    <requireLicenseAcceptance>true</requireLicenseAcceptance>
    <description>A Description of my Package</description>
    <license type="expression">MIT</license>
  </metadata>
  <files>
    <file src="build/MyBuildTask.targets" target="build/MyBuildTask.targets" />
    <file src="bin/Release/netcoreapp3.1/MyBuildTask.dll" 
          target="tools/netcoreapp3.1/MyBuildTask.dll" />
  </files>
</package>

Each <file> element describes a file that you want to include in the package. src is path to the file relative to the directory that contains the project file. target is the location in the package that the files wil end up in. All files that are part of the console app should be placed in the tools directory to ensure they are not referenced in the project that installs this package (could cause numerous issues).

Dig into the Project File

We want this to build itself, then pack itself, but only when we are not trying to debug it. As it stands, trying to run `dotnet pack` on this will fail if the project is built in any configuration other than Release. And on top of that, having to drop to a console after using a hotkey to build is absurd when we can trigger the pack automatically.

Add these two lines to your project file (in the same propertygroup that defines your target frameworks) to automatically pack the nuget package when built in Release.

<GeneratePackageOnBuild Condition="'$(Configuration)'=='RELEASE'">true</GeneratePackageOnBuild>
<NuspecFile>MyBuildTask.nuspec</NuspecFile>

Note: Many articles reference Replace Tokens to work around the configuration issue, but I've found that .NET Core on Linux replaces $Configuration$ with an empty string. If you working with Legacy .NET applications, or if this is fixed at a later date, you may wish to utilize that feature.

Create the Targets File and Add to the Nuspec

Create a directory name `build` and in this directory, create a file named `yourprojectname.targets`. It should look something like this:

<Project>
  <PropertyGroup>
    <MyBuildTaskExe Condition="'$(OS)' == Unix">
    dotnet "$(MSBuildThisFileDirectory)../tools/netcoreapp3.1/MyBuildTask.dll"
    </MyBuildTaskExe>
    <MyBuildTaskExe Condition="'$(OS)' == 'Windows\_NT'">
    dotnet "$(MSBuildThisFileDirectory)..\\tools\\netcoreapp3.1\\MyBuildTask.dll"
    </MyBuildTaskExe>
  </PropertyGroup>

  <Target Name="MyBuildTask" AfterTargets="Build">
    <Exec Command="$(MyBuildTaskExe) &quot;$(MSBuildProjectFullPath)&quot; 
     $(TargetFramework)" IgnoreExitCode="false" />
  </Target>
</Project>
  • MyBuildTaskExe is defined based on the OS (because the path separators cause a problem otherwise). Defines command we are going to run with the ExecTask (without the arguments)
  • MSBuildThisFileDirectory is the full path to the targets file.
  • MSBuildProjectFullPath is the path to the project file and can be used to find the directory and/or files for the build. It does not wrap itself in doube-quotes (so we wrap it with ")
  • AfterTargets could be replaced with BeforeTargets and is used to determine when to run the task.

Points of Interest

Additional Resources

History

  • 9/7/2020 - Initial publication
  • 6/2/2021 - Copied from CodeProject

Did you find this article valuable?

Support Curtis Carter by becoming a sponsor. Any amount is appreciated!