Most of the TFS2015 build tasks allow you to specify an expression of one or more files from source control on which the task should operate, such as the “Visual Studio Build” task’s Solution parameter. However, even though most tasks give a quick example such as “**\.sln
” or “**\*test*.dll;-:**\obj\**
“, there doesn’t seem to be good reference documentation explaining what exactly is allowed. Also, the VSBuild task has a quirk that most of the other tasks don’t have, which makes things even more confusing.
So let me share what I’ve discovered so far.
I’m using an on-premise installation of TFS2015 Update 1, although I expect this to work in the same way in Visual Studio Online (VSO).
Simple expressions
If the path expression doesn’t contain one of the wildcard characters ‘*’ or ‘?’, it is considered a simple expression that refers to a single file. This expression can be:
An absolute path in source control notation:
$/HelloWorld/HelloWorldApp/HelloWorldApp.sln
A relative path in filesysyem notation (relative w.r.t. the $(build.sourcesDirectory)
, see the Repository/Mappings of your build):
.\HelloWorldApp\HelloWorldApp.sln
The expression can contain placeholders like $(build.sourcesDirectory)
– these are replaced before they’re passed to the build task. Note that simple expressions do not support specifying multiple paths separated by ‘;’ – if you want this, you should include at least one ‘?’ so that it becomes a wildcard expression, which we’ll cover next.
Wildcard expressions
If the path expression contains at least one of the wildcard characters ‘*
‘ or ‘?
‘, the expression can contain:
A single arbitrary character substitution:
\HelloWorldApp\bin\ContractV?.dll
An arbitrary child directory (i.e. one level deep):
*\HelloWorldApp.sln
The current or an arbitrary subdirectory (i.e. 0-n levels deep):
**\HelloWorldApp.sln
All .sln files directly or indirectly below “Contracts”
Contracts\**\*.sln
A combined expression consisting of multiple path expressions separated by a semicolon (‘;
‘) – some constrains apply for VSBuild, see below!
**\*.dll;**\*.pdb
Inclusion (which is the default, or explicitly prefixed by “+:
“) and exclusion (prefixed by “-:
“) expressions, e.g. all solution files except Dummy.sln:
**\*.sln;-:**\Dummy.sln
All exclusion expressions always take precedence over the inclusion expressions, so if a file matches both an inclusion and an exclusion expression, the file is not included. If the path expression results in more than one matching file, the files’ paths are sorted and processed in that order.
The Visual Studio Build’s Solution parameter
The Solution parameter of the “Visual Studio Build” task works a bit differently, in the sense that it is constrained by the following rules:
– Only the first part of a combined expression is relative to the sources directory (the subsequent parts are relative to some undefined directory)
– If there are inclusions after the first part of a combined expression, these cannot start with a wildcard character. This does not apply to exclusions.
I.e. the following would work:
.\HelloWorldApp\*.sln;-:**\Dummy.sln
But this would not:
.\HelloWorldApp\*.sln;**\Dummy.sln (subsequent inclusions may not start with a wildcard) .\HelloWorldApp\*.sln;-:.\HelloWorldApp\Dummy.sln (subsequent parts are not relative to the sources directory)
There is a way to work around these limitations however, and that is to start every inclusion part beyond the first with the “$(build.sourcesDirectory)
” placeholder, so that it doesn’t start with a wildcard and is explicitly made relative to the sources directory again. In other words, to make the last 2 examples work:
.\HelloWorldApp\*.sln;$(build.sourcesDirectory)\**\Dummy.sln .\HelloWorldApp\*.sln;-:$(build.sourcesDirectory)\HelloWorldApp\Dummy.sln
Technical details
The reason why this works the way it does: Most of the build tasks (including the VSBuild task) that need solution files use the Get-SolutionFiles
function (see /Tasks/VSBuild/LegacyHelpers.ps1
), which only delegates the call to Find-Files
if the path expression contains a ‘*’ or ‘?’:
# check for solution pattern if ($Solution.Contains("*") -or $Solution.Contains("?")) { Write-Verbose "Pattern found in solution parameter." Write-Verbose "Find-Files -SearchPattern $Solution" $solutionFiles = Find-Files -SearchPattern $Solution Write-Verbose "solutionFiles = $solutionFiles" } else { Write-Verbose "No Pattern found in solution parameter." $solutionFiles = ,$Solution }
The Find-Files
cmdlet is a TFS2015-specific Cmdlet (implemented in Microsoft.TeamFoundation.DistributedTask.Task.Common.dll
, which can be found on any build agent), which is capable of prepending the same root folder to each part of combined expression, but only if that root folder is specified as a separate -RootFolder
parameter.
However, as you can see Get-SolutionFiles
doesn’t use this -RootFolder
parameter. Instead, the working folder is directly prepended to the path expression so that it only applies to the first part of the combined expression. This can be seen in the verbose trace of a build, where the path expression “**\HelloWorldApp.sln;-:**\Dummy.sln
” is logged as:
##[debug]Solution = C:\<agent folder>\_work\1\s\**\HelloWorldApp.sln;-:**\Dummy.sln
Hence, because of this the subsequent inclusion parts are relative to some unknown directory, unless you use “$(build.sourcesDirectory)
” to compensate for this.
Conclusion
The wildcard format is relatively straightforward – well, at least once you know where the quirks are 🙂
7 comments
OMG, Thank you! I’m using VSTS and searched everywhere for this information trying to figure out inclusions. Everything I tried was failing due to the second part starting with a wildcard.
Amy
Thanks for your response, it’s nice to know that this information was helpful to others 🙂
Léon Bouquiet
This is some great info! But does things work the same way in the Test Assembily field of the Visual Studio Test (https://www.visualstudio.com/en-us/docs/build/steps/test/visual-studio-test)?
Individually each of these work but when placed together with a semicolon I get the error: The given path’s format is not supported.
$(build.sourcesDirectory)v7.25_Core**MySystems.Xmts.Testbin**MySystems.Xmts.Test.dll; $(build.sourcesDirectory)v7.25_Core**MySystems.Xmts.WinForms.Testbin**MySystems.Xmts.WinForms.Test.dll
Any ideas? Driving me nuts!
Thanks.
Chris Lee
Thanks; As far as I can tell your path expression should work. A couple of things you could try:
– Try removing the space between ; and $, as Find-Files can be quite picky about these things.
– Maybe $(build.sourcesDirectory) is the culprit, try substituting it with the actual path and see if that changes things.
– Last resort: Have a look at the FindFilesCmdlet inside Microsoft.TeamFoundation.DistributedTask.Task.Common.dll with something like ILSpy, and figure out how it handles your input. I couldn’t find anything strange (but I only looked at it for a couple of minutes).
–Edit, apparently this is a known issue, see Chris Lee’s comment below.
Léon Bouquiet
The problem is a known issue and fixed in TFS 2015 Update 3.
“VsTest task fails if full path of 2 DLLs are given separated by semicolon.”
Chris Lee
I’m trying to ignore a certain sub path when building a .net core asp.net project. Using the above reference I arrived at: “Portal/API/**/*.csproj;-:Portal/API/Services/IdentityServer/**/*.csproj” but this does not work. If I just enter Portal/API/**/*.csproj it works and build everything, but as soon as I try to exclude a certain subpath with Portal/API/Services/IdentityServer/**/*.csproj vsts says no projects were found, so its basically removing everything. How do I include everything except what is in the identity server folder?
JJ
Thanks!
Sergei Meleshchuk