Inexplicably cleared msbuild properties in TeamCity build - msbuild

I'm trying to create a "tools NuGet package" that provides a tool and setting that is unpacked during build and used by a later TeamCity build step.
The NuGet package contains the following content in its build\MyPackageId.props file:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MyTool1>$(MSBuildThisFileDirectory)..\tools\MyTool.exe</MyTool1>
</PropertyGroup>
<Target Name="ReportMyToolToTeamCity" BeforeTargets="PrepareToRun">
<PropertyGroup>
<MyTool2>$(MSBuildThisFileDirectory)..\tools\MyTool.exe</MyTool2>
</PropertyGroup>
<Message Text="MyTool1 = $(MyTool1)" />
<Message Text="MyTool2 = $(MyTool2)" />
</Target>
</Project>
(The messages will eventually set a TeamCity property, but this is sufficient to demonstrate the issue.)
Because it's a props file, after installing the NuGet package into a C# project it has added an import as the very first thing, above the import of Microsoft.Common.props. I want a props file rather than a targets file so that the property values are also available to other project settings and targets files.
When I compile this inside Visual Studio 2015, I see both MyTool1 and MyTool2 paths set to the same (correct) path as expected.
When I compile this from TeamCity (2017.2.2, using the Visual Studio (sln) runner), according to the output the MyTool1 property is empty and only MyTool2 shows the correct value.
Why?

Related

How are we supposed to execute package build targets in the new world where nuget packages are consumed through msbuild PackageReference?

I am developing a suite of UI tests using Selenium. One of the run-time dependencies of this suite is the chromedriver.exe, which we are expected to consume through the Selenium.WebDriver.ChromeDriver NuGet package.
The old world
When this NuGet package is imported the following lines are injected into the csproj file:
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Selenium.WebDriver.ChromeDriver.2.44.0\build\Selenium.WebDriver.ChromeDriver.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Selenium.WebDriver.ChromeDriver.2.44.0\build\Selenium.WebDriver.ChromeDriver.targets'))" />
</Target>
<Import Project="..\packages\Selenium.WebDriver.ChromeDriver.2.44.0\build\Selenium.WebDriver.ChromeDriver.targets" Condition="Exists('..\packages\Selenium.WebDriver.ChromeDriver.2.44.0\build\Selenium.WebDriver.ChromeDriver.targets')" />
And it is automatic by the Visual Studio. This covers our bases, making sure the build targets provided by the Selenium.WebDriver.ChromeDriver package are there at the time of the build and running them as needed. The logic inside the build targets file copies/publishes the chromedriver.exe to the right location.
All is green.
The new world.
I consume the same NuGet package as PackageReference in the csproj file. Cool. However, the build targets of that package are no longer executed. See https://github.com/NuGet/Home/issues/4013. Apparently, this is by design.
I could import the targets manually, but the problem is that I will have to hard code the location where the package is restored. It is no longer restored in the packages directory in the solution, but under my windows profile. But there is no property pointing to this location and hard coding it sucks.
So, here is the version that works for me and I hate it:
<PropertyGroup>
<MyPackagesPath>$(UserProfile)\.nuget\packages\</MyPackagesPath>
<SeleniumWebDriverChromeDriverTargets>$(MyPackagesPath)selenium.webdriver.chromedriver\2.44.0\build\Selenium.WebDriver.ChromeDriver.targets</SeleniumWebDriverChromeDriverTargets>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="2.44.0" />
</ItemGroup>
<Target Name="EnsureChromeDriver" AfterTargets="PrepareForRun">
<Error Text="chrome driver is missing!" Condition="!Exists('$(OutDir)chromedriver.exe')" />
</Target>
<Import Project="$(SeleniumWebDriverChromeDriverTargets)" Condition="Exists('$(SeleniumWebDriverChromeDriverTargets)') And '$(ExcludeRestorePackageImports)' == 'true'" />
Overall, the Sdk style projects are absolutely great, but this whole business of running targets from the packages is totally broken, even if it is by design.
What am I missing?
EDIT 1
So, here is the content of the generated obj\UITests.csproj.nuget.g.targets:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)selenium.webdriver.chromedriver\2.44.0\build\Selenium.WebDriver.ChromeDriver.targets" Condition="Exists('$(NuGetPackageRoot)selenium.webdriver.chromedriver\2.44.0\build\Selenium.WebDriver.ChromeDriver.targets')" />
</ImportGroup>
</Project>
Notice the ImportGroup condition is '$(ExcludeRestorePackageImports)' != 'true'. Now, this condition is always false, because ExcludeRestorePackageImports seems to be hard coded to be true in
c:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\NuGet.targets
Inspecting binary log confirms this. Plus https://github.com/NuGet/Home/issues/4013 was closed as WontFix.
Or am I still missing something?
If you are running Restore and other targets during the build, you may get unexpected results due to NuGet modifying xml files on disk or because MSBuild files imported by NuGet packages aren't imported correctly.

How to ship the stylecop.json and custom.ruleset files with a NuGet package in VS2017

