views:

986

answers:

2

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.
+1  A: 

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.
Richard Berg
Task "Message" FOOBAR MSBuildProjectDirectory: C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\BuildTypeDone executing task "Message".Task "Message" FOOBAR SolutionRoot: C:\Users\tfsservice\AppData\Local\Temp\InfoTurn\Address Finalizer\SourcesDone executing task "Message".
Shire
The get is working fine, it is the checkout that fails, btw... I've edited the settign for my local folders to make it more visible what I'm doing, as I'm following your case #1...
Shire
Richard Berg
Add more information to the quest, per your request.The purpose of this build is to automated teh existign manual process, including generaling build #'s, labeling everything used in the build, etc... If I have only one workspace/local folder. Everything works fine, but is slow and labels all of the project, instead of just the one I am building. This is an undesriable side effect. When I change the build to have 2 workspaces, 1 for the project and 1 for its 3rd party dependencies, all of the Exec, Label and Copy tasks stop working as expected.
Shire
The current structure is that under TEAM-MAIN, we have several projects... Some of which are global projects and many which are team specific. Address Finalizer is a project used by all the teams. The HH-CAHPS is a project dealing with government regs that have an effect on the address finalizer... Both the finalizer and the HH-CAHPS project have C++ DLL's that get wrapped into .NET DLL's to make other builds easier to manage and deploy. Perhaps the biggest problem for me is that my experience is with other build tools and msbuild works quite different, yet looks so similar...
Shire
I do have the build script working now, and with testing everything but the labeling is solid. Basically I use teh 1st working folder for everything and I don't try to label the 2nd working folder. So everything is working and building as expected. However, while it appears to have a label on the files in my 2nd workspace (3rd party), I am unsure how to prove the files were actually labeled corrrectly as I was expecting no labels on those files... I asked a new question to tackle the labeling problems however. Thanks for all the help.
Shire
Ah ha! Just figure out one of the biggest problems... Has todo with properties... I've now created several custom targets todo work, found a way to verify the label and by moving many actions into custom targets everything is working like I wanted.
Shire
A: 

