62

How can I get MSBuild to evaluate and print in a <Message /> task an absolute path given a relative path?

Property Group

<Source_Dir>..\..\..\Public\Server\</Source_Dir>
<Program_Dir>c:\Program Files (x86)\Program\</Program_Dir>

Task

<Message Importance="low" Text="Copying '$(Source_Dir.FullPath)' to '$(Program_Dir)'" />

Output

Copying '' to 'c:\Program Files (x86)\Program\'

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
Eric Schoonover
  • 47,184
  • 49
  • 157
  • 202

5 Answers5

108

In MSBuild 4.0, the easiest way is the following:

$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\your\path'))

This method works even if the script is <Import>ed into another script; the path is relative to the file containing the above code.

(consolidated from Aaron's answer as well as the last part of Sayed's answer)


In MSBuild 3.5, you can use the ConvertToAbsolutePath task:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         DefaultTargets="Test"
         ToolsVersion="3.5">
  <PropertyGroup>
    <Source_Dir>..\..\..\Public\Server\</Source_Dir>
    <Program_Dir>c:\Program Files (x86)\Program\</Program_Dir>
  </PropertyGroup>

  <Target Name="Test">
    <ConvertToAbsolutePath Paths="$(Source_Dir)">
      <Output TaskParameter="AbsolutePaths" PropertyName="Source_Dir_Abs"/>
    </ConvertToAbsolutePath>
    <Message Text='Copying "$(Source_Dir_Abs)" to "$(Program_Dir)".' />
  </Target>
</Project>

Relevant output:

Project "P:\software\perforce1\main\XxxxxxXxxx\Xxxxx.proj" on node 0 (default targets).
  Copying "P:\software\Public\Server\" to "c:\Program Files (x86)\Program\".

A little long-winded if you ask me, but it works. This will be relative to the "original" project file, so if placed inside a file that gets <Import>ed, this won't be relative to that file.


In MSBuild 2.0, there is an approach which doesn't resolve "..". It does however behave just like an absolute path:

<PropertyGroup>
    <Source_Dir_Abs>$(MSBuildProjectDirectory)\$(Source_Dir)</Source_Dir_Abs>
</PropertyGroup>

The $(MSBuildProjectDirectory) reserved property is always the directory of the script that contains this reference.

This will also be relative to the "original" project file, so if placed inside a file that gets <Import>ed, this won't be relative to that file.

Community
  • 1
  • 1
