get list of subdirectories in msbuild - msbuild

Given a list of directories:
<ItemGroup>
<Dirs Include="Foo\Dir1" />
<Dirs Include="Foo\Dir2" />
</ItemGroup>
How can I get a list of all subdirectories.
Transforming this list with "$(Identity)\**" does not match anything and transforming with "$(Identity)\**\*" and then with RelativeDir yields only directories that contain files.
Currently I have to resort to C#:
<UsingTask TaskName="GetSubdirectories" TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Directories ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<SubDirectories ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs"><![CDATA[
var result = new List<ITaskItem>();
foreach (var dirItem in Directories) {
foreach (var dir in Directory.GetDirectories(dirItem.ItemSpec, "*", SearchOption.AllDirectories)) {
if (dir.Contains(#"\.svn\") || dir.EndsWith(#"\.svn")) continue;
result.Add(new TaskItem(dir));
}
}
SubDirectories = result.ToArray();
]]></Code>
</Task>
</UsingTask>
<GetSubdirectories Directories="#(Dirs)">
<Output TaskParameter="SubDirectories" ItemName="SubDirs" />
</GetSubdirectories>
But I would like to know if there is an easier way.

Excerpted from the book "MSBuild Trickery":
<Import Project="EnableAllPropertyFunctions.tasks" />
<Target Name="GetSubdirectories">
<ItemGroup>
<Dirs Include="$([System.IO.Directory]::
EnumerateDirectories(
`.\Foo`,
`*`,
System.IO.SearchOption.AllDirectories))"
/>
</ItemGroup>
<Message Text="%(Dirs.Identity)" />
</Target>
You'll need to first enable the extended property function set by ensuring that the environment variable MSBuildEnableAllPropertyFunctions is set to the value 1 (that is what the imported .tasks file accomplishes, with an inline task).
Once #(Dirs) is set up, you can filter it with the Remove attribute to get rid of the Subversion folders.

<CreateItem Include="$(OutputFolder)\*\*.*">
<Output TaskParameter="Include" ItemName="FilesInSubFolders" />
</CreateItem>
<RemoveDuplicates Inputs="#(FilesInSubFolders->'%(RelativeDir)')">
<Output TaskParameter="Filtered" ItemName="SubDirs"/>
</RemoveDuplicates>
<Message Text="#(SubDirs)"/>
This will put all the immediate subfolder paths into #(SubDirs). If you change Include="$(OutputFolder)\*\*.*" to Include="$(OutputFolder)\**\*.*", it'll include all subfolders recursively.

To expand on Brian's answer with a fully self-contained example:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="GetSubdirectories">
<UsingTask TaskName="SetEnvironmentVariable"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v$(MSBuildToolsVersion).dll">
<ParameterGroup>
<Name ParameterType="System.String" Required="true" />
<Value ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Code Type="Fragment" Language="cs"><![CDATA[
Environment.SetEnvironmentVariable(Name, Value);
]]></Code>
</Task>
</UsingTask>
<Target Name="GetSubdirectories">
<SetEnvironmentVariable Name="MSBuildEnableAllPropertyFunctions" Value="1" />
<ItemGroup>
<Dirs Include="$([System.IO.Directory]::EnumerateFiles('.\Stuff', '*', System.IO.SearchOption.AllDirectories))"/>
</ItemGroup>
<Message Text="%(Dirs.Identity)" />
</Target>
</Project>
I got the UsingTask example from this answer.

Related

MSBuild: How Do I Calculate a File Checksum Using MSBuild?

How Do I Calculate a File Checksum Using MSBuild?
I don't want to take a dependency on MSBuild extensions.
One option is to use C# code within the project file.
The sample shows how to pass parameter to and from C# code.
<?xml version="1.0" encoding="utf-8"?>
<Project>
<UsingTask TaskName="GenerateHash" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<InputFile ParameterType="System.String" Required="true" />
<Checksum ParameterType="System.String" Output="true" />
</ParameterGroup>
<Task>
<Using Namespace="System.IO" />
<Using Namespace="System.Security.Cryptography" />
<Code Type="Fragment" Language="cs">
<![CDATA[
using (var md5 = MD5.Create())
{
using (var stream = new FileStream(InputFile, FileMode.Open))
{
byte[] bytes = md5.ComputeHash(stream);
// Convert byte array to a string
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
builder.Append(bytes[i].ToString("x2"));
}
Checksum = builder.ToString();
}
}
]]>
</Code>
</Task>
</UsingTask>
<Target Name="GenerateManifestJson" AfterTargets="PostBuild" >
<GenerateHash InputFile="$(FileName)">
<Output
TaskParameter="Checksum"
PropertyName="ArtifactChecksum" />
</GenerateHash>
<ItemGroup>
<ManifestJson Include="{" />
<ManifestJson Include="%20"checksum":"$(ArtifactChecksum)"" />
<ManifestJson Include="}" />
</ItemGroup>
<WriteLinesToFile File="c:\temp\hello.manifest.json" Lines="#(ManifestJson)" Overwrite="true" Encoding="utf-8" />
</Target>
</Project>
This has been added in MSBuild v16, see GetFileHash Task.
Example:
<Project>
<ItemGroup>
<FilesToHash Include="$(MSBuildThisFileDirectory)\*" />
</ItemGroup>
<Target Name="GetHash">
<GetFileHash Files="#(FilesToHash)">
<Output
TaskParameter="Items"
ItemName="FilesWithHashes" />
</GetFileHash>
<Message Importance="High"
Text="#(FilesWithHashes->'%(Identity): %(FileHash)')" />
</Target>
</Project>

how to use the absolute path in the msbuild config file(*.proj)?

the relative content in msbuild.proj file:
<Message Text="*****check the site exists*****" Importance="high"/>
<Exec Command="C:\WINDOWS\System32\inetsrv\appcmd.exe list site /name:$(WebAppSiteName) " ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="ErrorCode2" />
</Exec>
<Message Text="*****if not exists create site*****" Importance="high" Condition="'$(ErrorCode2)' > '0'" />
<Exec Command="C:\WINDOWS\System32\inetsrv\appcmd.exe add site /name:$(WebAppSiteName) /bindings:http/*:80:$(SiteDomain) /applicationDefaults.applicationPool:$(WebAppSiteName) /physicalPath:$(BuildSolutionDir)$(DeployDir)Website" Condition="$(WebAppSiteName)!='' and '$(ErrorCode2)' > '0'"></Exec>
in above: the actual parameter(/physicalPath) value of runtime is :
D:\YDJWebsite.Dev\deply\fw\..\..\mkltest2Website
actually, the path of above is equivalent to D:\YDJWebsite.Dev\mkltest2Website
when accessing the site, the error shows:
500 - internal server error,
resource not found
if i change the path to right format "D:\YDJWebsite.Dev\mkltest2Website", the error disappeared
the physical path of website in iis shows here.
now, i know the only solution is to convert the paramter value(/physicalPath) to right format?
but how to do it in proj file? is there any suggestion ? tks.
i have solved the problem with the msbuild's inline task.
<UsingTask TaskName="AbsolutePath" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<Path ParameterType="System.String" Required="true" />
<FullPath ParameterType="System.String" Output="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Text.RegularExpressions" />
<Using Namespace="Microsoft.Build.Framework" />
<Using Namespace="Microsoft.Build.Utilities" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
if(string.IsNullOrWhiteSpace(Path))
{
FullPath = "";
}
else
{
DirectoryInfo dirInfo = new DirectoryInfo(Path);
FullPath = dirInfo.FullName;
}
}
catch (Exception ex) {
FullPath = "";
}
]]>
</Code>
</Task>
<AbsolutePath Path="$(BuildSolutionDir)$(DeployDir)Website">
<Output TaskParameter="FullPath" PropertyName="FullPath" />
</AbsolutePath>
<Message Text="$(FullPath)"/>
tks.

Check if Targets exists with MSBuild task

