Setting file properties with NuGet using .targets file

Matt W picture Matt W · Apr 12, 2018 · Viewed 12.6k times · Source

I am building a project to be installed as a NuGet package and I want to set the properties of a SpecFlow feature file (because it is latest SpecFlow and should not produce code-behind files for the features.)

To achieve the effect of selecting a file, opening it's Properties pane and setting a couple of values, I have set my project structure like this:

\MyProject
  \build
    MyProject.targets
    \Framework <the folder containing the file I want to affect>
      FrameworkTests.feature <the file I want to affect>
  \Framework
    FrameworkTests.feature <the original location of the file I want to affect>

My .nuspec like this:

<?xml version="1.0"?>
<package >
  <metadata minClientVersion="2.5">
    <id>$id$</id>
    <version>$version$</version>
    ...
  </metadata>
  <files>
    <file src="build\MyProject.targets" target="build\MyProject.targets" />
    <file src="build\FrameworkTests\FrameworkTests.feature" target="build\Framework\FrameworkTests.feature" />
  </files>
</package>

My .targets file like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <None Include="$(MSBuildThisFileDirectory)FrameworkTests\FrameworkTests.feature">
      <Link>FrameworkTests.feature</Link>
      <CopyToOutputDirectory>Copy if newer</CopyToOutputDirectory>
      <CustomToolNamespace></CustomToolNamespace>
    </None>
  </ItemGroup>
</Project>

I am not seeing the FrameworkTests.feature file get copied to the project when the package is installed. What do I need to change?

Answer

Leo Liu-MSFT picture Leo Liu-MSFT · Apr 12, 2018

I am not seeing the FrameworkTests.feature file get copied to the project when the package is installed. What do I need to change?

This is the default behavior of the nuget folder conventions. If you set the file in the content folder, nuget will copy those contents to the project root, then you will see them in the solution explorer. If you set files in the build folder, nuget will automatically inserted them into the project file or project.lock.json, like following script in the project file:

<Import Project="..\packages\MyProject.1.0.0\build\MyProject.targets" Condition="Exists('..\packages\MyProject.1.0.0\build\MyProject.targets')" />

So, this is the reason why you could not see the file FrameworkTests.feature in the solution explorer unless you change the folder to content.

You can refer to the document Creating the .nuspec file for more details:

NuGet folder conventions

Besides, if you change the folder to content, the .targets would not work. Because when you change the folder to content, in the .targetsfile, you need to change the path from:

<None Include="$(MSBuildThisFileDirectory)FrameworkTests\FrameworkTests.feature">

To:

<None Include="$(MSBuildThisFileDirectory)..\content\FrameworkTests\FrameworkTests.feature">

But MSBuild could not parse the path ..\content. To resolve this issue, we need to change the .targets file to:

<None Include="$(ProjectDir)FrameworkTests.feature">

Alternatively, you can change property of files with Install.ps1 file, set it into the nuget package.

See this thread for more details.

Update:

I have also applied the changes you've described, but still cannot get the CopyToOutputDirectory file property to be updated.

Found it. There are two points you need update and one point should to know.

First point:

I found following script in your .nuspec:

  <files>
    <file src="build\MyProject.targets" target="build\MyProject.targets" />
    <file src="build\FrameworkTests\FrameworkTests.feature" target="build\Framework\FrameworkTests.feature" />
  </files>

Note: You set your target folder for build\Framework but in your .targets file, you including it with FrameworkTests;

<None Include="$(MSBuildThisFileDirectory)FrameworkTests\FrameworkTests.feature">

So, when change it to content folder, your .nuspec file should be:

  <files>
    <file src="build\MyProject.targets" target="build\MyProject.targets" />
    <file src="content\FrameworkTests\FrameworkTests.feature" target="content\FrameworkTests\FrameworkTests.feature" />
  </files>

And the .targets file should be:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup>
    <None Include="$(ProjectDir)FrameworkTests\FrameworkTests.feature">
      <Link>FrameworkTests.feature</Link>
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CustomToolNamespace></CustomToolNamespace>
    </None>
  </ItemGroup>
</Project>

Second point need update:

The value of CopyToOutputDirectory in the .targets file should be PreserveNewest Not Copy if newer.

The point you need to know:

When you use this method to change the property of the FrameworkTests.feature, the property of this file would not changed in the solution explorer, however, MSBuild will apply this change when you build the project. That because <Import Project="....\MyProject.targets" Condition="Exists('...')" /> will be parsed when you compile the project.

So, you can check the output for CopyToOutputDirectory file property after build the project(After build your project, the file FrameworkTests.feature would be copied to the output folder.)

Update2:

A lot of comments circulating that PowerShell scripts can't be run on build servers because the install command doesn't run them; Only VisualStudio will

Not sure of all the reasons why PowerShell scripts can't be run on build servers, but the vast majority are because PowerShell's default security level.

Try typing this in the PowerShell:

set-executionpolicy remotesigned

In the Visual Studio 2015, you need to install the extension PowerShell Tools for Visual Studio 2015 and the Windows Management Framework 4.0. After install them, the install.ps1 would works fine. Following is my .nuspec file and install.ps1 script.

.nuspec file:

  <files>
    <file src="content\FrameworkTests\FrameworkTests.feature" target="content\FrameworkTests\FrameworkTests.feature" />
    <file src="scripts\Install.ps1" target="tools\Install.ps1" />
  </files>

Note: Do not forget remove the MyProject.targets in the .nuspec file.

install.ps`1 script:

param($installPath, $toolsPath, $package, $project)

function MarkDirectoryAsCopyToOutputRecursive($item)
{
    $item.ProjectItems | ForEach-Object { MarkFileASCopyToOutputDirectory($_) }
}

function MarkFileASCopyToOutputDirectory($item)
{
    Try
    {
        Write-Host Try set $item.Name
        $item.Properties.Item("CopyToOutputDirectory").Value = 2
    }
    Catch
    {
        Write-Host RecurseOn $item.Name
        MarkDirectoryAsCopyToOutputRecursive($item)
    }
}

#Now mark everything in the a directory as "Copy to newer"
MarkDirectoryAsCopyToOutputRecursive($project.ProjectItems.Item("FrameworkTests"))

Then, after install the nuget package, the property of the file FrameworkTests.feature would be changed to copy if newer:

enter image description here

Hope this helps.