Skip to content

Instantly share code, notes, and snippets.

@baronfel
Last active August 3, 2022 19:23
Show Gist options
  • Save baronfel/bac76982414d0e0611aa902b2fbd3cda to your computer and use it in GitHub Desktop.
Save baronfel/bac76982414d0e0611aa902b2fbd3cda to your computer and use it in GitHub Desktop.
changes made to seamlessly move to multitargeting

Multitargeting your MSBuild tasks and targets

Why would you multitarget?

For all practical purposes, you must multitarget. There is a huge chunk of the MSBuild ecosystem that's stuck on the .NET Framework build of MSBuild - everyone using tooling inside Visual Studio meets this criteria. If you don't support both Full and Core MSBuild distributions you're artificially cutting out your user base.

What does multitargeting mean?

For 'normal' .NET SDK projects, multitargeting means setting multiple TargetFrameworks in your project file. When you do this, builds will be triggered for both TFM, and the overall results can be packaged as a single artifact.

That's not entirely what we mean for MSBuild. MSBuild has two primary shipping vehicles: Visual Studio and the .NET SDK. These are wildly different runtime environments - one runs on the .NET Framework runtime, and other runs on the CoreCLR. What this means is that while your code can target netstandard2.0, your task logic may have differences based on what MSBuild Runtime Type is currently in use. Practically, since there are so many new APIs in .NET 5.0 and up, it makes sense to both multitarget-TFM your MSBuild task source code as well as multitarget-RuntimeType your MSBuild target logic.

Concrete changes specifically required to multitarget

  • Change your project file to use the net472 and net6.0 TFMs (the latter may change based on which SDK level you want to target - right now you may want to target netcoreapp3.1 right now until netcoreapp3.1 goes out of support in December). When you do this, the package folder structure changes from tasks/ to tasks/<TFM>/.
<TargetFrameworks>net472;net6.0</TargetFrameworks>
  • Update your .targets files to use the correct TFM to load your tasks. The TFM required will change based on what .NET TFM you chose above, but for a project targeting net472 and net6.0, you would have a property like:
<_Ionide_KeepAChangelog_Tasks_TFM Condition=" '$(MSBuildRuntimeType)' != 'Core' ">net472</_Ionide_KeepAChangelog_Tasks_TFM>
<_Ionide_KeepAChangelog_Tasks_TFM Condition=" '$(MSBuildRuntimeType)' == 'Core' ">net6.0</_Ionide_KeepAChangelog_Tasks_TFM>

Note here that we're using the MSBuildRuntimeType as a proxy for which hosting environment we're in. Once this property is set, you can use it in the UsingTask to load the correct AssemblyFile:

<UsingTask
    AssemblyFile="$(MSBuildThisFileDirectory)../tasks/$(_Ionide_KeepAChangelog_Tasks_TFM)/Ionide.KeepAChangelog.Tasks.dll"
    TaskName="Ionide.KeepAChangelog.Tasks.ParseChangeLogs" />

Changes unrelated to multitargeting that we should make to the existing documentation

  • replace the use of CopyLocalLockFileAssemblies and EnableDynamicLoading with GenerateDependencyFile
  • make the packing of the generated .deps.json files less hardcoded, so that we get the correct deps.json files from each TFM's build
    • This means removing the .deps.json line from this target:
       <Target Name="CopyProjectReferencesToPackage" DependsOnTargets="ResolveReferences">
          <ItemGroup>
          <BuildOutputInPackage Include="$(OutputPath)/*.deps.json" />
          <!-- the dependencies of your MSBuild task must be packaged inside the package, they cannot be expressed as normal PackageReferences -->
          <BuildOutputInPackage Include="@(ReferenceCopyLocalPaths)" TargetPath="%(ReferenceCopyLocalPaths.DestinationSubPath)" />
          </ItemGroup>
      </Target>
      and adding in a new target specifically for the deps.json file(s):
      <Target
        Name="AddBuildDependencyFileToBuiltProjectOutputGroupOutput"
        BeforeTargets="BuiltProjectOutputGroup"
        Condition=" '$(GenerateDependencyFile)' == 'true'">
      
          <ItemGroup>
          <BuiltProjectOutputGroupOutput
              Include="$(ProjectDepsFilePath)"
              TargetPath="$(ProjectDepsFileName)"
              FinalOutputPath="$(ProjectDepsFilePath)" />
          </ItemGroup>
      </Target>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment