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

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)).

Related

MSBuildWorkspace cannot compile project with <ProjectReference> to another project

Note I've also asked this question (with reproduction code) on Roslyn's GitHub
Create a new solution with 2 projects (CoreLibrary and DownstreamLibrary).
Add a project reference from DownstreamLibrary to CoreLibrary.
Run the following code and note that DownstreamLibrary no longer compiles.
Note that I've tried net461, netcoreapp2.1 and netstandard2.0 as target frameworks for the projects - same problem each time.
static async Task Main(string[] args)
{
MSBuildLocator.RegisterDefaults();
using (var workspace = MSBuildWorkspace.Create())
{
workspace.WorkspaceFailed += (sender, workspaceFailedArgs) => WriteLine(workspaceFailedArgs.Diagnostic.Message);
var solution = await workspace.OpenSolutionAsync(#"c:\source\ForRoslynTest\ForRoslynTest.sln");
WriteLine($"Loaded solution {solution.FilePath}");
var projectTree = workspace.CurrentSolution.GetProjectDependencyGraph();
foreach (var projectId in projectTree.GetTopologicallySortedProjects())
{
await CompileProject(workspace.CurrentSolution.GetProject(projectId));
}
}
}
private static async Task CompileProject(Project project)
{
WriteLine($"Compiling {project.Name}. It has {project.MetadataReferences.Count} metadata references.");
var compilation = await project.GetCompilationAsync();
var errors = compilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error);
if (errors.Any())
{
WriteLine($"COMPILATION ERROR: {compilation.AssemblyName}: {errors.Count()} compilation errors: \n\t{string.Join("\n\t", errors.Where(e => false).Select(e => e.ToString()))}");
}
else
{
WriteLine($"Project {project.Name} compiled with no errors");
}
}
You will receive the following output:
Msbuild failed when processing the file 'c:\source\ForRoslynTest\DownstreamLibrary\DownstreamLibrary.csproj' with message: C:\Program Files\dotnet\sdk\2.1.602\Microsoft.Common.CurrentVersion.targets: (1548, 5): The "Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput" task could not be loaded from the assembly Microsoft.Build.Tasks.Core, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a. Confirm that the <UsingTask> declaration is correct, that the assembly and all its dependencies are available, and that the task contains a public class that implements Microsoft.Build.Framework.ITask.
Found project reference without a matching metadata reference: c:\source\ForRoslynTest\CoreLibrary\CoreLibrary.csproj
Loaded solution c:\source\ForRoslynTest\ForRoslynTest.sln
Compiling CoreLibrary. It has 113 metadata references.
Project CoreLibrary compiled with no errors
Compiling DownstreamLibrary. It has 0 metadata references.
COMPILATION ERROR: DownstreamLibrary: 3 compilation errors:
c:\source\ForRoslynTest\DownstreamLibrary\Class1.cs(1,7): error CS0246: The type or namespace name 'System' could not be found (are you missing a using directive or an assembly reference?)
c:\source\ForRoslynTest\DownstreamLibrary\Class1.cs(5,18): error CS0518: Predefined type 'System.Object' is not defined or imported
c:\source\ForRoslynTest\DownstreamLibrary\Class1.cs(5,18): error CS1729: 'object' does not contain a constructor that takes 0 arguments
So my question is how do I fix the above errors and get DownstreamLibrary to compile?
EDIT
I'm 99% sure the underlying cause is this error
The "Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput" task could not be loaded from the assembly Microsoft.Build.Tasks.Core, Version=15.1.0.0
I've confirmed with procmon that it's loading the following DLL (C:\Program Files\dotnet\sdk\2.1.602\Microsoft.Build.Tasks.Core.dll) which I've confirmed with ILSpy DOESN'T have the ResolveNonMSBuildProjectOutput task in it. Older versions of this DLL did have this task.
I found a manual workaround to this WorkspaceFailed error:
[Failure] Msbuild failed when processing the with message: The "Microsoft.Build.Tasks.ResolveNonMSBuildProjectOutput" task could not be loaded from the assembly Microsoft.Build.Tasks.Core, Version=15.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a. Confirm that the declaration is correct, that the assembly and all its dependencies are available, and that the task contains a public class that implements Microsoft.Build.Framework.ITask.
There were a few resolution steps:
following https://github.com/microsoft/msbuild/issues/4770 I updated VS 2019 from 16.2 to 16.5.3... This didn't fix my bug, but it's worth documenting I did this.
I upgraded my Microsoft.Build.* and Microsoft.CodeAnalysis dependencies to latest, THIS DIDN'T FIX THINGS YET, same bug.
I navigated to C:\Program Files\dotnet\sdk which previously had a few directories:
1.0.0/ 1.0.0-rc4-004771/ 2.0.3/ 2.1.505/
1.0.0-preview1-002702/ 1.0.3/ 2.1.202/ 3.1.100/
1.0.0-preview2-003121/ 1.0.4/ 2.1.4/ 3.1.201/
1.0.0-preview4-004233/ 1.1.0/ 2.1.502/ NuGetFallbackFolder/
I renamed this to sdk_o and created a new folder C:\Program Files\dotnet\sdk, copying in only 3.1.201/
Success! This error was gone, but running my roslyn app then resulted in some error like (paths stripped)
[Failure] Msbuild failed when processing the file with message The "ProcessFrameworkReferences" task failed unexpectedly. System.IO.FileNotFoundException: Could not load file or assembly 'NuGet.Frameworks, Version=5.5.0.4, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. The system cannot find the file specified. File name: 'NuGet.Frameworks, Version=5.5.0.4, Culture=neutral, PublicKeyToken=31bf3856ad364e35'
Solved this one by adding NuGet.ProjectModel 5.5.1 to my project, the csproj now has the following package references:
<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="16.5.0" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Framework" Version="16.5.0" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.Build.Locator" Version="1.2.6" />
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="16.5.0" ExcludeAssets="runtime" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.5.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.5.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="3.5.0" />
<PackageReference Include="NuGet.ProjectModel" Version="5.5.1" />
</ItemGroup>
No more WorkspaceFailed events for this code:
Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults();
var workspace = MSBuildWorkspace.Create();
workspace.WorkspaceFailed += (s, e) => { Console.WriteLine(e.Diagnostic); };
var project = await workspace.OpenProjectAsync(#"C:/path/to.csproj");
The csproj I'm loading with Roslyn looks as follows:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<OutputType>Library</OutputType>
<ApplicationIcon />
<StartupObject />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\dependencies\some.csproj" />
</ItemGroup>
</Project>

XML Documentation file is not included in publish folder

I've selected "Xml documentation file" in my ASP.NET Core MVC application and it displays "bin\Debug\net452\MyProject.xml" as output folder. The problem is that this file doesn't exist in publish folder. Do I need to someting additional to include it? Using .NET Core 1.0-RC4 and VS.NET 2017 RC4 (new csproject format).
If you're using project.json then you can control the files and folders that are both included and excluded by the publish process:
"publishOptions": {
"include": [
"wwwroot",
"appsettings.json",
"appsettings.*.json",
"web.config"
],
"exclude": [
"wwwroot/less"
]
}
For .csproj based projects here is a good resource for replicating old project.json settings in XML, for example:
<ItemGroup>
<Compile Include="..\Shared\*.cs" Exclude="..\Shared\Not\*.cs" />
<EmbeddedResource Include="..\Shared\*.resx" />
<Content Include="Views\**\*" PackagePath="%(Identity)" />
<None Include="some/path/in/project.txt" Pack="true" PackagePath="in/package.txt" />
<None Include="notes.txt" CopyToOutputDirectory="Always" />
<!-- CopyToOutputDirectory = { Always, PreserveNewest, Never } -->
<Content Include="files\**\*" CopyToPublishDirectory="PreserveNewest" />
<None Include="publishnotes.txt" CopyToPublishDirectory="Always" />
<!-- CopyToPublishDirectory = { Always, PreserveNewest, Never } -->
</ItemGroup>
Apparently this is not implemented in with dotnet publish but will be in the near future: https://github.com/dotnet/sdk/issues/795
The project.json file should have the "xmlDoc": true under "buildOptions":
{
"buildOptions": {
"xmlDoc": true
}
}
If you have documentation in your code like this:
/// <summary>
/// Documentation for FooClass
/// </summary>
public static class FooClass
{
// ...
}
Then you should have an xml file in your output that looks like this:
<doc>
<assembly>
<name>Foo.Bar</name>
</assembly>
<members>
<member name="T:Foo.Bar.FooClass">
<summary>
Documentation for FooClass
</summary>
</member>
</members>
</doc>
Tested on 1.1.2
ResolvedFileToPublish is the Item that publish uses to know which files to put in the publish folder. The Include is the file's source, and the RelativePath is where inside the publish folder the file should be placed.
ComputeFilesToPublish is exactly as its name implies - it is the Target that gathers all the files to be published.
<Target Name="CopyDocumentationFile"
AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<ResolvedFileToPublish Include="#(FinalDocFile)"
RelativePath="#(FinalDocFile->'%(Filename)%(Extension)')" />
</ItemGroup>
</Target>
Just add this target to your .csproj and make sure that
GenerateDocumentationFile is set to true
<GenerateDocumentationFile>true</GenerateDocumentationFile>
https://github.com/dotnet/sdk/issues/795#issuecomment-289782712
In Asp.net Core, if you are having trouble setting DocumentationFile to work, we can do that by setting GenerateDocumentationFile property in .csproj:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

Processing batch items in parallel

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);
//...
}

