MsBuild- Delete files from a directory based on time modified / name - build

I have a list of files generated on each build inside a directory C:\BuildArtifacts
The Contents of a directory looks like this:
TestBuild-1.0.0.1.zip
TestBuild-1.0.0.2.zip
TestBuild-1.0.0.3.zip
TestBuild-1.0.0.4.zip
TestBuild-1.0.0.5.zip
TestBuild-1.0.0.6.zip
Now, with each incremental build, I just want to retain two recent artifacts and delete the rest. So, in this example, I want to retain TestBuild-1.0.0.5.zip and TestBuild-1.0.0.6.zip
How can I do it with MSBuild?
Note:
I have managed to fetch the above list in an item
<Exec WorkingDirectory="$(Artifacts)\.." Command="dir /B /A:-D /O:-N" Outputs="ArchiveFileList" />

Well, we wrote a custom task to sort the files by their name and then output the list of the files to delete (excluding the first two in the list) into an Item
Custom Task:
<UsingTask
TaskName="Cleanup"
TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll" >
<ParameterGroup>
<TargetPath ParameterType="System.String" Required="true"/>
<BackupLength ParameterType="System.Int32" Required="true"/>
<FilesToExclude ParameterType="System.String[]" Output="true" />
<FilesToDelete ParameterType="System.String[]" Output="true" />
</ParameterGroup>
<Task>
<Using Namespace="System.IO" />
<Using Namespace="System.Linq" />
<Code Type="Fragment" Language="cs">
<![CDATA[
var diInfo = new DirectoryInfo(TargetPath);
if (diInfo.Exists)
{
var fiInfo = diInfo.GetFiles().OrderByDescending(file => file.Name);
FilesToExclude = fiInfo.Take(BackupLength).Select(file => file.FullName).ToArray();
FilesToDelete = fiInfo.Skip(BackupLength).Select(file => file.FullName).ToArray();
}
]]>
</Code>
</Task>
</UsingTask>
Usage:
<!-- Clean old archives. Keep the recent two and deletes rest. -->
<Cleanup TargetPath="$(PackageRoot)" BackupLength="2">
<Output TaskParameter="FilesToDelete" ItemName="FilesToClean" />
</Cleanup>
<Message Text="Cleaning Old Archives" Importance="High" />
<Delete Files="#(FilesToClean)" />

Please test this command then <exec ...> it with del instead of echo:
for /f %x in ('cmd /c "dir /B /A-D /O-N | more +2"') do echo %x

Related

How to list the two most recent folders inside a directory using their timestamp

I have a parent folder and inside that I have a few folders. For an automation, I want to take the latest of the two folders according to timestamp.
I have tried to take the latest folder by using timstampselector.
<timestampselector property="latest.modified">
<path>
<dirset dir="MyDirectoryPath">
<include name="*" />
</dirset>
</path>
</timestampselector>
Inside my parent folder, I have the following folders:
test (Last modified on 07/04/2019 10:30 AM)
check (Last modified on 08/04/2019 05:00 PM)
integrate (Last modified on 08/04/2019 12:30 PM)
slave (Last modified on 09/04/2019 05:00 PM)
Our script should take the latest two modified folders, which is in the above case it should be integrate & slave.
How can I achieve that?
Generally speaking, it's a good idea to stay away from ant-contrib whenever possible. This particular problem can be quickly solved with native Ant's resource collections:
<last count="2" id="latest.two.files">
<sort>
<date />
<fileset dir="MyDirectoryPath" />
</sort>
</last>
Full example target:
<target name="select-latest">
<delete dir="testdir" />
<mkdir dir="testdir" />
<touch file="testdir/test" datetime="07/04/2019 10:30 AM" />
<touch file="testdir/check" datetime="08/04/2019 05:00 PM" />
<touch file="testdir/integrate" datetime="08/04/2019 12:30 PM" />
<touch file="testdir/slave" datetime="09/04/2019 05:00 PM" />
<last count="2" id="latest.two.files">
<sort>
<date />
<fileset dir="testdir" />
</sort>
</last>
<echo message="${toString:latest.two.files}" />
</target>
The task you are using is part of Ant-Contrib rather than core Ant. The documentation says you can use the count attribute to say how many items you want to select. In your case, set it to two:
<timestampselector property="latest.modified" count="2">
<path>
<dirset dir="MyDirectoryPath">
<include name="*" />
</dirset>
</path>
</timestampselector>
This appeared to work fine for me: the property was set to a comma-separated list of two directories.

