Search code examples
tfsmsbuildmsbuild-buildengine

Can you use multiple working folders with TFS?


In projects in which the workspace has only one working folder, my build scripts work great. Now that I am working with a new project that required 2 working folders, all of the checkout and checkin commands of my previous script fail, with no files found.

Obviously, I'm not understanding a critical part of the implementation of the workspace here... I have a project which is dependant on other projects, the second working folder is basically a 3rd party folder with references to the various published DLL's and headers files needed to compile my project. There are 2 active folders and the local folders are:

$(SourceDir)\TEAM-MAIN\Address Finalizer
$(SourceDir)\TEAM-MAIN\HH-CAHPS Project\MAINLINE\3rd Party

The built code works fine, but the custom AfterGet fails on the following entry:

<!-- Check out all of the assemblyInfo files -->
<Exec Command="$(TfCommand) checkout AssemblyInfo.cs /recursive"
      WorkingDirectory="$(MSBuildProjectDirectory)\..\sources"
      ContinueOnError="false"/>

The project will of course work if I have a single working folder and move the source to a high enough point to get all the needed files, but I don't want to troll through 43 other projects todo what I want, let along mucking with their assembly files...

I have also tried :

<!-- Check out all of the assemblyInfo files -->
<Exec Command="$(TfCommand) checkout AssemblyInfo.cs /recursive"
      WorkingDirectory="$(SolutionRoot)"
      ContinueOnError="false"/>

Same problem, unable to find any assembly files... I have checked build log and I definately see assembly files check out during the build phase...

Task "Get"
  Get TeamFoundationServerUrl="http://pgpd-team01:8080/" BuildUri="vstfs:///Build/Build/1430" Force=True Overwrite=False PopulateOutput=False Preview=False Recursive=True Version="C7564" Workspace="SBN01P-TFS03_61"
<snip>
  Getting C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\Sources\Address Finalizer\Address Finalizer\Properties\AssemblyInfo.cs;C7525.

If anyone has any ideas or can point me to some article to better explain how mutliple working folders work, I'd appreciate it.

Values for some of the build variables:

MSBuildProjectDirectory: C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\BuildType

SolutionRoot: C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\Sources

To provide more information, I added the following command:

    <!-- Report what our working folders are -->
    <Exec 
      Command='$(TfCommand) workfold'
      WorkingDirectory="$(SolutionRoot)\TEAM-MAIN\Address Finalizer"/>

The result was:

Task "Exec"
  Command:
  "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\..\tf.exe" workfold
  ===============================================================================
  Workspace: SBN01P-TFS03_61 (tfsservice)
  Server   : http://pgpd-team01:8080/
   $/InfoTurn/TEAM-MAIN/Address Finalizer: C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\Sources\TEAM-MAIN\Address Finalizer
   $/InfoTurn/TEAM-MAIN/HH-CAHPS Project/MAINLINE/3rd Party: C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\Sources\TEAM-MAIN\HH-CAHPS Project\MAINLINE\3rd Party

I have found that the following working directory will work:

WorkingDirectory="$(SolutionRoot)\TEAM-MAIN\Address Finalizer"

But that the following two do not, note that the 2nd is my 2nd working folder:

WorkingDirectory="$(SolutionRoot)"
WorkingDirectory="$(SolutionRoot)\TEAM-MAIN\HH-CAHPS Project\MAINLINE\3rd Party"

The error that I get for the label task is the most useful:

Using "Label" task from assembly "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\Microsoft.TeamFoundation.Build.Tasks.VersionControl.dll".
Task "Label"
  Label TeamFoundationServerUrl="http://pgpd-team01:8080/" BuildUri="vstfs:///Build/Build/1507" Name="Address Finalizer 2.0.1 Build 039" Recursive=True Comments="Automated build: Address Finalizer 2.0.1 Build 039" Version="W" Child="replace" Files="C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\Sources"
C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\BuildType\TFSBuild.proj(310,5,310,5): error : Error: Unable to determine the workspace.

The actual error from the check out, which is not useful, is:

Task "Exec"
  Command:
  "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\..\tf.exe" checkout AssemblyInfo.cs /recursive
  No matching items found in C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\Sources\AssemblyInfo.cs in your workspace.
C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\BuildType\TFSBuild.proj(280,5): error MSB3073: The command ""C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\..\tf.exe" checkout AssemblyInfo.cs /recursive" exited with code 1.

