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.
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.
- 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/
totasks/<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" />
- replace the use of
CopyLocalLockFileAssemblies
andEnableDynamicLoading
withGenerateDependencyFile
- 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:and adding in a new target specifically for the deps.json file(s):<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>
<Target Name="AddBuildDependencyFileToBuiltProjectOutputGroupOutput" BeforeTargets="BuiltProjectOutputGroup" Condition=" '$(GenerateDependencyFile)' == 'true'"> <ItemGroup> <BuiltProjectOutputGroupOutput Include="$(ProjectDepsFilePath)" TargetPath="$(ProjectDepsFileName)" FinalOutputPath="$(ProjectDepsFilePath)" /> </ItemGroup> </Target>
- This means removing the