Rename a file before Copy Task in ant build

I am new to ant build files.
Currently I get a list of files for build as:
a.cls
b.cls
c.cls
but in my local I have to run build on files, in the same directory:
a-meta.cls
b-meta.cls
c-meta.cls
Here meta keyword stays consistent. And I am using the following build.xml file. I am not sure how can I rename filename before actually copying them. I tried replace, mapper and other antlib tasks. But not helpful.
<project name="test" default="compile">
<taskdef resource="net/sf/antcontrib/antlib.xml">
<classpath>
<pathelement location="lib/ant-contrib-1.0b3.jar"/>
</classpath>
</taskdef>
<loadfile property="file" srcfile="filesToMove.txt"/> <!-- these are the list of files, i mentioned earlier -->
<target name="compile">
<echo>${file}</echo> <!-- here i have to rename file name to include -meta -->
<copy file="./classes/${file}" tofile="./src/classes/${file}" overwrite="true"/>
</target>
</project>
How to rename the files before moving them.
The solution to it was replacing the .cls to find only the name and then append the -meta.html. As follows (some portion is changed compared to previous version in the question)
<project name="test" default="compile">
<taskdef resource="net/sf/antcontrib/antlib.xml">
<classpath>
<pathelement location="lib/ant-contrib-1.0b3.jar"/>
</classpath>
</taskdef>
<loadfile property="file" srcfile="filesToMove.txt"/> <!-- these are the list of files, i mentioned earlier -->
<target name="compile">
<echo>${file}</echo> <!-- here i have to rename file name to include -meta -->
<copy file="./classes/${file}" tofile="./src/classes/${file}" overwrite="true"/>
<for param="file">
<path>
<fileset dir="./" includes="*.cls"/>
</path>
<sequential>
<basename file="#{file}" property="#{file}" suffix=".md"/>
<echo message=" ${#{file}}"/>
<copy file="${#{file}}-meta.cls" toDir="test"/>
</sequential>
</for>
</target>
</project>

How can one modify an ItemDefinitionGroup from an MSBuild target?

