TFS Path Limitations

A recent project was ported into TFS2010, and like all things with custom software development there were issues. I've worked on this project before, and we've run into the issue that the path names were too long, which would cause compile problems in VS2008. There was a simple fix though; just map the solution to the C: drive and call it good. As a result, I’ve adopted the approach in general as this works great for client side development.

Unfortunately in TFS you can't map the repository to the C: drive (OK maybe you can but it's not a great idea and your TFS admins shouldn't allow it...) so the question arises then: How do you do continuous integration builds if TFS can't handle the path names involved? There are three solutions:

  1. Change the Filepath names to be shorter.
  2. Use a different source control/build approach (not best practices solution).
  3. Use MSBuild and scripting (not best practices solution).

File Name Path Length

One of those "silly requirements" that seems like a holdover of eras past where computers were less robust and all variables must be named with six characters or less, is that the max filename path length cannot exceed 256 characters. The error one would get if they try to run a solution whose path lengths are overly long from inside TFS would be:

TF270002: An error occurred copying files from {PATH A HERE}' to '{PATH B HERE}'. Details: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.

TFS Errors

Changing the Filepath name lengths

The first part of this solution is to find out which files are problematic. How do you do that? The solutions I typically work with are big – I mean really big (40+ solutions >500MB). In these solutions, hunting down the errant paths is either a pain in the kiester, or an opportunity to write a tool to do the work for you. I started with the code from Coding Horror (old post but still very relevant) then made a few modifications. You can download the solution here, and read more about file naming on MSDN. Using this code, I was able to find a plethora of files which exceed the system path length character count of 256. Further, TFS "likes" to have character lengths which have 12 characters less than the system max (248 or 242 for Azure and PC, respectively). For a full list of related error messages, see below; for more about this issue in Azure, see this post. That was the easy part. Now you have a list of files which need to be addressed.

With those fixed; “What is the best way to set up solutions in TFS,” you ask? Glad you asked.

Best practices: Solution Layout

Microsoft Patterns and Practices Logo

The Microsoft Patterns and Practices group has a section devoted to Team Development with Visual Studio Team Foundation Server, in which there is a section which specifically addresses Structuring Projects and Solutions in Source Control. The section in there is short, so I will go into a little more detail. Without getting overly philosophical, a source tree is basically just a folder with stuff in it, and tying it to namespace is a bad idea as you can see in these two pictures:

TFS TFS

For those of you who want to have more structure in your solution, you can use Virtual Folders or Linking, but be mindful of how different types of solution structure can affect Branching & Merging. Personally, I prefer approaching my solution using the “GoodApproach” style (bet that was a surprise), but here is my reasoning: if someone new looks at your code, they can see instantly from the namespace how to drill down to the file in question. Virtual folders can come in handy when you want to structure the code more deeply than the namespace would signify. For example if you have a strategy pattern, it may be helpful to put the individual strategies into a virtual folder to keep your solution clean, but have them in the same physical folder, like this:

TFS Virtual Folder

Setting up an MSBuild Approach