Roman Starkov
  • 59,298
  • 38
  • 251
  • 324
  • Be careful with `[System.IO.Path]::GetFullPath`. I ran in to an error building in Visual Studio because the current working directory in msbuild was `C:\Windows\System32`. `GetFullPath` resolves relative to the working directory rather than the project directory. – Chris Chilvers Dec 22 '15 at 17:19
  • @ChrisChilvers That's unfortunate. Last time I tested this (admittedly a long time ago), it was relative to the file containing the code. – Roman Starkov Dec 24 '15 at 21:01
  • It also works for backtracing `..\..\your\path` style paths. Also note that the `$(MSBuildThisFileDirectory)` macro already includes a trailing slash, so you must specify `your\path` without the leading slash i.e `$(MSBuildThisFileDirectory)your\path`. One case where this matters is when you use it for an `OutDir` for a Microsoft Unit Testing Framework Test, when you try to run the tests it will be unable to evaluate the `\\\` in the concatenated path to find the built dll. – Adam Yaxley Jun 30 '16 at 02:05
  • @AdamYaxley `GetFullPath` automatically collapses double backslashes, so it will work even if you have that extra backslash. Your issue must have been due to something else. – Roman Starkov Jun 30 '16 at 16:51
35

MSBuild 4.0 added Property Functions which allow you to call into static functions in some of the .net system dlls. A really nice thing about Property Functions is that they will evaluate out side of a target.

To evaluate a full path you can use System.IO.Path.GetFullPath when defining a property like so:

<PropertyGroup>
  <Source_Dir>$([System.IO.Path]::GetFullPath('..\..\..\Public\Server\'))</Source_Dir>
</PropertyGroup>

The syntax is a little ugly but very powerful.

Aaron Carlson
  • 5,522
  • 4
  • 31
  • 35
  • It also evaluates the path relatively to (as far as I see) the project file where the property is defined, which is very nice when you want to include that property in other files. For me, it was the perfect solution. – Jean Hominal Apr 23 '12 at 12:39
  • @JeanHominal If you use ``, it's still relative to the place where it's imported to, which is a shame. Still, this is the only approach that also works outside of targets, which gets it a +1 from me. See [this answer](http://stackoverflow.com/a/2421596/33080) for a way to work around the `` issue. – Roman Starkov Sep 02 '12 at 14:59
  • 1
    Yes, I was mistaken when I said what I did - all relative paths are always relative to the "executing project" path (the project that is currently being executed by MSBuild); however, you can use `$(MSBuildThisFileDirectory)` to get a full path to the currently executing file's directory. – Jean Hominal Sep 02 '12 at 22:36
8

Wayne is correct that well-known metadata does not apply to properties - only to items. Using properties such as "MSBuildProjectDirectory" will work, but I'm not aware of a built in way to resolve the full path.

Another option is to write a simple, custom task that will take a relative path and spit out the fully-resolved path. It would look something like this:

public class ResolveRelativePath : Task
{
    [Required]
    public string RelativePath { get; set; }

    [Output]
    public string FullPath { get; private set; }

    public override bool Execute()
    {
        try
        {
            DirectoryInfo dirInfo = new DirectoryInfo(RelativePath);
            FullPath = dirInfo.FullName;
        }
        catch (Exception ex)
        {
            Log.LogErrorFromException(ex);
        }
        return !Log.HasLoggedErrors;
    }
}

And your MSBuild lines would look something like:

<PropertyGroup>
    <TaskAssembly>D:\BuildTasks\Build.Tasks.dll</TaskAssembly>
    <Source_Dir>..\..\..\Public\Server\</Source_Dir>
    <Program_Dir>c:\Program Files (x86)\Program\</Program_Dir>
</PropertyGroup>
<UsingTask AssemblyFile="$(TaskAssembly)" TaskName="ResolveRelativePath" />

<Target Name="Default">
    <ResolveRelativePath RelativePath="$(Source_Dir)">
    <Output TaskParameter="FullPath" PropertyName="_FullPath" />
    </ResolveRelativePath>
    <Message Importance="low" Text="Copying '$(_FullPath)' to '$(Program_Dir)'" />
</Target>
brock.holum
  • 3,133
  • 2
  • 20
  • 15
  • 6
    Dude, I have learnt more about how to build an MS Task from that piece of code above, then I ever have in the MSBuild documentation. Thank you! :-) – evilhomer Nov 06 '08 at 16:34
5

You are trying to access an item metadata property through a property, which isn't possible. What you want to do is something like this:

<PropertyGroup>
  <Program_Dir>c:\Program Files (x86)\Program\</Program_Dir>
</PropertyGroup>
<ItemGroup>
   <Source_Dir Include="..\Desktop"/>
</ItemGroup>     
<Target Name="BuildAll">
   <Message Text="Copying '%(Source_Dir.FullPath)' to '$(Program_Dir)'" />
</Target>

Which will generate output as:

 Copying 'C:\Users\sdorman\Desktop' to 'c:\Program Files (x86)\Program\'

(The script was run from my Documents folder, so ..\Desktop is the correct relative path to get to my desktop.)

In your case, replace the "..\Desktop" with "......\Public\Server" in the Source_Dir item and you should be all set.

Scott Dorman
  • 42,236
  • 12
  • 79
  • 110
  • +1 This works nicely and I (like others) landed here searching for a way to canonicalize an ItemGroup (pretty much assuming a batch and copy to a new ItemGroup was necessary) - the syntax you show does it without that confusion. IOW I had forgotten about the `FullPath` [Well known metadata](http://msdn.microsoft.com/en-us/library/ms164313.aspx) – Ruben Bartelink Apr 11 '12 at 14:45
4

If you need to convert Properties to Items you have two options. With msbuild 2, you can use the CreateItem task

  <Target Name='Build'>
    <CreateItem Include='$(Source_Dir)'>
      <Output ItemName='SRCDIR' TaskParameter='Include' />
    </CreateItem>

and with MSBuild 3.5 you can have ItemGroups inside of a Task

  <Target Name='Build'>
    <ItemGroup>
      <SRCDIR2 Include='$(Source_Dir)' />
    </ItemGroup>
    <Message Text="%(SRCDIR2.FullPath)" />
    <Message Text="%(SRCDIR.FullPath)" />
  </Target>
Scott Weinstein
  • 18,890
  • 14
  • 78
  • 115