MSBuild UsingTask Resolve References

I feel like I've fixed this before, but I can't remember how.
I have a tasks file that looks like this (CustomTasks.tasks):
<UsingTask AssemblyFile="CustomTasks.dll" TaskName="MyCustomTask"/>
it references an assembly (namely Ionic.Zip.dll). Ionic.Zip.dll is not in the GAC (and I don't want it to be). It sits right next to my CustomTasks.dll.
I have a directory called MSBuild one level up from my sln file which has CustomTasks.tasks, CustomTasks.dll and Ionic.Zip.dll in it.
I have a csproj that references the tasks file and calls the custom task:
<Import Project="$(ProjectDir)\..\MSBuild\CustomTasks.tasks" />
<MyCustomTask ..... />
at build time, this yields:
The "MyCustomTask" task could not be loaded from the assembly ....MyCustomTasks.dll. Could not load file or assembly 'Ionic.Zip,......' or one of its dependencies.
Got tired and frustrated and took a direct approach...I don't think this is the same way I solved the problem previously...but maybe this will help someone else. Other, more elegant solutions are more than welcome.
<Target Name="BeforeBeforeBuild" BeforeTargets="BeforeBuild">
<HandleAssemblyResolve SearchPath="$(ProjectDir)\..\MSBuild\" />
</Target>
<UsingTask TaskName="HandleAssemblyResolve" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<SearchPath ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Reflection" />
<Code Type="Fragment" Language="cs">
<![CDATA[
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
{
var assemblySearchPath = Path.Combine(SearchPath, e.Name.Split(',')[0]);
if (File.Exists(assemblySearchPath)) return Assembly.LoadFrom(assemblySearchPath);
return null;
};
]]>
</Code>
</Task>
</UsingTask>
This is actually easy to fix. Put your custom build tasks and dependencies in a different folder. Then dependencies are loaded correctly.
For example like so:
<UsingTask AssemblyFile="..\BuildTools\CustomTasks.dll" TaskName="MyCustomTask"/>

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>