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.
- Click the Use This Template button
- 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) "$(MSBuildProjectFullPath)"
$(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 theExecTask
(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 withBeforeTargets
and is used to determine when to run the task.
Points of Interest
- This template can be used to run existing commands as well by modifying the Targets File to run a command that should exist on the machine (not recommended for public packages)
- Emitting Warnings/Errors to MSBuild is as simple as logging to the console with the correct pattern
Additional Resources
- NuGetDefense
- The template was created from the source for this package.
- MSBuild ExecTask
- Nate McMaster's articles on the subject
- Nuspec File Reference
- Targets File Documentation
- MSBuild Properties
History
- 9/7/2020 - Initial publication
- 6/2/2021 - Copied from CodeProject