Home: Producing an MSBuild task package

Created on 19 Apr 2017  ·  18Comments  ·  Source: NuGet/Home

_From @natemcmaster on April 14, 2017 18:41_

Can we get better support for projects designed to produce MSBuild tasks?

Pain points

  • packing. You _can_ get a task assembly and files into the package, but requires deep understanding of how to alter the default layout of the package in csproj.

    • TargetFrameworks. A package that only has task assemblies/targets is compiled for net46 and netcoreapp1.0, but the _package_ itself can be compatible with any project, regardless of its TFM.

  • task references. When loading a task assembly, MSBuild does not use the NuGet cache to find dependencies. This means we have to package assemblies to sit side-by-side on disk so task loading works.
  • tasks with native dependencies. Never been able to make this work.

Workarounds
Task assembly projects end up looking like this

  <PropertyGroup>
    <!--
      The netstandard1.0 and net45 TFMs don't actually compile tasks.
      Only here so the generated nuspec includes the TFM so it appears as "compatible" on NuGet.org
      and in VS NuGet GUI.

      netstandard1.6 => loaded on dotnet.exe
      net46 => loaded on MSBuild.exe
    -->
    <TargetFrameworks>netstandard1.6;net46;netstandard1.0;net45</TargetFrameworks>
    <!-- Be quiet NuGet. I don't want assemblies in lib/ and yes I'm sure this is right. -->
    <NoPackageAnalysis>true</NoPackageAnalysis>
    <!-- forces SDK to copy dependencies into build output to make packing easier -->
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
  </PropertyGroup>

  <ItemGroup>
    <!-- ensure nuspec doesn't contain any package references as we bundle their assemblies in our package -->
    <PackageReference Update="@(PackageReference)" PrivateAssets="All" />
  </ItemGroup>

  <!-- 
    Copy dependencies into the tools/$(TargetFramework)/ package folders.
    MSBuild does not resolve task runtime dependencies from PackageReference
  -->
  <Target Name="PackTaskDependencies" BeforeTargets="GenerateNuspec">
    <!--
    The include needs to happen after output has been copied to build output folder
    but before NuGet generates a nuspec.
    -->
    <ItemGroup>
      <_PackageFiles Include="bin\$(Configuration)\*\Newtonsoft.Json.dll;bin\$(Configuration)\*\NUglify.dll">
        <PackagePath>tools\%(RecursiveDir)</PackagePath>
        <Visible>false</Visible>
        <BuildAction>Content</BuildAction>
      </_PackageFiles>
    </ItemGroup>
  </Target>

Some MSBuild task projects in the wild:

madskristensen/BundlerMinifier
aspnet/BuildTools
natemcmaster/Yarn.MSBuild

_Copied from original issue: dotnet/sdk#1125_

Pack Icebox 2 DCR

Most helpful comment

This should be part of NuGet package analysis actually. /cc @emgarten

All 18 comments

cc @emgarten @rohit21agrawal

_From @rohit21agrawal on April 14, 2017 21:27_

we already added some extension points that should make this easier : https://github.com/NuGet/NuGet.Client/pull/1255

We can't make a generic Task package type since all packages would differ widely. Do you have any concrete suggestions on what you would like to see that would make it easier for you to create task packages?

_From @natemcmaster on April 14, 2017 21:59_

In a world of unlimited resources, I'd love to see this:

<Project Sdk="Microsoft.Build.Task">
</Project>

Microsoft.NET.Sdk is geared towards runtime packages, not MSBuild task packages. Packages designed to carry MSBuild tasks are fundamentally different from packages that only carry runtime bits.

_From @natemcmaster on April 14, 2017 22:18_

@rohit21agrawal if you're looking for short-term fixes to the pack task...

One of the problems today is that package authors have to choose between nuspec and csproj=>nuspec generation. Keeping csproj and nuspec aligned is difficult, esp. for users new to MSbuild. Wrangling MSBuild to make csproj=>nuspec generation work right is difficult, even for advanced MSBuild users. I had to read NuGet's source code to figure some of this out.