I was very lucky that the previous solution (again, because the path lengths were too long) had been set up to use MSBuild (3.0 tooling). I could take the existing build project, .bat file, and environment settings file and just migrate them. Easy to say, but more complicated to do as it turns out (isn't that always the case?). This sample has been designed to work on your local system, note that the TFS passwords have been commented out. I'd like to stress that I don't believe it is a sustainable approach for long lived application life cycle management, however there are times when it is the approach which must be used, so here is how you start.

The Parts

Simple.sln

You have four basic parts. Obviously, you have your .sln or .proj files from inside VS, you have a MSBuild PROJ file (*.proj), you have a Windows Batch File (*.bat) file, and then you have some optional stuff (an additional settings file (*.proj). You might also have some other static resources you need to deploy along with your solution, like install instructions, readme's, etc. Here is the MSBuild specific folder structure that I like. Note that my source is not in this folder structure (if you’re working with TFS this may be important). We'll assume you have a working (compiling at least) VS.proj file, and we will call it "Simple.sln" which lives at C:Code\Samples\Simple\Simple.sln.

The MSBuild.proj file

The first thing to note here is that I’m using two external build task libraries: the MSBuild Extension Pack on codeplex and the MSBuild Community Tasks Project available from Tigris.org. Both offer extended functionality for working with MSBuild, and both are worth taking a look at. Also worthy of note here is that most of the "stuff" that will change from implementation to implementation is in the settings file discussed below. In a nutshell, this script will delete/create the Drop folder as output, and package up the solution for you. I should mention that you can also use a TFS build event this way, however as we are talking about solutions that don't work in TFS, that approach will not work. You can download a sample here. Note: I've commented out the part of the file that would be used to pull the solution From TFS ( see: <!-- to Get latest from TFS --> ), this is left as an exercise for the user.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"   
ToolsVersion="4.0" DefaultTargets="FullBuild">


<
Import Project="$(MSBuildExtensionsPath)\ExtensionPack\4.0\
MSBuild.ExtensionPack.tasks
" />


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

         <!-- Project Imports -->

         <Import Project="$(MSBuildToolsPath)\Microsoft.Common.targets" />

         <Import Project="..\Settings\Simple_Settings.proj" />

         <ItemGroup>

                 <!-- Solution To Build -->

                 <SolutionToBuild Include="$(SourceRoot)\Simple.sln" />

         </ItemGroup>

         <PropertyGroup>

                 <!-- Solution  bin folder-->

                 <SolutionBinFolder>$(SourceRoot)\Simple\bin</SolutionBinFolder>

         </PropertyGroup>

         <!-- Build Target Dependencies -->

         <PropertyGroup>

                 <FullBuildDependsOn>

                          $(FullBuildDependsOn);

                          BeforeBuild;

                          CoreBuild;

                          AfterBuild

                 </FullBuildDependsOn>

                 <FullRebuildDependsOn>

                          $(FullRebuildDependsOn);

                          BeforeRebuild;

                          Clean;

                          FullBuild;

                          AfterBuild

                 </FullRebuildDependsOn>

         </PropertyGroup>

 

         <!-- FullBuild Target -->

         <Target Name="FullBuild" DependsOnTargets="$(FullBuildDependsOn)"/>

 

         <!-- BeforeBuild Target -->

         <Target Name="BeforeBuild">

                 <Message Importance="high" Text="Begin BeforeBuild" />

                 <!-- Clean and Recreate Output Directory -->

                 <RemoveDir Directories="$(OutDir)" />

                 <MakeDir Directories="$(OutDir)"/>

                 <!-- Get Date/Time to create unique .zip file name -->

<MSBuild.ExtensionPack.Framework.DateAndTime TaskAction="Get" 
Format="MMddyyyy">

                          <Output TaskParameter="Result" PropertyName="_DateTime"/>

                 </MSBuild.ExtensionPack.Framework.DateAndTime>

                 <!-- Delete current source code -->

                 <Message Text="Deleting: $(SourceRoot)Simple\*.*" />

                 <RemoveDir Directories="$(SourceRoot)Simple\" />

 

                 <ItemGroup>

                          <!-- Solution  bin folder-->

<ZipFileNameFullSource Include="$(OutDir)Simple_Source_$(BuildName)
_$(_DateTime).zip
"/>

                          <!-- Solution  bin folder-->

<ZipFileNameAppCompiled Include="$(OutDir)Simple_App_$(BuildName)
_$(_DateTime).zip
"/>

                 </ItemGroup>

                 <!-- to Get latest from TFS -->

                 <!--

                 <Message Text="Attempting to get Latest" />

<Exec Command="$(TFSExec) get $(TFSRoot) /recursive /force" 
WorkingDirectory="$(SourceRoot)" Condition="$(TFSLogin)==''"/>

                 <Message Text="TFS Get Complete" />-->

                 <Message Importance="high" Text="Finish BeforeBuild" />

                 <OnError ExecuteTargets="HandleErrors"/>

         </Target>

         <Target Name="AfterBuild">

                 <Message Importance="high" Text="Begin AfterBuild" />

                 <Message Text="Build completed, zipping the source" />

<Message Text="************************Zip Source ***********************
*****
" />

                 <ItemGroup>

                          <SourceFilesToZip Include="$(SourceRoot)\**\*"

 Exclude="$(SourceRoot)\**\obj\**\*;$(SourceRoot)\**\bin\**\*"   />

                 </ItemGroup>

<MSBuild.Community.Tasks.Zip  Files="@(SourceFilesToZip)" 
ZipFileName="@(ZipFileNameFullSource)" WorkingDirectory="@(SourceRoot)" />

                 <Message Text="zip folder:$(SourceRoot)" />

<Message Text="************************Zip App **************************
****s
" />

                 <Message Text="Pulling files for zip from:$(SolutionBinFolder)" />

                 <ItemGroup>

<WebFilesToZip Include="$(SolutionBinFolder)\**\*" Exclude="" />

                 </ItemGroup>

                 <MSBuild.Community.Tasks.Zip

                          Files="@(WebFilesToZip)"

                          ZipFileName="@(ZipFileNameAppCompiled)"

                          WorkingDirectory="@(SourceRoot)"/>

                 <Message Importance="high" Text="Finish AfterBuild" />

                 <OnError ExecuteTargets="HandleErrors"/>

         </Target>

         <!-- CoreBuild Target -->

         <Target Name="CoreBuild">

                 <Message Importance="high" Text="Begin CoreBuild" />

                 <MSBuild

                 Projects="@(SolutionToBuild)"

                 BuildInParallel="true"

                 Properties="Configuration=%(AllConfigurations.Identity)"/>


<
Message Importance="high" Text="*****************Build Finished**********
***********
" />

                 <OnError ExecuteTargets="HandleErrors"/>

         </Target>

         <!-- FullRebuild Target -->

<Target Name="FullRebuild" DependsOnTargets=
"$(FullRebuildDependsOn)"/>

         <!-- Clean Target -->

         <Target Name="Clean">

                 <!-- Clean for each configuration -->

                 <MSBuild

                 Projects="@(SolutionToBuild)"

                 BuildInParallel="true"

                 Properties"Configuration=%(AllConfigurations.Identity);"

                 Targets="Clean"  />

                 <OnError ExecuteTargets="HandleErrors"/>

         </Target>

         <Target Name="HandleErrors">

<Message Text="An error has occurrend and the build will fail"  
Importance="high" />

         </Target>

</Project>

The MSBuild_Settings.proj file

As noted above, this file houses all the environment specifics needed to tweak the build script:

<Project DefaultTargets = "Compile" ToolsVersion="4.0" xmlns="
http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>

    <BuildName>MySimpleBuild</BuildName>

    <!--  Root folder for source control. This folder is only child of
    this folder.  
-->

    <SourceRoot>C:\Code\Samples\Simple</SourceRoot>

    <DeploymentResourcesFolder>C:\Code\Samples\Simple\Resources
    </DeploymentResourcesFolder>

    <!--         Root folder for zip file output.  -->

    <OutDir>C:\Drop\</OutDir>

    <TFSExec>"C:\Program Files (x86)\Microsoft Visual Studio 10.0\
    Common7\IDE\tf.exe"
</TFSExec>

 

    <SolutionToBuild>$(SourceRoot)\Simple.sln</SolutionToBuild>

 

    <TFSRoot>$/Simple\Simple</TFSRoot>

 

    <!-- MSBuild Extension Pack Locations -->

    <ExtensionPackRoot>C:\Program Files\MSBuild\ExtensionPack\
    </ExtensionPackRoot>

    <CommunityTaskRoot>C:\Program Files\MSBuild\MSBuildCommunityTasks\
    </CommunityTaskRoot>

  </PropertyGroup>

  <ItemGroup>

    <!-- define all the configurations that we should build for -->

    <AllConfigurations Include="Debug" />

    <AllConfigurations Include="Release" />

  </ItemGroup>

</Project>

The Windows Batch File

Simple, just call the appropriate framework number of MSBuild.exe, giving it the path of your solution file (remember that the settings are included in that file).

C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\msbuild.exe

C:\BuildFiles\Projects\Build_Simple_Sln.proj

Here is what the output in the command line will look like:

The resource file

This can be any random file that you want to move around:

Tying it all together

You will need to install the sample folders accordingly:

  1. XCOPY the BuildFiles, and Code Folders to your C: drive (you can place them elsewhere, but you will need to modify the Filepaths in the projects and settings files to get it to work)
  2. Install MSBuild Extension Pack on codeplex and the MSBuild Community Tasks Project available from Tigris.org, the appropriate CPU and Framework (I used 4.0 in the sample)
  3. In cmd.exe Run: C:BuildFiles\BatFiles\Simple

Conclusion

Migrating your legacy solution to a continuous integration approach in TFS may seem challenging at first, but don't let that stop you from leveraging all the wonderful benifits that TFS has to offer. Path name limitations, as has been shown here, can easily be found and fixing them can be incorporated into your maintenance cycles. If you run into problems it doesn't mean that you should abandon all hope, just get creative until you can fix the underlying problem! By using best practices you can navigate your way through a relatively simple workaround, to get your continious integration up and running right off the bat.

Appendix:

Relevant Errors Texts
  • "The Path is too long after being fully qualified"
  • "The path is too long after being fully qualified. Make sure the full path is less than 260 characters and the directory name is less than 248 characters."
Categories

Copyright 2012, Magenic
Locations      Services      Portfolio      About Magenic      Careers      Contact      Blog        Login