Here is my code:
<MSBuild Projects="outs.proj" Targets="Build;MyCustomTarget">
</MSBuild>
I need to check if MyCustomTarget exists in outs.proj before executing.
It throws an error when MyCustomTarget is not imported, so depending on the result use either Build or Build+MyCustomTarget.
Thanks in advance for your help.
Getting a list of targets is cumbersome, you can either reflect on TaskHost via BuildEngine to get current Project or re-evaluate the project with an inline task.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="Targets" TaskFactory="CodeTaskFactory" AssemblyName="Microsoft.Build.Tasks.v12.0">
<ParameterGroup>
<Project ParameterType="System.String" Required="true" />
<All ParameterType="System.String[]" Output="true" />
<Run ParameterType="System.String[]" Output="true" />
</ParameterGroup>
<Task>
<Reference Include="Microsoft.Build" />
<Reference Include="System.Xml" />
<Code>
All = new Microsoft.Build.Evaluation.Project(Project).Targets.Select(t => t.Key).ToArray();
Run = Run.Where(All.Contains).ToArray();
</Code>
</Task>
</UsingTask>
<Target Name="Foo">
<Targets Project="$(MSBuildProjectFullPath)" Run="Foo;Baz">
<Output TaskParameter="All" ItemName="All" />
<Output TaskParameter="Run" ItemName="Run" />
</Targets>
<Message Text="All Targets: #(All)" />
<Message Text="Run Targets: #(Run)" />
</Target>
<Target Name="Bar" />
</Project>
Edit:
You don't provide much details so I can't help with your particular issue, but if new Project(Project) throws may be try ProjectCollection.GlobalProjectCollection.LoadProject(Project) instead to get to the Targets; the same collection has the LoadedProjects property as well as GetLoadedProjects and UnloadProject methods to play around with to get around your exception. If you're in control of the project file and it's flat with no Import you might want to try parsing it as a simple XML file rather than a fully fledged MSBuild project.
<XmlPeek XmlInputPath="$(MSBuildProjectFullPath)" Query="/p:Project/p:Target/#Name" Namespaces="<Namespace Prefix='p' Uri='http://schemas.microsoft.com/developer/msbuild/2003' />">
<Output TaskParameter="Result" ItemName="All" />
</XmlPeek>
<ItemGroup>
<In Include="Foo;Baz" />
<Out Include="#(In)" Exclude="#(All)" />
<Run Include="#(In)" Exclude="#(Out)" />
</ItemGroup>
<Message Text"#(Run)" />
In either case, you pass outs.proj path to whichever method you choose go with and get back #(All) with all of the targets in that project (Foo;Bar) then you filter your targets down from Foo;Baz to just Foo since Baz doesn't exit in #(All). Then you do whatever you want to do with this information, e.g. <MSBuild Projects="outs.proj" Targets="Build;#(Run)">.

How can I insert lines into a file in an MSBuild Task?

I'm trying to insert some text into a file on the second line of the text. I've currently got it inserting the text at the top by using ReadLinesFromFile. Is there a way to break the list I get back from that into 2 pieces so I can insert on the second line?
What I have now:
<Target>
<ReadLinesFromFile File="targetfile.txt">
<Output TaskParameter="Lines" ItemName="TargetFileContents"/>
</ReadLinesFromFile>
<WriteLinesToFile File="targetfile.txt" Lines="$(TextToInsert)" Overwrite="true"/>
<WriteLinesToFile File="targetfile.txt" Lines="#(TargetFileContents)" Overwrite="false"/>
</Target>
It's a bit of a sledge hammer with all the scaffolding, but you can write a task into a project file (or included file, which often has the .targets extension):
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTarget="InsertLine">
<Target Name="InsertLine">
<InsertIntoFile FilePath="test.txt" LineNumber="999" Text="Test complete" />
<InsertIntoFile FilePath="test.txt" LineNumber="1" Text="2" />
<InsertIntoFile FilePath="test.txt" LineNumber="2" Text="3" />
<InsertIntoFile FilePath="test.txt" LineNumber="1" Text="1" />
<InsertIntoFile FilePath="test.txt" LineNumber="1" Text="Testing the 2MC" />
</Target>
<UsingTask
TaskName="InsertIntoFile"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<FilePath ParameterType="System.String" Required="true" />
<LineNumber ParameterType="System.Int32" Required="true" />
<Text ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Code Type="Fragment" Language="cs">
<![CDATA[
// By tradition, text file line numbering is 1-based
var lines = File.Exists(FilePath)
? File.ReadAllLines(FilePath).ToList()
: new List<String>(1);
lines.Insert(Math.Min(LineNumber - 1, lines.Count), Text);
File.WriteAllLines(FilePath, lines);
return true;
]]>
</Code>
</Task>
</UsingTask>
</Project>