Anything you can do to make nuspec + csproj easier would help with this ask.

One idea:
Make all msbuild properties available in nuspec. Currently you have to manually marshal these via <NuspecProperties>

<package>
  <metadata>
    <id>${MSBuild.PackageId}</id>
    <version>${MSBuild.PackageVersion}</version>
  </metadata>
</package>

_From @rohit21agrawal on April 16, 2017 19:19_

@natemcmaster instead of making csproj + nuspec easier, i'd like to understand more about what we can do to make the need for nuspec go away.
what are some of the things that you can't achieve via csproj currently and for which you absolutely do need the nuspec?

_From @natemcmaster on April 17, 2017 18:14_

@rohit21agrawal

  • Packing the output of multiple projects. Example: Microsoft.EntityFrameworkCore.Tools.nuspec
  • "Fat packages". i.e. I want a nupkg with the output of "dotnet publish". Example: NuGetPackageVerifier.nuspec
  • <summary>. cref https://github.com/NuGet/Home/issues/4587.
  • Explicitly defining the <dependencies> section.

    • Example: I want my MSBuild task package to say <dependencies><group targetFramework="Any"></group></dependencies>, but nuspec generation always uses <TargetFrameworks> which is set to the target frameworks for which the task assembly compiles. In this case, <TargetFrameworks> does not accurately represent which frameworks the _package_ is compatible with.

_From @conniey on April 18, 2017 20:17_

I think this issue overlaps with: https://github.com/Microsoft/msbuild/issues/1756

Given that this conversation is happening around pack, I am moving this issue over to NuGet. Please, re-activate or file new issues for specific asks for the SDK to support this.

Here's another example of this with nasty workarounds:
https://github.com/paulcbetts/refit/blob/04db09ffb67ba500ee2bc3370bc68b81437b46a0/Refit/Refit.csproj#L28-L46

We need to call dotnet publish on the task package we want to include in our library package (we do build-time code generation). It leads to build-time weirdness and is far too hard to figure out. One of the issues is ensuring that the dependent publish is only called once, so that's why it's in a tfm condition. Was all trial-and-error to figure out.

@onovotny I'm trying to do something similar. Could you please explain how you ended up with build\MSBuild<TargetFramework> being the destination folder? Is this documented somewhere? Unfortunately, the way you've created the package didn't work for me at first try. Have to check later :/

There is nothing special about the directory name or location; it comes from detecting core msbuild vs full here:

https://github.com/paulcbetts/refit/blob/master/Refit/targets/refit.targets#L12-L13

We also needed a custom msbuild task base type (ContextAwareTask) to handle assembly loading in the correct directory:
https://github.com/paulcbetts/refit/tree/master/InterfaceStubGenerator.BuildTasks

Then in our main library we call publish on each TFM to get all the files we need in the publish output directory:
https://github.com/paulcbetts/refit/blob/master/Refit/Refit.csproj#L39-L50

And finally, include them in the package in the right location.

Hope this helps!

The point of all of this though is that's far too complicated and painful to create MSBuild tasks that work on both Full and Core MSBuild.

@onovotny Thanks for the explanation. It seems that my target file is not getting included. Are there any obstacles you know about that? First tried with build\net45 and build\netstandard1.4 (as in refit), then with just build\ ... but still nothing. For now I'm not using any task assembly; just a <Error Text="..." />.

Your props/targets file has to match the package id for it to be automatically included. For Refit, that'd be Refit.targets. Do yours match?

🤦‍♂️

Thanks to both of you, @onovotny and @jnm2 .

I remember tripping over the same magic early this year when I wrote my first MSBuild target and tried to package it.

This should be part of NuGet package analysis actually. /cc @emgarten

Was this page helpful?
0 / 5 - 0 ratings