Solution

  • It is possible to use disjoint workspace mappings + recursion at the same time. You just have to be careful.

    This gets all C# files in Foo & Bar:

    $/project/dir1/foo -> c:\code\foo
    $/project/dir2/bar -> c:\code\bar
    
    tf get c:\code\*.cs -recursive
    

    This usually gets everything under Foo & Bar, but will fail if the working directory cannot be resolved to a workspace unambiguously:

    $/project/foo -> c:\code1\foo
    $/project/bar -> c:\code2\bar
    
    tf get $/project/* -recursive
    

    This might work (same caveat as above), but it probably doesn't do what you expect!

    $/project/foo -> c:\code1\foo
    $/project/bar -> c:\code2\bar
    
    tf get c:\code* -recursive
    

    There are more variations; the behavior depends on how your workspace is defined, as well as the presence of other workspaces on the same machine with similar local paths. Can you spell out your build definition exactly? And can you add some debugging statements to verify exactly what $(SolutionRoot) and other MSBuild properties are equal to when your target is being executed?

    / EDIT /

    As I said, disjoint mappings + recursion is very tricky to get right. Turns out that behavior varies not only with the subtle details of your filespec vs the workspace definition, but also varies between commands! Unlike my first example, this fails:

    $/project/dir1/foo -> c:\code\foo
    $/project/dir2/bar -> c:\code\bar
    
    tf checkout c:\code\*.cs -recursive
    

    Confused yet? I am, and I worked on the TFS team for several years!

    Let me summarize my recommendations from the various comment threads:

    • Be super careful. Before putting anything into msbuild scripts, log onto the build machine & test the heck out of it! You'll get nicer error messages & much quicker feedback when testing interactively. Which is important, because what you're trying to do is quite fragile. Taking your scripts at face value, here are some corrections to get you started:
      • The Checkout task should use a server path for best results. Once that's fixed, you can run it from either $(SolutionRoot)\TEAM-MAIN\Address Finalizer or $(SolutionRoot)\TEAM-MAIN\HH-CAHPS Project\MAINLINE\3rd Party, makes no difference. [not plain old $(SolutionRoot), unfortunately -- as demonstrated above, Checkout is not as smart as Get] The reason it appeared to work from Address Finalizer while using a local itemspec is because you actually have some matching files under that local dir. You don't have any AssemblyInfo.cs files under 3rd Party, presumably. By contrast, using a server path such as $/InfoTurn/TEAM-MAIN/AssemblyInfo.cs will widen the scope so that TFS searches all of TEAM-MAIN for matching files to checkout. You still have to run it from a working directory that unambiguously determines a workspace -- or specify both the /server and /workspace options -- so that TFS can limit the query to items actually in the workspace and knows whom to check them out to.
      • Similarly, the Label task you've written needs to use a server path + run from a directly mapped folder. Or you could use a fully qualified versionspec (Wwsname;domain\wsowner) instead of an abbreviation (W).
    • Better still, don't use a disjoint workspace in the first place. Look at all of the local paths being mapped: their common ancestor should itself be a mapped folder. Otherwise (a) commands run from there will be ambiguous, requiring extra parameters, and/or not work at all (b) commands run from individually mapped subfolders won't query across the whole workspace [unless you use server paths]. To avoid all this, pick one mapping to be the "root", then ensure all subsequent mappings point somewhere underneath it. Once you have established a single, unified root, things get much easier. tf commands run from anywhere inside it will function much more predictably (to newbs & experts alike!). There are several ways to setup a unified workspace that meets your goals:

      • Switch back to one big mapping, then cloak out the folders you don't want. Note that you can create positive mappings under cloaks (e.g. cloak $/InfoTurn/TEAM-MAIN/HH-CAHPS Project while still mapping $/InfoTurn/TEAM-MAIN/HH-CAHPS Project/MAINLINE/3rd Party). This leaves you with a nice clean workspace and no funky side effects. Downside is the amount of work involved. If there are tons of folders you could use a script to create the cloaks, but even so there's the issue of keeping them up to date as new folders are added or existing ones are renamed.
      • Find a folder that's a parent of your current mappings, and create a one-level mapping to it. For example, you could create a one-level mapping $/InfoTurn/TEAM-MAIN/* -> $(SolutionRoot)\TEAM-MAIN. This will pull down all the immediate children of TEAM-MAIN but not recurse any deeper, except for the two folders you've already added with full recursion. Downside is having a few extra files be downloaded & labeled.
      • Change your current mappings so that one folder maps underneath the other. For example, you could map $/InfoTurn/TEAM-MAIN/Address Finalizer -> $(SolutionRoot) and $/InfoTurn/TEAM-MAIN/HH-CAHPS Project/MAINLINE/3rd Party -> $(SolutionRoot)\3rd Party. Now $(SolutionRoot) is a unified root for all the code you need. Problem with this solution is that it might break any makefiles that rely on relative paths staying constant between your build configuration and others that also try to build Address Finalizer w/o using funky mappings.
      • Move your 3rd Party folder to a more natural place in the source control structure. After all, you are probably not the only person working in TEAM-MAIN who needs to reference 3rd Party components. If you all agreed to keep shared resources at the root and craft your makefiles appropriately, then the most of the problems around mapping deep paths into a workspace go away.
    • Even better still, don't checkout/checkin files during the build process. Seriously. Common situations that are easy for humans to handle (eg locks, version conflicts) are a nightmare to automate given all the possible edge cases. And god help you if you ever try to use continuous integration... Meanwhile, your all important builds are breaking while you debug functionality that I truly don't see any benefit for. Instead:

      • Consolidate all systemwide assembly info (including build numbering) into one single AssemblyInfo.cs file.
      • Reference this C# file from a common MSBuild *.targets file that each project imports. (Good opportunity to refactor project-level tasks if you haven't done so already).
      • Have Team Build modify this C# file as needed. E.g. you could fill in the build number anytime prior to the CoreCompile task. Note that we're just overwriting things on disk, not touching source control.
      • If you want your desktop builds to have incremental numbering as well (I don't see the point, personally), add a similar task to your common *.targets file with a condition that excludes it from Team Builds.