Processing batch items in parallel - msbuild

I have an ItemGroup, and want to process all its items in parallel (using a custom task or an .exe).
I could write my task/exe to accept the entire ItemGroup and process its items in parallel internally. However, I want this parallelism to work in conjunction with MSBuild's /maxCpuCount param, since otherwise I might end up over-parallelizing.
This thread says there's no way.
My testing shows that MSBuild's /maxCpuCount only works for building different projects, not items (see code below)
How can I process items from an ItemGroup in parallel?
Is there a way to author a custom task to work in parallel in conjunction with MSBuild's Parallel support?
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Build" >
<!-- Runs only once - I guess MSBuild detects it's the same project -->
<!--<MSBuild Projects="$(MSBuildProjectFullPath);$(MSBuildProjectFullPath)" Targets="Wait3000" BuildInParallel="true" />-->
<!-- Runs in parallel!. Note that b.targets is a copy of the original a.targets -->
<MSBuild Projects="$(MSBuildProjectFullPath);b.targets" Targets="Wait3000" BuildInParallel="true" />
<!-- Runs sequentially -->
<ItemGroup>
<Waits Include="3000;2000"/>
</ItemGroup>
<Wait DurationMs="%(Waits.Identity)" />
</Target>
<Target Name="Wait3000">
<Wait DurationMs="3000" />
</Target>
<UsingTask TaskName="Wait" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<DurationMs ParameterType="System.Int32" Required="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs">
Log.LogMessage(string.Format("{0:HH\\:mm\\:ss\\:fff} Start DurationMs={1}", DateTime.Now, DurationMs), MessageImportance.High);
System.Threading.Thread.Sleep(DurationMs);
Log.LogMessage(string.Format("{0:HH\\:mm\\:ss\\:fff} End DurationMs={1}", DateTime.Now, DurationMs), MessageImportance.High);
</Code>
</Task>
</UsingTask>
</Project>

I know this is old, but if you get a few minutes, revisit your attempt to use the MSBuild task. Using the Properties and/or AdditionalProperties reserved item metadata elements* will resolve the issue you described in your code sample ("Runs only once - I guess MSBuild detects it's the same project").
The MSBuild file below processes items from an ItemGroup in parallel via MSBuild's parallel support (including /maxCpuCount). It does not use BuildTargetsInParallel from the MSBuild Extension Pack, nor any other custom or inline task.
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Build" >
<ItemGroup>
<Waits Include="3000;2000"/>
</ItemGroup>
<ItemGroup>
<ProjectItems Include="$(MSBuildProjectFullPath)">
<Properties>
WaitMs=%(Waits.Identity)
</Properties>
</ProjectItems>
</ItemGroup>
<MSBuild Projects="#(ProjectItems)" Targets="WaitSpecifiedMs" BuildInParallel="true" />
</Target>
<Target Name="WaitSpecifiedMs">
<Wait DurationMs="$(WaitMs)" />
</Target>
</Project>
* Well-hidden under "Properties Metadata" on the MSBuild Task reference page.