At the moment we are switching from VS2015 to VS2017. One of our upgrade steps is to switch from stylecop to the new Stylecop.Analyzer package. The new Stylecop is using 2 files. The stylecop.json and the Stylecop.ruleset.
The target: I want to provide the stylecop files as a custom nuget package. But I dont know how to create the needed .csproj entries.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<CodeAnalysisRuleSet>packages\My.StyleCop.1.0.0-pre15\RuleSet\My.StyleCop.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
...
<ItemGroup>
<AdditionalFiles Include="packages\My.StyleCop.1.0.0-pre15\Config\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
</Project>
In the past, there was the possibility to use a install.ps1 script to do this stuff. But with NuGet 3. (or 4.) the install scripts are obsolete and will be ignored.
I already tried to use My.StyleCop.targets:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AdditionalFiles Include="packages\My.StyleCop.1.0.0-pre17\Config\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
</Project>
But here I have some issues, too. Since NuGet 3. (or 4.) there is no solution wide package folder and I dont know any variable or placeholder I can use here to get a absolute or relative path to my package.
You can add .props or .targets files to the build folder in your packages and they will be imported to the projects.
On the .props file, you can use the MSBuildThisFileDirectory MSBuild variable that represents the folder where that file is located.
Thanks to Paulo.
How I did it:
This is the structure of my NuGet package.
The solution is quiet easy. You need to create to files. A .props and a .targets file named like the NuGet package and place them in the build folder of your package.
In these MSBuild files you can use the $(MSBuildThisFileDirectory) variable to get the path of your NuGet package.
MSBuildThisFileDirectory = C:\Users\UserName\.nuget\packages\sig.stylecop\1.0.0-pre23\build\
My SIG.StyleCop.props file:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)\..\RuleSet\SIG.combiLink.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>
My SIG.StyleCop.targets file:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)\..\Config\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
</Project>
Cause of the structure of my package i need to navigate (..) into the Config and into the RuleSet folder.
The variable $(MSBuildThisFileDirectory) already includes the backslash at the end. It is important to omit the backslash when you reference the ruleset and the stylecop.json file:
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)..\RuleSet\SIG.combiLink.ruleset</CodeAnalysisRuleSet>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)..\Config\stylecop.json">
With the double backslash I experienced two strange problems in Visual Studio 2017:
Unit tests rebuild the code each time I start them, even without any code change
The IDE shows many StyleCop errors in the Error List window and shows red marks in the scroll bar even for rules that are explicitly disabled in the rule set.

VS 2017 new project format and external props file

Lets say I have the following project file:
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<TargetFramework>net47</TargetFramework>
</PropertyGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>
Is it possible to move TargetFramework property to external props file? When I try doing that, Visual Studio gives me a "one-way upgrade" error.
You've hit a known issue in the way that Visual Studio interprets csproj files. On command line (msbuild.exe or dotnet.exe) you can build a project with the property TargetFramework coming in from an imported file. Visual Studio, however, does not recognize this when it is imported.
See https://github.com/dotnet/project-system/issues/1358.

Use MSBuild Targets to copy files recursively from NuGet package

I have a .targets file in my NuGet package build folder which is then automatically included in the project that consumes the NuGet package.
I want this .targets file to copy some folders on post-build. The following script shows how that is done, but the output I get is wrong because %(RecursiveDir) starts at the first wild-card which I used for the version number of the package.
My question: How can I specify the version of MyPackage in the .targets file dynamically so that I can remove the first wildcard?
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="AfterBuild">
<ItemGroup>
<!-- MyPackage.* should be replaced by MyPackage.1.0.0.4534. But the version is set by NuGet.exe pack -Version -->
<FilesToCopy Include="$(SolutionDir)packages\MyPackage.*\myfolder\**\*.*"/>
</ItemGroup>
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="$(SolutionDir)bin\$(Configuration)\myfolder\%(RecursiveDir)"/>
</Target>
</Project>
To avoid having to the package version you could use the MSBuildThisFileDirectory property instead.
The MSBuildThisFileDirectory property gives you the directory where the .targets file is so you can change the <FilesToCopy> element to use a path relative to that directory and you do not have to use the version number.
<FilesToCopy Include="$(MSBuildThisFileDirectory)myfolder\**\*.*"/>

Unable to find MSDeploy task within MSBuild

I am trying to use the MSDeploy task within MSBuild (instead of calling it form the command line). I assumed this task was built in to MSBuild but I seem to be having trouble finding the task. The error Im getting is below. I have just re-installed the Web Deploy Tool to see if it might help.
C:\CLIENTS\DAM\Components\Umbraco\SiteTemplate_v6_1_6\Build>msbuild MSBuildScript.csproj -t:Deploy_v2
Microsoft (R) Build Engine version 4.0.30319.17929
[Microsoft .NET Framework, version 4.0.30319.18052]
<!-- some other stuff -->
error MSB4036: The "MSDeploy" task was not found. Check
the following: 1.) The name of the task in the project file is the same as the name of the task class. 2.) The task class is "public" and imple
ments the Microsoft.Build.Framework.ITask interface. 3.) The task is correctly declared with <UsingTask> in the project file, or in the *.tasks
files located in the "c:\Windows\Microsoft.NET\Framework\v4.0.30319" directory.
v10.0 can vary (v11.0 for example)
Do a search for your "Microsoft.WebApplication.targets" file and alter the import statement to match.
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="AllTargetsWrapped">
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
<!-- Bunch of Other Stuff -->
<Target Name="AllTargetsWrapped">
<CallTarget Targets="ShowVariables" />
</Target>
<Target Name="ShowVariables" >
<Message Text="MSBuildExtensionsPath = $(MSBuildExtensionsPath)" />
</Target>