My script is now working, there was several small problems that was causing the problem. One was that for many commands you have to reference the exact working directory(local folder) in order for the commands to work. Another problem was that setting properties needed to be moved into a custom task so they could be used properly by yet another task. So hopefully my full build script include below will help someone else. Pasting this in any editor will make it easier to read.

  <!-- Generate all of the project specific variables we will need for versioning -->
  <PropertyGroup>
    <VersioningFile>$(SolutionRoot)\TEAM-MAIN\Address Finalizer\Address Finalizer\versionNumber.txt</VersioningFile>
    <TfCommand>"$(TeamBuildRefPath)\..\tf.exe"</TfCommand>
    <WorkingDirectory>$(SolutionRoot)\TEAM-MAIN\Address Finalizer</WorkingDirectory>
    <WorkingDirectory2>$(SolutionRoot)\TEAM-MAIN\HH-CAHPS Project\MAINLINE\3rd Party</WorkingDirectory2>
    <DllName>Address_Finalizer.dll</DllName>
    <PublishedFolder>$(SolutionRoot)\TEAM-MAIN\HH-CAHPS Project\MAINLINE\3rd Party\bin\PG File Import</PublishedFolder>
    <LabelPath>unknown</LabelPath>
  </PropertyGroup>

  <!-- Only edit the properties when usign this as a template for other builds -->

  <Import Project="$(MSBuildExtensionsPath)\ExtensionPack\MSBuild.ExtensionPack.tasks"/>
  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>

  <Target Name="AfterGet">   
    <Message Text="FOOBAR Version File: $(VersioningFile)" />
    <Message Text="FOOBAR Working Dir : $(WorkingDirectory)" />

    <!-- Report what our working folders are -->
    <Exec
      Command='$(TfCommand) workfold'
      WorkingDirectory="$(WorkingDirectory)"/>

    <!-- Generate the version # -->
    <CallTarget Targets="GenerateVersion"/>

    <!-- Version the assemblies -->
    <CallTarget Targets="VersionAssemblies"/>      

    <!-- Configure the deployment for QA -->
    <CallTarget Targets="ConfigureForQA"/>

    <!-- Apply a label to everything -->
    <CallTarget Targets="LabelWorkingDirectory"/>
    <CallTarget Targets="LabelWorkingDirectory2"/>
  </Target>

  <Target Name="AfterDropBuild">
    <!-- Generate all of the project specific variables we will need for deployment, dependant on versioning -->
    <PropertyGroup>
      <ReleaseDLL>$(DropLocation)\$(BuildNumber)\Release\$(DllName)</ReleaseDLL>
      <PublishedDLL>$(PublishedFolder)\$(DllName)</PublishedDLL>
    </PropertyGroup>

    <!-- Check out the published DLL -->
    <Exec 
      Command='$(TfCommand) checkout /lock:checkout "$(PublishedDLL)"'
      WorkingDirectory="$(WorkingDirectory)" />

    <!-- Copy release to published -->
    <Copy 
      SourceFiles="$(ReleaseDLL)" 
      DestinationFolder="$(PublishedFolder)"/>

    <!-- Check in the published DLL -->
    <Exec 
      Command='$(TfCommand) checkin /override:Automated /noprompt /comment:"$(VersionComment)" "$(PublishedDLL)"'
      WorkingDirectory="$(WorkingDirectory)" />
  </Target>

  <Target Name="GenerateVersion">
    <!-- Check out the version file -->
    <Exec
      Command='$(TfCommand) checkout /lock:checkout "$(VersioningFile)"'
      WorkingDirectory="$(SolutionRoot)"/>

    <!-- Increment the revision # in the file and save it -->
    <Version VersionFile="$(VersioningFile)" BuildType="None" RevisionType="Increment">
      <Output TaskParameter="Major" PropertyName="Major" />
      <Output TaskParameter="Minor" PropertyName="Minor" />
      <Output TaskParameter="Build" PropertyName="Build" />
      <Output TaskParameter="Revision" PropertyName="Revision" />
    </Version>

    <!-- Pad the revision out to 3 digits. -->
    <PropertyGroup>
      <PaddedRevision Condition="$(Revision) < 1000">$(Revision)</PaddedRevision>
      <PaddedRevision Condition="$(Revision) < 100">0$(Revision)</PaddedRevision>
      <PaddedRevision Condition="$(Revision) < 10">00$(Revision)</PaddedRevision>
    </PropertyGroup>

    <!-- Generate what our version string looks like for the logfile -->
    <CreateProperty Value="Address Finalizer $(Major).$(Minor).$(Build) Build $(PaddedRevision)">
      <Output TaskParameter="Value" PropertyName="ProjectVersion" />
    </CreateProperty>

    <CreateProperty Value="Automated build: $(ProjectVersion)">
      <Output TaskParameter="Value" PropertyName="VersionComment" />
    </CreateProperty>

    <Message Text="New Version is: $(ProjectVersion)"/>

    <!-- Check in the version file -->
    <Exec
      Command='$(TfCommand) checkin /noprompt /comment:"$(VersionComment)" "$(VersioningFile)"'
      WorkingDirectory="$(SolutionRoot)"/>
  </Target>

  <Target Name="VersionAssemblies">
    <!-- Get all of the assemblyinfo files from all of the directories (should only be one) -->
    <ItemGroup>
      <AssemblyInfoList Include="$(SolutionRoot)\**\assemblyinfo.cs"/>
    </ItemGroup>

    <Message Text="FOOBAR Build Dir    : $(MSBuildProjectDirectory)"/>
    <Message Text="FOOBAR Solution Root: $(SolutionRoot)"/>
    <Message Text="FOOBAR Working Dir  : $(WorkingDirectory)"/>

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

    <!-- Change the version # on all of the assemblyInfo files -->
    <MSBuild.ExtensionPack.Framework.AssemblyInfo AssemblyFileVersion="$(Major).$(Minor).$(Build).$(Revision)" AssemblyInfoFiles="@(AssemblyInfoList)"/>
    <MSBuild.ExtensionPack.Framework.AssemblyInfo AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)" AssemblyInfoFiles="@(AssemblyInfoList)"/>

    <!-- Check in all of the assemblyInfo files -->
    <Exec 
      Command='$(TfCommand) checkin /override:Automated /comment:"$(VersionComment)" /noprompt AssemblyInfo.cs /recursive'
      WorkingDirectory="$(WorkingDirectory)"
      ContinueOnError="false"/> 
  </Target>

  <Target Name="ConfigureForQA">
    <!-- Grab all of the QA app.config files (should only be one) -->
    <CreateItem Include="$(SolutionRoot)\**\app-qa.config">
      <Output TaskParameter ="Include" ItemName ="AppConfigFiles"/>
    </CreateItem>

    <!-- Copy the QA app.config to app.config to make it the driving config file -->
    <Copy
      SourceFiles="@(AppConfigFiles)"
      OverwriteReadOnlyFiles="True"
      DestinationFiles="@(AppConfigFiles->'$(SolutionRoot)\%(RecursiveDir)app%(Extension)')"/>
  </Target>

  <Target Name="LabelWorkingDirectory">
    <Message Text="FOOBAR Version: $(ProjectVersion)" />
    <Message Text="FOOBAR Comment: $(VersionComment)" />
    <Message Text="FOOBAR Path   : $(LabelPath)" />

    <!-- Apply Label on workspace -->
    <Label
      TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
      BuildUri="$(BuildUri)"
      Name="$(ProjectVersion)"
      Files="$(WorkingDirectory)"
      Comments="$(VersionComment)"
      ContinueOnError="False"
      Recursive="True" />
  </Target>

  <Target Name="LabelWorkingDirectory2">
    <Message Text="FOOBAR Version: $(ProjectVersion)" />
    <Message Text="FOOBAR Comment: $(VersionComment)" />
    <Message Text="FOOBAR Path   : $(LabelPath)" />

    <!-- Apply Label on workspace -->
    <Label
      TeamFoundationServerUrl="$(TeamFoundationServerUrl)"
      BuildUri="$(BuildUri)"
      Name="$(ProjectVersion)"
      Files="$(WorkingDirectory2)"
      Comments="$(VersionComment)"
      ContinueOnError="False"
      Recursive="True" />
  </Target>
Shire