I have an msbuild script I wrote to compile Google Protocol Buffers files:
<ItemGroup>
<ProtocolBuffer Include="Whitelist.proto" />
<ProtocolBuffer Include="Whitelist2.proto" />
</ItemGroup>
<ItemDefinitionGroup>
<ProtocolBuffer>
<ProtoPath>$(ProjectDir)</ProtoPath>
</ProtocolBuffer>
</ItemDefinitionGroup>
<PropertyGroup>
<ProtoC>$([System.IO.Path]::GetFullPath($(ProjectDir)..\ThirdParty\protobuf-2.4.1\protoc.exe))</ProtoC>
<ProtoOutPath>$(IntDir)CompiledProtocolBuffers</ProtoOutPath>
</PropertyGroup>
<Target Name="CompileProtocolBuffers"
BeforeTargets="ClCompile"
Inputs="#(ProtocolBuffer)"
Outputs="#(ProtocolBuffer->'$(ProtoOutPath)\%(FileName).pb.cc');#(ProtocolBuffer->'$(ProtoOutPath)\%(FileName).pb.h')">
<MakeDir Directories="$(ProtoOutPath)" />
<Exec
Command=""$(ProtoC)" --proto_path="$([System.IO.Path]::GetDirectoryName(%(ProtocolBuffer.ProtoPath)))" --cpp_out="$(ProtoOutPath)" "%(ProtocolBuffer.FullPath)" --error_format=msvs"
/>
<ItemGroup>
<ClInclude Include="$(ProtoOutPath)\%(ProtocolBuffer.FileName).pb.h" />
<ClCompile Include="$(ProtoOutPath)\%(ProtocolBuffer.FileName).pb.cc">
<AdditionalIncludeDirectories>$(MSBuildThisDirectory)..\ThirdParty\protobuf-2.4.1\src</AdditionalIncludeDirectories>
<PrecompiledHeader></PrecompiledHeader>
<DisableSpecificWarnings>4244;4276;4018;4355;4800;4251;4996;4146;4305</DisableSpecificWarnings>
<PreprocessorDefinitions>GOOGLE_PROTOBUF_NO_RTTI</PreprocessorDefinitions>
<WarningLevel>Level3</WarningLevel>
</ClCompile>
</ItemGroup>
</Target>
This compiles the protocol buffers files perfectly, and adds them to the compiler's inputs (yay!). However, my other source files that want to include the .pb.h files need to know where these files got generated -- that generation location needs to be put on the include path.
Therefore, if and only if the user has included a <ProtocolBuffer item somewhere in their script, I want to add the generation location (in this case $(ProtoOutPath) to ClCompile's <AdditionalIncludeDirectories>.
Is that possible or do I need to make .cpp files that want to use these generated bits jump through hoops?
Read your question and thought "can't be that hard". Man, was I wrong. First I thought just putting a condition on it, but of course one can't use ItemGroups in toplevel conditions because of evaluation order. Then I figured it's also not possible to put an ItemDefinitionGroup in a target (cause there one can use conditions) and modify it there. Then I bonked my head on the keyboard a couple of times after I realized that's probably why you asked the question :] (btw you know including a nonexisting directory is not really a problem since the compiler will happily ignore it?)
Maybe there's a simpler solution, but lastly I figured: if nothing works, my favourite msbuild toy aka CodeTaskFactory must be able to fix it. It does (I hope, didn't fully test the result), but it's not straightforward at all. Here you go, make sure to invoke the Test target somewhere before the C++ build starts.
<!--Uncomment the below to define some ProtocolBuffers-->
<!--<ItemGroup>
<ProtocolBuffer Include="Whitelist.proto" />
<ProtocolBuffer Include="Whitelist2.proto" />
</ItemGroup>-->
<!--Suppose these are your default include files defined in your C++ project-->
<ItemDefinitionGroup Label="DefaultIncludes">
<ClCompile>
<AdditionalIncludeDirectories>/path/to/x;/path/to/y</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<!--Include at least one item so we can play with it-->
<ItemGroup>
<ClCompile Include="iamaninclude"/>
</ItemGroup>
<!--Use code to append to AdditionalIncludeDirectories-->
<UsingTask TaskName="AppendMetadata" TaskFactory="CodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Append ParameterType="System.String" Required="true"/>
<ItemList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true"/>
<OutputItemList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />
</ParameterGroup>
<Task>
<Code>
<![CDATA[
const string dirz = "AdditionalIncludeDirectories";
foreach( var item in ItemList )
{
var cur = item.GetMetadata( dirz );
item.SetMetadata( dirz, cur + ";" + Append );
}
OutputItemList = ItemList;
]]>
</Code>
</Task>
</UsingTask>
<!--Main target-->
<Target Name="Test">
<!--stage 1: copy the itemgroup, then clear it:
if an Output TaskParameter is an Itemgroup, apparently the content
gets appended to the group instead of replacing it.
Found no documentation about this whatsoever though???-->
<ItemGroup Condition="#(ProtocolBuffer) != ''">
<ClCompileCopy Include="#(ClCompile)"/>
<ClCompile Remove="#(ClCompile)"/>
</ItemGroup>
<!--stage 2: append 'ProtoBufIncludeDir' to AdditionalIncludeDirectories,
and append the result to the origiginal again-->
<AppendMetadata ItemList="#(ClCompileCopy)" Append="ProtoBufIncludeDir" Condition="#(ProtocolBuffer) != ''">
<Output ItemName="ClCompile" TaskParameter="OutputItemList"/>
</AppendMetadata>
<!--stage 3: use modified itemgroup-->
<Message Text="#(ClCompile->'%(Identity): %(AdditionalIncludeDirectories)')"/>
</Target>
This prints
iamaninclude: /path/to/x;/path/to/y
unless the ProtocolBuffer is not empty in which case it prints
iamaninclude: /path/to/x;/path/to/y;ProtoBufIncludeDir

