3
votes

In order to emulate the "PerProject" option in TFS 2013's XAML build in the new Build 2015 task based builds, I'd like to be able to pass the SolutionName to the msbuild commandline arguments without having to manually set it every time.

I'd like to do something like:

/p:OutputPath=$(Build.BinariesDirectory)\$(SolutionName)\

Where I'd like MsBuild to infer the $(SolutionName) parameter. But when passing this on the commandline, the new task runner will substitute the $(Build.BinariesDirectory) with the correct target path and leaves $(SolutionName) alone. Unfortunately MsBuild subsequently also leaves the property alone:

Copying file from "obj\Debug\TFSBuild.exe" to "bin\debug\$(SolutionName)\TFSBuild.exe".
TFSBuild -> b\$(SolutionName)\TFSBuild.exe
Copying file from "obj\Debug\TFSBuild.pdb" to "b\$(SolutionName)\TFSBuild.pdb".

I can't remember a way to pass a property to the commandline and have it do late-expansion... Any tips?

For those looking to emulate SingleFolder or AsConfigured, those are easy:

SingleFolder -> /p:OutputPath="$(Build.BinariesDirectory)"
Asconfigured -> don't pass OutputPath
PerProject   -> /p:OutputPath="$(Build.BinariesDirectory)\HARDCODESOLUTIONNAME"
4
How about passing /p:MyOutputPathBaseDir=$(Build.BinariesDirectory) and then in the project file set the OutputPath property to $(MyOutputPathBaseDir)\$(SolutionName)? - stijn
That would require me to edit all project files, which works, but then I might as well insert my own variable and use AsConfigured - jessehouwing
That would require me to edit all project files not necessarily, msbuild has multiple extension points where it can import arbitrary files you pass it on the commandline. In your case, that file would just set the property as I described. - stijn
How would I import an extra targets file from the command line? That would help a lot - jessehouwing

4 Answers

3
votes

As I feared there doesn't seem to be a simple way to override a property from the commandline and "inject" the value of another property into it during the evaluation stage.

There are a few ways to get around it, but they're not ideal and certainly not universal for each language supported by MsBuild. A pity.

I've debugged the MsBuild targets files and found a solution to reproduce the old behaviour from the 2005/2008 era. Not entirely per solution, but it does redirect projects into a subfolder.

 /p:GenerateProjectSpecificOutputFolder=true /p:OutDirWasSpecified=true
 /p:OutputPath=$(Build.BinariesDirectory)
1
votes

One solution is to mimic such 'late evaluation' yourself by altering OutputPath withing the projectfile. To do without manually changing each single project file you can use the CustomBeforeMicrosoftCSharpTargets extension point. Which is an fancy way of saying it is just a property which when found and pointing to an existing file, will lead that file to be imported somewhere before all the actual build logic. Here's the idea: create a file like paths.targets somewhere - either include it in source control or you can generate it on the fly as part of the build process. Contents:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <OutputPath Condition="'$(OutputPathBaseDir)'!=''">$(OutputPathBaseDir)\$(SolutionName)</OutputPath>
  </PropertyGroup>
</Project>

So this just overrides OutputPath to some base dir + solutionname. Then if you build the solution like

msbuild my.sln /p:CustomBeforeMicrosoftCSharpTargets=paths.targets;
                  OutputPathBaseDir=$(Build.BinariesDirectory)

each project will import the paths.targets file and set output property to valueOfBinariesDirectory\my which I think is exactly what you are after.

1
votes

Normally, $(SolutionName) is defined when executing solution-level MSBuild pipelines, such as running dotnet restore in the root solution directory.

To make $(SolutionName) available for project-level MSBuild pipelines, add a Directory.Build.props file in the root of your solution with the following contents:

<Project>
    <PropertyGroup>
        <SolutionName Condition="'$(SolutionName)' == ''">
            $([System.IO.Path]::GetFileNameWithoutExtension($([System.IO.Directory]::GetFiles("$(MSBuildThisFileDirectory)", "*.sln")[0])))
        </SolutionName>
    </PropertyGroup>
</Project>

Now $(SolutionName) will be defined even when executing project-level MSBuild pipelines.

This answer works best when there is exactly one solution file in the root of the solution directory. You'll need to massage the above a bit for other project structures.

Of course, you can also be lazy and specify the solution name directly, but this opens up the possibility of refactoring issues (need to remember to update this file if the solution name changes).

<Project>
    <PropertyGroup>
        <SolutionName Condition="'$(SolutionName)' == ''">
            MySolutionName
        </SolutionName>
    </PropertyGroup>
</Project>
0
votes

You are right that TFS vNext build can't recognize $(SolutionName) in OutputPath, as $(SolutionName) doesn't list in the Predefined variables.

As an alternative, we may name the build definition with the solution name, then specify the MSBuild argument to: /p:OutputPath="$(Build.BinariesDirectory)\$(Build.DefinitionName)"in this way, we can get the output under the solution name.