When using MSBuild you can import external declarations into your project by using the Import element. This is very useful for distributing your shared targets across your organization, and is a way to segregate where things are located. When MSBuild encounters this tag, a few things happen. First the current directory is changed to that of the file that is being imported. This is done so assembly locations specified by the Using Task and other imports are resolved correctly. What you may be surprised to find out is that if the importing project file declares any items, those item declarations are always relative to the top project file location. To clarify this if you have the following directory structure:
C:.
├───one
│ └───Two
│ YourProject.proj
│
├───Shared
│ │ CurrentDirectory.dll
│ │ SharedTargets.targets
│ │
│ └───Another
│ SharedTargets_2.targets
│
└───utils
CommandLine.txt
In this scenario YourProject.proj is the top project file, it imports both SharedTargets.targets and SharedTargets_2.targets files. If the SharedTargets.targets had an item declaration of
When imported by YourProject.proj this item declaration would actually resolve to C:\one\two\test.txt instead of the expected C:\Shared\test.txt value. In 95% of the time this is not an issue at all. But for that other 5% how can we accomplish this?
Well, there is no magic reserved MSBuild property for this. So we're gonna have to do some work here. To accomplish this we'll have to create a custom MSBuild task. If you've never created an MSBuild task you might be surprised how easy it is!
I created a new project named DirectoryTask which will house this task. Following this I added a reference to Microsoft.Build.Framework and Microsoft.Build.Utilities assemblies. Following this I wrote the task. It is shown below
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace CurrentDirectory
{
///
/// Task that will return the folder that contains the current project.
/// Inheriting from AppDomainIsolatedTask causes this task to execute in its own
/// App domain. Not necessary here, only for demonstration.
///
/// Sayed Ibrahim Hashimi
/// www.sedodream.com
///
public class CurrentDir : AppDomainIsolatedTask
{
private ITaskItem currentDir;
[Output]
public ITaskItem CurrentDirectory
{
get
{
return this.currentDir;
}
}
public override bool Execute()
{
System.IO.FileInfo projFile = new System.IO.FileInfo(base.BuildEngine.ProjectFileOfTaskNode);
this.currentDir = new TaskItem(projFile.Directory.FullName);
return true;
}
}
}
As you can see this is a pretty simple task. When Execute is called the directory is gathered from the project file that contains the task invocation. The name of the assembly that I built this into is CurrentDirectory.dll.
Now to see this in action I will use the same directory structure shown above. The contents of the SharedTargets.targets file is:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask AssemblyFile="CurrentDirectory.dll" TaskName="CurrentDir"/>
<Target Name="SharedTarget">
<CurrentDir>
<Output ItemName="CurrentDir" TaskParameter="CurrentDirectory" />
CurrentDir>
<Message Text="Inside the SharedTargets.targets" Importance="high"/>
<Message Text="Location: @(CurrentDir->'%(Fullpath)')"/>
Target>
Project>
This uses the CurrentDirectory task to determine what the current directory is. The SharedTargets_2.targets file is very similar and is:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="SharedTarget2">
<CurrentDir>
<Output ItemName="CurrentDir2" TaskParameter="CurrentDirectory" />
CurrentDir>
<Message Text="Inside the SharedTargets_2.targets" Importance="high"/>
<Message Text="Location: @(CurrentDir2->'%(Fullpath)')"/>
Target>
Project>
Now let’s take a look at the very simple YourProject.Proj file.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Print">
<Import Project="..\..\Shared\SharedTargets.targets"/>
<Import Project="..\..\Shared\Another\SharedTargets_2.targets"/>
<PropertyGroup>
<PrintDependsOn>
SharedTarget;
SharedTarget2
PrintDependsOn>
PropertyGroup>
<Target Name="Print" DependsOnTargets="$(PrintDependsOn)">
Target>
Project>
This file simply imports the two other files and defines the print target. Now we want to invoke the Print target on this project file. To do this open the Visual Studio2005 command prompt and go the directory containing the YourProjec.proj file. Then execute the following:
>msbuild.exe YourProject.proj /t:Print
The results of this invocation are:
__________________________________________________
Project "C:\Data\Community\msbuild\MSBuildDirectoryExample\one\Two\YourProject.proj" (default targets):
Target SharedTarget:
Inside the SharedTargets.targets
Location: C:\Data\Community\msbuild\MSBuildDirectoryExample\Shared
Target SharedTarget2:
Inside the SharedTargets_2.targets
Location: C:\Data\Community\msbuild\MSBuildDirectoryExample\Shared\Another
As you can see the correct location was resolved for both of these items. Previously if you had command line utilities in source control and .targets files that would manage invoking those tools, it was error prone. This is because the .targets file wouldn’t be able to resolve the location of the command line util, even if in the same directory. The solution to this problem it to rely on the developer to set a property (or environment variable) which states where this tool can be located; or some other similar means. With this task we no longer have to rely on such a solution. The .targets file is able to resolve the location of the command line utility.
I have bundled all related files into a zip file which you can download below.
MSBuildDirectoryExample.zip (you may have to right-click->Save As)
http://www.sedodream.com/content/binary/MSBuildDirectoryExample.zip
Sayed Ibrahim Hashimi
Comments are closed.