MSBuild RegexMatch not matching

I have the following
<RegexMatch Input="$(Configuration)" Expression="^.*?(?=\.)">
<Output ItemName="Theme" TaskParameter="Output" />
</RegexMatch>
My configuration variable is as follows Theme.Environment
So "Default.Debug"
or "Yellow.Release"
I would like to get the first portion in to a varaible called theme.
I have tested this regex and it works in stand alone regex testers
^.*?(?=\.)
but not when used in my build file.
I am echoing the variable out so that i can see the output
<Exec Command="echo $(Theme)"/>
<Exec Command="echo $(Configuration)"/>
Ideas?
If you should use MSBuild Community tasks for that - check this line: <Output PropertyName="Theme" TaskParameter="Output" />
you should use PropertyName="Theme" if you want to refer it like $(Theme) later.
ItemName will create items set, not property.
But it's much simplier to use MSBuild 4.0 inline functions than Msbuild community tasks for that concrete task. Your code will looks like this (adopt for your script):
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0" DefaultTarget="Play">
<PropertyGroup>
<Configuration>Yellow.Release</Configuration>
</PropertyGroup>
<Target Name="Play">
<PropertyGroup>
<Theme>$([System.Text.RegularExpressions.Regex]::Match($(Configuration), `^.*?(?=\.)`))</Theme>
</PropertyGroup>
<Message Text="$(Theme)" />
<Message Text="$(Configuration)" />
</Target>
</Project>
Just realised that RegexMatch doenst return the matched string but rather returns the entire string if matched.
basically it called IsMatch method not Match method
Have re written as a RegexReplace
<RegexReplace Input="$(Configuration)" Expression="\..*" Replacement="" Count="1">
<Output ItemName="Theme" TaskParameter="Output" />
</RegexReplace>
After that it still wasnt working and then i realised i was doing
$(Theme)
Should have been
#(Theme)

nant script doesn't display unit test details

Can someone please tell me why my build script (nant) doesn't display the unit test details in the command prompt window? I have verbose set to true, but it doesn't want to display any details about my unit tests. Here's the target:
<target name="run-unit-tests" depends="compile, move.assemblies.for.tests, rebuildDatabase">
<mkdir dir="${tests.output.dir}" />
<nunit2 haltonfailure="true" failonerror="true" verbose="true">
<formatter type="Xml" extension=".xml" outputdir="${tests.output.dir}" usefile="true" />
<test assemblyname="${test.assembly.file}" />
</nunit2>
<echo message="Unit Testing Done!" />
</target>
The command prompt window just displays this:
[mkdir] Creating directory 'C:\Projects\TestProject\build\artifacts\UnitTestOutput'.
[echo] Unit Testing Done!
build:
BUILD SUCCEEDED
Am I missing something here?
Thanks!
I found the answer. I looked at the source for CodeCampServer and saw a line
<formatter type="Plain" />
and added it to my build script so it looks like this:
<nunit2 haltonfailure="true" failonerror="true" verbose="true">
<formatter type="Xml" extension=".xml" outputdir="${tests.output.dir}" usefile="true" />
<formatter type="Plain" />
<test assemblyname="${test.assembly.file}" />
</nunit2>
and now it displays the details.
Sorry to ask the question prematurely on here, but at least it might help someone in the future if they have a similar problem.
Is there a log file in ${tests.output.dir} ? If so, what if you set usefile to false and type to "Plain"?