MSbuild, How to access project property value in Inline Task code?

I have inline Task code written in csharp
I wonder if there is anyway to access propect property in inline Task code
For ex. I am trying to replace string match with project property value. is it possible?
<![CDATA[
MatchCollection matches = Regex.Matches(SourceStr, Pattern);
for (int i = 0; i < matches.Count; i++)
// replace the match value with project property... possible?
]]>
Pass it as a parameter, like you would with a compiled task?
<ParameterGroup>
<Foo ParameterType="System.Bar" Required="true" />
<ParameterGroup>
Edit: Looks like simple inline tokens work too.
<![CDATA[
Console.WriteLine("$(Foo)");
]]>
Too big to reply as comment.
Reason it's empty is as in your other question -- evaluation order. The code is evaluated and tokenized prior to hitting the target, I assume that's where you set the value, and setting the property.
If I understood what you're trying to achieve with the task, have a look below at an example, just pass them in as properties and out as output.
That said, I think you should look into Property Functions first, save youself a bunch of trouble.
http://msdn.microsoft.com/en-us/library/dd633440.aspx
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="Foo" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<SourceStr Required="true" Output="true" />
<Pattern Required="true" />
<Value Required="true" />
<Macros ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
</ParameterGroup>
<Task>
<Using Namespace="System.Linq" />
<Using Namespace="System.Text.RegularExpressions" />
<Code Type="Fragment" Language="cs">
<![CDATA[
var regex = new Regex(Pattern);
var matches = regex.Matches(SourceStr).Cast<Match>().Select(m => m.Value).ToList();
matches.ForEach(m => Log.LogMessage("{0} -> {1}", m, Value));
Macros = matches.Select(m => new TaskItem(m)).ToArray();
SourceStr = regex.Replace(SourceStr, Value);
]]>
</Code>
</Task>
</UsingTask>
<Target Name="Foo">
<PropertyGroup>
<SourceStr>Bar</SourceStr>
<Value>z</Value>
</PropertyGroup>
<Message Text="in $(SourceStr)" />
<Foo SourceStr="$(SourceStr)" Pattern="r$" Value="$(Value)">
<Output TaskParameter="SourceStr" PropertyName="SourceStr" />
<Output TaskParameter="Macros" ItemName="Macros" />
</Foo>
<Message Text="out $(SourceStr)" />
<Message Text="sans %(Macros.Identity)" />
</Target>
</Project>
There are examples in the MSDN documentation. For example:
<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="4.0">
<UsingTask TaskName="TokenReplace" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Path ParameterType="System.String" Required="true" />
<Token ParameterType="System.String" Required="true" />
<Replacement ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs">
<![CDATA[
string content = File.ReadAllText(Path);
content = content.Replace(Token, Replacement);
File.WriteAllText(Path, content);
]]>
</Code>
</Task>
</UsingTask>
<Target Name='Demo' >
<TokenReplace Path="C:\Project\Target.config" Token="$MyToken$" Replacement="MyValue"/>
</Target>
</Project>
If $(MyValue) was a project property you could do something like:
<TokenReplace Path="C:\Project\Target.config" Token="$MyToken$" Replacement="$(MyValue)"/>