As you said yourself, you can't parallelize on target or task level, you can yield though.
My custom tasks parallelize heavily using TPL, i.e. my base task wrapper has a ForEach wrapper.
public bool ForEach<T>(IEnumerable<T> enumerable, Action<T> action, int max = -1)
{
return enumerable != null && Parallel.ForEach(enumerable, new ParallelOptions { MaxDegreeOfParallelism = max }, (e, s) =>
{
if (Canceled)
s.Stop();
if (s.ShouldExitCurrentIteration)
return;
action(e);
Interlocked.Increment(ref _total);
}).IsCompleted;
}
Typically limit is omitted and managed by .NET itself, with few exception like non-thread safe operations like MSDeploy, deploying SSRS reports that has a config DoS limit of 20 from single IP, or a zip task that degrades heavily if it's more than CPU count even by 1. It's probably not worth reading maxCpuCount and use Environment.ProcessorCount or %NUMBER_OF_PROCESSORS%, but you can try parsing the command line or reflecting on the host object, e.g. my base task class has this method to get all properties, targets, etc. for various extra special global flags.
private void Engine(object host)
{
var type = host.GetType();
if (type.FullName != "Microsoft.Build.BackEnd.TaskHost")
{
Log.Warn("[Host] {0}", type.AssemblyQualifiedName);
return;
}
var flags = BindingFlags.NonPublic | BindingFlags.Instance;
var taskLoggingContext = type.GetProperty("LoggingContext", flags).GetValue(host, null);
var targetLoggingContext = taskLoggingContext.GetType().GetProperty("TargetLoggingContext", flags).GetValue(taskLoggingContext, null);
ProjectTask = taskLoggingContext.GetType().GetProperty("Task", flags).GetValue(taskLoggingContext, null).To<ProjectTaskInstance>();
ProjectTarget = targetLoggingContext.GetType().GetProperty("Target", flags).GetValue(targetLoggingContext, null).To<ProjectTargetInstance>();
var entry = type.GetField("requestEntry", flags).GetValue(host);
var config = entry.GetType().GetProperty("RequestConfiguration").GetValue(entry, null);
Project = config.GetType().GetProperty("Project").GetValue(config, null).To<ProjectInstance>();
Properties = Project.Properties.ToDictionary(p => p.Name, p => p.EvaluatedValue);
Typical task would look something like this using ForEach:
public class Transform : Task
{
[Required]
public ITaskItem[] Configs { get; set; }
protected override void Exec()
{
//...
ForEach(Configs, i =>
{
//...
}, Environment.ProcessorCount);
//...
}

Related

MSBuild: re-use method from one CodeTaskFactory target in another one

Updated due to weak description.
I have created this CodeTaskFactory MSBuild Task for the "BeforeBuild" Target:
<UsingTask
TaskName="WriteIntoFileIfDifferent"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
<ParameterGroup>
<FileName ParameterType="System.String" Required="True" Output="False"/>
<Text ParameterType="System.String" Required="True" Output="False"/>
</ParameterGroup>
<Task>
<Code Type="Class" Language="cs">
<![CDATA[
...
public class WriteIntoFileIfDifferent : Task
{
...
public override bool Execute ()
{
...
CopyFileIfNeeded (tempFileName, m_fileName);
...
}
private void CopyFileIfNeeded (string i_sourceFileName,
string i_targetFileName)
{
...
CheckFileCopyNeeded (i_sourceFileName, i_targetFileName))
...
}
private static bool CheckFileCopyNeeded (string i_sourceFileName,
string i_targetFileName)
{
...
return !CheckStreamContentsEqual (sourceFileStream, targetFileStream);
...
}
private static bool CheckStreamContentsEqual (FileStream i_sourceFileStream,
FileStream i_targetFileStream)
{
...
}
}
]]>
</Code>
</Task>
</UsingTask>
Now I have created another Task of the same type, also for the "BeforeBuild" Target, which is now calling both tasks. Both tasks contain a "CopyFileIfNeeded()" method; it's actually a plain copy.
How can I extract the method CopyFileIfNeeded() from both tasks and put it at a separate place, so that both tasks can use it?
Is this even possible?
If yes, how can I call this method in my tasks?
If someone is interested in the full code, I can add it here.
Not sure how to resue code across Tasks using CodeTaskFactory; it might be possible depedning on how those are implemented exactly. E.g. if they'd create a dll from the code supplied and you can figure out which dll you could reference it and use it's functions. But instead of figuring out whether that is the case, we can also just write that exact principle ourselves:
create a file with the common code, Common.cs:
namespace Foo
{
public static class Bar
{
public static bool CopyFileIfDifferent(string A, string B)
{
return true;
}
}
}
build that file on the fly if needed (should only build if the source file changes or the dll does not exist because of using Inputs and Outputs):
<PropertyGroup>
<CommonDll>$(Temp)\Common.dll</CommonDll>
</PropertyGroup>
<Target Name="BuildCommonDll" Inputs="$(MSBuildThisFileDirectory)Common.cs" Outputs="$(CommonDll)">
<Csc Sources="$(MSBuildThisFileDirectory)Common.cs"
References="System.dll;mscorlib.dll"
TargetType="Library" OutputAssembly="$(CommonDll)"/>
</Target>
in your Task(s) reference the built dll:
<UsingTask
TaskName="WriteIntoFileIfDifferent"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
<ParameterGroup>
<FileName ParameterType="System.String" Required="True" Output="False"/>
<Text ParameterType="System.String" Required="True" Output="False"/>
</ParameterGroup>
<Task>
<Reference Include="$(CommonDll)" />
<Code Type="Class" Language="cs">
<![CDATA[
using Foo;
public class WriteIntoFileIfDifferent : Microsoft.Build.Utilities.Task
{
public string FileName{ get; set;}
public string Text{ get; set;}
public override bool Execute ()
{
var result = Foo.Bar.CopyFileIfDifferent(FileName, Text);
Log.LogMessage("result = " + result.ToString());
return true;
}
}
]]>
</Code>
</Task>
</UsingTask>
in the Targets invoking this make sure the dll building is a dependency:
<Target Name="Build" DependsOnTargets="BuildCommonDll">
<WriteIntoFileIfDifferent FileName="A" Text="B"/>
</Target>
Output of first run:
Project "c:\temp\foo.targets" on node 1 (Build target(s)).
BuildCommonDll:
C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\bin\Roslyn\csc.exe /reference:System
.dll /reference:mscorlib.dll /out:c:\temp\Common.dll /target:library c:\temp\Common.cs
Build:
result = True
Done Building Project "c:\temp\foo.targets" (Build target(s)).
Output of consecutive runs:
Project "c:\temp\foo.targets" on node 1 (Build target(s)).
BuildCommonDll:
Skipping target "BuildCommonDll" because all output files are up-to-date with respect to the input files.
Build:
result = True
Done Building Project "c:\temp\foo.targets" (Build target(s)).

Problem Reading Embedded Resource With ASP.NET Core 3.1 in Console App

I'm failing at being able to read embedded resources in ASP.NET Core 3.1. Specifically, I'm following the example in the docs here:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers?view=aspnetcore-3.1
I've updated my csproj file to the following adding the <GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Data\sessions.json" />
<EmbeddedResource Include="Data\speakers.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EFLib\EFLib.csproj" />
<ProjectReference Include="..\RepositoryLib\RepositoryLib.csproj" />
<ProjectReference Include="..\SeedDataLib\SeedDataLib.csproj" />
</ItemGroup>
</Project>
I have console app as follows and I get the error below when I run it.
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Hello World!");
var manifestEmbeddedProvider =
new ManifestEmbeddedFileProvider(typeof(Program).Assembly); // ERROR HERE
{"Could not load the embedded file manifest 'Microsoft.Extensions.FileProviders.Embedded.Manifest.xml' for assembly 'TestConsoleApp'."}
I'm basically trying to do what I use to do in ASP.NET Core 2 which was this and it's not working.
var assembly = Assembly.GetEntryAssembly();
string[] resources = assembly.GetManifestResourceNames(); // debugging purposes only to get list of embedded resources
I faced the same issue you described. Make sure that you added the following package reference to the .csproj where embedded resources are declared. Once I added it to my project and rebuilt the solution, it started working.
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="3.1.0" />
</ItemGroup>

How to re-run property evaluation in MSBuild target?

I have a custom MSBuild target, partial snippet as follows ..
<Target Name="PublishHtm">
<PropertyGroup>
<PublishHtmTemplateContents>$([System.IO.File]::ReadAllText($(PublishHtmTemplatePath)))</PublishHtmTemplateContents>
<PublishHtm>$(PublishHtmTemplateContents)</PublishHtm>
</PropertyGroup>
<WriteLinesToFile Lines="$(PublishHtm)" File="$(PublishDir)\publish.htm" Overwrite="true"/>
</Target>
This is a rework attempt for this solution in that I'm trying to isolate this template to an external file. The template contains MSBuild property references such as $(ApplicationName). When doing this exactly as described in the linked solution, it works fine, but when loading the template in as a string, none of these property expressions are evaluated by the time it gets to the file.
<SPAN CLASS="BannerTextApplication">$(ApplicationName)</SPAN>
Is there an MSBuild expression/function I can use to get the string to be reevaluated given the context that the Target is being invoked?
BTW I'd rather not work around the problem using find/replace or regex replace, and stick with the MSBuild expression engine.
Using Visual Studio 2012 & .NET Framework 4.5.
Sorry for not getting back to this question for awhile.
Initially I thought that to solve this problem we'll need to bend MSBuild in very unusual way (plan for today was to write complex inline task which will do regex-replace in external file using Msbuild properties as tokens ). But I think this can be solved easier, using CDATA section, which is valid inside property definition:
Here is main script:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
<PropertyGroup>
<MyOtherProperty>$([System.DateTime]::Now)</MyOtherProperty>
<Version>1.0.1b</Version>
<ProjectName>MSBuild Rox</ProjectName>
<Author>Alexey Shcherbak</Author>
</PropertyGroup>
<Target Name="Build">
<ItemGroup>
<PropsToPass Include="MyOtherProperty=$(MyOtherProperty)" />
<PropsToPass Include="Version=$(Version)" />
<PropsToPass Include="ProjectName=$(ProjectName)" />
<PropsToPass Include="Author=$(Author)" />
</ItemGroup>
<MSBuild Projects="TransformHTML.Template.proj" Properties="#(PropsToPass)" />
</Target>
</Project>
And here is your template. It's not pure html, it's still msbuild file, but at least without ugly encoding for html tags in xml. It's just a block in CDATA
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Transform">
<PropertyGroup>
<HtmlProperty><![CDATA[
<body>
<div>$(MyOtherProperty)</div>
<div>$(Version)</div>
<div>$(ProjectName)</div>
<div>$(Author)</div>
</body>
]]></HtmlProperty>
</PropertyGroup>
<Target Name="Transform">
<Message Text="HtmlProperty: $(HtmlProperty)" Importance="High" />
</Target>
</Project>
Maybe it's not very elegant ( I personally don't like the section with #PropsToPass) but it'll do the job. You can put everything inline into single file and then you don't need to pass properties to MSBuild task. I don't like massive html-encoding from proposed "this solution" but I'd rather prefer to keep HTML template in the same script where it'll be transformed, just in nice html format, without encoding.
Single file example:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
<PropertyGroup>
<MyOtherProperty>$([System.DateTime]::Now)</MyOtherProperty>
<Version>1.0.1b</Version>
<ProjectName>MSBuild Rox</ProjectName>
<Author>Alexey Shcherbak</Author>
</PropertyGroup>
<Target Name="Build">
<PropertyGroup>
<HtmlProperty><![CDATA[
<body>
<div>$(MyOtherProperty)</div>
<div>$(Version)</div>
<div>$(ProjectName)</div>
<div>$(Author)</div>
</body>
]]></HtmlProperty>
</PropertyGroup>
<Message Text="HtmlProperty: $(HtmlProperty)" Importance="High" />
</Target>
</Project>
You can also download these files here
You can do it using Eval task
<Target Name="PublishHtm">
<PropertyGroup>
<PublishHtmTemplateContents>$([System.IO.File]::ReadAllText($(PublishHtmTemplatePath)))</PublishHtmTemplateContents>
<Eval Values="$(PublishHtmTemplateContents)">
<Output TaskParameter="Result" ItemName="EvalItemTemp"/>
</Eval>
<PublishHtm>%(EvalItemTemp.Identity)</PublishHtm>
</PropertyGroup>
<WriteLinesToFile Lines="$(PublishHtm)" File="$(PublishDir)\publish.htm" Overwrite="true"/>
</Target>
Actually the task does nothing except returning exactly the same value it received, however when you pass the returned value %(EvalItemTemp.Identity) to anywhere, msbuild does evaluation!
Eval task source:
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Choose>
<When Condition="'$(MSBuildToolsVersion)' == 'Current' OR $(MSBuildToolsVersion.Split('.')[0]) >= 14">
<PropertyGroup>
<TasksAssemblyName>Microsoft.Build.Tasks.Core.dll</TasksAssemblyName>
</PropertyGroup>
</When>
<Otherwise>
<PropertyGroup>
<TasksAssemblyName>Microsoft.Build.Tasks.v$(MSBuildToolsVersion).dll</TasksAssemblyName>
</PropertyGroup>
</Otherwise>
</Choose>
<UsingTask TaskName="Eval" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\$(TasksAssemblyName)">
<ParameterGroup>
<Values ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="True" Output="False" />
<Result ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="False" Output="True" />
</ParameterGroup>
<Task>
<Code Type="Class" Language="cs" Source="$(MSBuildThisFileDirectory)TaskSource\EvalTask.cs"/>
</Task>
</UsingTask>
</Project>
Where TaskSource\EvalTask.cs is
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Diagnostics;
using System.Threading;
namespace Varonis.MSBuild.Tasks
{
public class Eval : Task
{
[Required]
public ITaskItem[] Values { get; set; }
[Output]
public ITaskItem[] Result { get; set; }
public override bool Execute()
{
Result = new TaskItem[Values.Length];
for (int i = 0; i < Values.Length; i++)
{
Result[i] = new TaskItem(Values[i].ItemSpec);
}
return true;
}
}
}

How do I update an xml file with msbuild with two namespaces?

This msbuild below task can take into account one namespace, but in the case where I'm updating an mxml (flex) that has a mix of namespaces, can I use this task or another msbuild task to do the update?
<XmlUpdate
Prefix="fx"
Namespace="http://ns.adobe.com/mxml/2009"
XmlFileName="myFlexApp.mxml"
Xpath="//mx:Application/fx:Declarations/fx:String[#id='stringId']"
Value="xxxxx">
Here is the flex xml I'm trying to update:
<mx:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns:s="library://ns.adobe.com/flex/spark">
<fx:Declarations>
<fx:String id="stringId">UPDATE_ME</fx:String>
</fx:Declarations></mx:Application>
I was able to successfully update the source for XmlUpdate so that it takes multiple namespaces:
if (!string.IsNullOrEmpty(_prefix) && !string.IsNullOrEmpty(_namespace))
{
string[] prefixes = _prefix.Split(';');
string[] namespaces = _namespace.Split(';');
if (prefixes.Length != namespaces.Length)
throw new Exception("The number of prefixes is different from the number of namespaces");
for (int prefixIndex = 0; prefixIndex < prefixes.Length; prefixIndex++)
{
manager.AddNamespace(prefixes[prefixIndex], namespaces[prefixIndex]);
}
}
This works with the example of
<XmlUpdate
Prefix="fx;mx"
Namespace="http://ns.adobe.com/mxml/2009;library://ns.adobe.com/flex/mx"
XmlFileName="myFlexApp.mxml"
Xpath="//mx:Application/fx:Declarations/fx:String[#id='stringId']"
Value="xxxxx">
You'll have to use XmlMassUpdate task. (This task is from MSBuild Community Tasks)
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- The replacement value is here -->
<!-- ProjectExtensions keep MSBuild to try to evaluate the content -->
<ProjectExtensions>
<ReplacementNode>
<String id="stringId">CHANGE</String>
</ReplacementNode>
</ProjectExtensions>
<Target Name="XmlUpdate">
<XmlMassUpdate
ContentFile="myFlexApp.mxml"
NamespaceDefinitions="msb=http://schemas.microsoft.com/developer/msbuild/2003;
fx=http://ns.adobe.com/mxml/2009;
mx=library://ns.adobe.com/flex/mx"
ContentRoot="//mx:Application/fx:Declarations/fx:String[#id='stringId']"
SubstitutionsFile="$(MSBuildProjectFullPath)"
SubstitutionsRoot="msb:Project/msb:ProjectExtensions/msb:ReplacementNode/msb:String"/>
</Target>
</Project>
Changing the value during execution
The tricky part is that you can't define a value on fly using XmlMassUpdate only, you'll need to use XmlUpdate to update the value in your replacement node first.
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- The replacement value is here -->
<!-- ProjectExtensions keep MSBuild to try to evaluate the content -->
<ProjectExtensions>
<ReplacementNode>
<String id="stringId">CHANGE</String>
</ReplacementNode>
</ProjectExtensions>
<Target Name="XmlUpdate" DependsOnTargets="ChangeXmlValue">
<XmlMassUpdate
ContentFile="myFlexApp.mxml"
NamespaceDefinitions="msb=http://schemas.microsoft.com/developer/msbuild/2003;
fx=http://ns.adobe.com/mxml/2009;
mx=library://ns.adobe.com/flex/mx"
ContentRoot="//mx:Application/fx:Declarations/fx:String[#id='stringId']"
SubstitutionsFile="$(MSBuildProjectFullPath)"
SubstitutionsRoot="msb:Project/msb:ProjectExtensions/msb:ReplacementNode/msb:String"/>
</Target>
<Target Name="ChangeXmlValue">
<XmlUpdate Prefix="n"
Namespace="http://schemas.microsoft.com/developer/msbuild/2003"
XPath="n:Project/n:ProjectExtensions/n:ReplaceNode/n:String/text()"
XmlFileName="$(MSBuildProjectFullPath)"
Value="$(NewValue)" />
</Target>
</Project>

How can I get MSBUILD to evaluate and print the full path when given a relative path?

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\'
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.
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.
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>
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.
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>