Search code examples
c#.netmsbuildteamcity

dotnet test command not finding Microsoft.TextTemplating.targets


I have this line inside my csproj

<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TextTemplating\Microsoft.TextTemplating.targets" />  

But when I run dotnet test it seems to resolve MSBuildExtensionsPath with

C:\Program Files\dotnet\sdk\8.0.204\

I tried to set an environment MSBuildExtensionsPath and set it to the expected path which is

C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\

Nothing worked so far. I'm running this as TeamCity build step. Also tried to use environment variables in TeamCity.

The error which I receive is

error MSB4019: The imported project "C:\Program Files\dotnet\sdk\8.0.204\Microsoft\VisualStudio\v17.0\TextTemplating\Microsoft.TextTemplating.targets" was not found. Confirm that the expression in the Import declaration "C:\Program Files\dotnet\sdk\8.0.204\Microsoft\VisualStudio\v17.0\TextTemplating\Microsoft.TextTemplating.targets" is correct, and that the file exists on disk.


Solution

  • Build with MSBuild and Test the Built Test Assemblies

    Build with MSBuild

    From the comments a solution is being used with a mix of .Net 8, .Net Framework 4.8, and .Net Standard projects.

    The dotnet CLI can't build .Net Framework.

    To build the mixed solution at the command line (or via the Team City Agent), a "Developer Command Prompt for VS" or its equivalent, and MSBuild are needed.

    The Dev Cmd Prompt shortcut runs the VsDevCmd.bat batch file which adds to the PATH and adds some environment variables. If VsDevCmd.bat has been run, then msbuild can be found via the PATH and msbuild can find the Visual Studio install directory. $(MSBuildExtensionsPath) will be set appropriately, e.g. C:\Program Files\Microsoft Visual Studio\2022\BuildTools\MSBuild.

    The solution can be built with the command:

    msbuild mixed.sln
    

    Test the Built Test Assemblies

    The dotnet CLI embeds its own copies of several tools including MSBuild. The MSBuild embedded in dotnet will use the SDK path for $(MSBuildExtensionsPath), e.g. C:\Program Files\dotnet\sdk\8.0.204\.

    The command dotnet test mixed.sln will use the embedded MSBuild to attempt to evaluate the projects and determine if the projects need to be built. Evaluating the projects will fail because $(MSBuildExtensionsPath) will be wrong.

    However, dotnet test forwards to MSBuild for projects, solutions, directories, and when no argument is supplied. When an assembly (.dll or .exe) is the supplied argument, dotnet test forwards to the test runner and doesn't evaluate the project. In other words, run dotnet test against the built test assemblies.

    Other Approaches

    If there are circumstances that interfere with the above approach, there are other options.

    Override MSBuildExtensionsPath

    You can override MSBuildExtensionsPath, but that may have unintended side effects.

    The dotnet test command when forwarding to MSBuild, will accept MSBuild arguments. An override may look like the following:

    dotnet test mixed.sln -p:MSBuildExtensionsPath="C:\Program Files\Microsoft Visual Studio\2022\BuildTools\MSBuild"
    

    The -p option is defining a property.

    Note that projects that expect extensions from the SDK directory will break. This may include standard SDK style project support so this may not be a viable approach.

    Edit the Projects to Conditionalize the T4 Import

    An Import can have a Condition:

      <Import Project="example.targets" Condition="Exists('example.targets')" />
    

    In the Microsoft.TextTemplating.targets file, a $(T4BuildTasksAssemblyFile) property is defined which can be expected to be reasonbably unique to the file. You can test this property to determine if an Import was performed.

    As an example:

      <PropertyGroup>
        <T4TargetsFile>Microsoft.TextTemplating.targets</T4TargetsFile>
        <T4StdPath>$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TextTemplating\$(T4TargetsFile)</T4StdPath>
        <T4AltPath>C:\Program Files\Microsoft Visual Studio\2022\BuildTools\Msbuild\Microsoft\VisualStudio\v17.0\TextTemplating\$(T4TargetsFile)</T4AltPath>
      </PropertyGroup>
    
      <Import Project="$(T4StdPath)" Condition="Exists('$(T4StdPath)')" />
      <Import Project="$(T4AltPath)" Condition="'$(T4BuildTasksAssemblyFile)' == ''" />
    

    If the Import of $(T4StdPath) was performed, then $(T4BuildTasksAssemblyFile) willl not be empty and the Import of $(T4AltPath) will not be done.

    If $(T4StdPath) doesn't exist, then $(T4BuildTasksAssemblyFile) will be empty and the Import of $(T4AltPath) will be attempted. If $(T4AltPath) also doesn't exist, then there will be an MSB4019 error.

    The pattern can be extended to test three or more paths:

      <Import Project="$(T4Path0)" Condition="Exists('$(T4Path0)') and '$(T4BuildTasksAssemblyFile)' == ''" />
      <Import Project="$(T4Path1)" Condition="Exists('$(T4Path1)') and '$(T4BuildTasksAssemblyFile)' == ''" />
      <Import Project="$(T4Path2)" Condition="'$(T4BuildTasksAssemblyFile)' == ''" />
    

    Instead of maintaining a copy of this MSBuild code in every project that uses Microsoft.TextTemplating.targets, place this conditional logic code in its own .targets file and change the Import of Microsoft.TextTemplating.targets to an Import of the conditional logic file.