MSBuild 17: how to generate a temporary folder name - msbuild

I have an MSBuild project target which needs to create a zip file from a folder (lets call it FolderA) of files, some of which files need to be excluded and not added to the zip file, so the target needs to copy the files (except for the excluded files) from FolderA to a temporary folder, then call the target ZipDirectory target on the temp folder.
I'm creating the temp folder by creating an itemgroup
<ItemGroup>
<TempStagingFolder Include="$([System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()))" />
</ItemGroup>
but this never evaluates to a folder name, just the static method calls on System.IO.Path
How can I create a temp random folder name in MSBuild to pass to the ZipDirectory target?

Change your code to
<ItemGroup>
<TempStagingFolder Include="$([System.IO.Path]::Combine($([System.IO.Path]::GetTempPath()), $([System.IO.Path]::GetRandomFileName())))" />
</ItemGroup>
Each call static property method call needs to be enclosed in $().
For troubleshooting, maintenance, and for overriding you may find it useful to build up the folder path and name from properties, e.g.:
<PropertyGroup>
<TempStagingPath Condition="'$(TempStagingPath)' == ''">$([System.IO.Path]::GetTempPath())</TempStagingPath>
<TempStagingFolderName Condition="'$(TempStagingFolderName)' == ''">$([System.IO.Path]::GetRandomFileName())</TempStagingFolderName>
<TempStagingFolder Condition="'$(TempStagingFolder)' == ''">$([System.IO.Path]::Combine($(TempStagingPath), $(TempStagingFolderName)))</TempStagingFolder>
</PropertyGroup>
<ItemGroup>
<TempStagingFolder Include="$(TempStagingFolder)" />
</ItemGroup>
Property names and Item names do not collide. $(TempStagingFolder) and #(TempStagingFolder) are different 'objects'.
By having separate properties, if there is an issue you can check the specific property. Testing that the property is not already set, allows for overriding the property with a different value.

Related

MSBuild task to Copy contents of a specific folder by using a wild card in the path

My Folder structure is as follows
Module
-->Asia - 1.0.0.12
------>Deployment
------>Install.exe
------>version.xml
-->Africa - 1.0.3.4
------>Deployment
------>Install.exe
------>version.xml
-->Europe - 2.0.1.2
------>Deployment
------>Install.exe
------>version.xml
I want to copy the 'deployment' folder (and subfolders) under each region to my output directory. The region numbers will change so i cannot hardcode them in my Include statement. The command i am using to copy for Asia region is
<ItemGroup>
<GetFiles Include="$(MSBuildProjectDirectory)\Module\Asia*\Deployment\**\*">
<Destination>D:\Region\Asia</Destination>
</GetFiles >
</ItemGroup>
<Copy
SourceFiles="%(GetFiles.Identity)"
DestinationFolder="%(GetFiles.Destination)\%(RecursiveDir)"
/>
Instead of the Deployment folder and its subdirectories getting copied under the Destination, i am getting the folder structure as
D:\Region\Asia\Asia - 1.0.0.12\Deployment
what i want is
D:\Region\Asia\Deployment
Can this be achieved? Thanks
MSBuild task to Copy contents of a specific folder by using a wild
card in the path
If you use wildcard in msbuild and do a copy task, the MSBuild will always copy the path from the first address where the wildcard is used.
For your situation, since you just use wildcard under Asia*, so it will keep Asia - 1.0.0.12\Deployment folder structure under D:\Region\Asia.
As a suggestion, to get what you want, you need to clearly indicate the name of the Asia folder, even if it is much more complicated, you need to specify one by one.
Use like this:
<Target Name="xxx" BeforeTargets="Build">
<ItemGroup>
<GetFiles Include="$(MSBuildProjectDirectory)\Module\Asia - 1.0.0.12\Deployment\**\*">
<Destination>D:\Region\Asia</Destination>
</GetFiles>
</ItemGroup>
<Copy SourceFiles="%(GetFiles.Identity)"
DestinationFolder="%(GetFiles.Destination)\Deployment\%(RecursiveDir)"/>
</Target>

MSBuild: Filtering Items based on a part of the file path

I have a set of files I need to copy that are underneath a folder that contains the version of the package that deposited those files:
<ItemGroup>
<MyFiles Include="$(MyPackages)\foo.x64*\binaries\*.*"/>
</ItemGroup>
foo.x64* can resolve to more than one folder like foo.x64.17.5.50 and foo.x64.17.6.2 where the suffix represents the version of package. I want to filter my items based on that version but I can't seem to find a way to extract any part of the item file path as metadata on my item to then use it in batching or conditionals.
There are a few features of msbuild that can be used here. those are:
Defining custom item metadata. let's call themFooArch and FooVersion
MSBuild allows calling some .net APIs including regular expressions. So we can use RegEx.Match() here to extract the information you need to know.
When doing numeric comparison on strings, MSBuild parses them into Versions. This will allow you to do range conditions like '%(FooItems.FooVersion)' < '17.6.2'.
What's not possible out of the box is determining the highest version of a list of items. To achieve this, you'll need to write a custom msbuild task.
A more concrete example: To add the required metadata on the items and copy the right version and architecture, you can do the following:
<PropertyGroup>
<FooRegEx>[\\/]foo\.(?<arch>x(\d*))\.(?<version>((\d+)(\.\d+)+))</FooRegEx>
<FooVersionToUse>17.5.50</FooVersionToUse>
</PropertyGroup>
<ItemGroup>
<FooFiles Include="mypkgs\foo*\binaries\**\*">
<FooArch>$([System.Text.RegularExpressions.Regex]::Match(%(Identity), $(FooRegEx)).get_Groups().get_Item("arch"))</FooArch>
<FooVersion>$([System.Text.RegularExpressions.Regex]::Match(%(Identity), $(FooRegEx)).get_Groups().get_Item("version"))</FooVersion>
</FooFiles>
</ItemGroup>
<Target Name="IncludeFoo" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<FooArchToUse Condition="'$(FooArchToUse)' == '' and '$(Platform)' != 'AnyCPU'">$(Platform)</FooArchToUse>
<FooArchToUse Condition="'$(FooArchToUse)' == ''">x64</FooArchToUse>
</PropertyGroup>
<ItemGroup>
<Content Include="#(FooFiles)"
Link="foo\%(Filename)%(Extension)"
CopyToOutputDirectory="PreserveNewest"
Condition="'%(FooFiles.FooVersion)' == '$(FooVersionToUse)' and '%(FooFiles.FooArch)' == '$(FooArchToUse)'"/>
</ItemGroup>
</Target>
This will copy the items of the selected version and platform built for (defaulting to x64) into a foo subfolder of the output directory. (Tested on MSBuild 15.1). I've uploaded the example project to GitHub here.
You can you use the ItemGroup's Condition Attribute to filter out elements based on some boolean expression. You can use regex for instance as per this answer.

How to use MSBuild transform when ItemGroup files all have identical names?

I have a bunch of files x.txt in various directories throughout my project. As part of my build step, I would like to collect these and place them in a single folder (without needing to declare each one). I can detect them by using:
<ItemGroup>
<MyFiles Include="$(SRCROOT)\**\x.txt"/>
</ItemGroup>
However, if I copy these to a single folder - they all overwrite each other. I have tried using a transform where I append a GUID to each file name, but the GUID is only created once, and then re-used for each transform (thus they overwrite each over). Is there a way of generating unique names in MSBuild when copying an ItemGroup with identically named files? The end naming scheme is not important, as long as all the files end up in that folder.
The transform works but you have to 'force' it to generate new data on each iteration. It took me a while to figure that out and it makes sense now but I couldn't find any documentation explaining this. But it works simply by referencing other existing metadata: msbuild sees that has to be evaluated on every iteration so it will happily evaluate anything part of the new metadata. Example, simply using %(FileName):
<Target Name="CreateUniqueNames">
<ItemGroup>
<MyFiles Include="$(SRCROOT)\**\x.txt"/>
<MyFiles>
<Dest>%(Filename)$([System.Guid]::NewGuid())%(FileName)</Dest>
</MyFiles>
</ItemGroup>
<Message Text="%(MyFiles.Identity) -> %(MyFiles.Dest)"/>
</Target>
Alternatively you can make use of the unique metadata you already have namely RecursiveDir:
<Target Name="CreateUniqueNames">
<ItemGroup>
<MyFiles Include="$(SRCROOT)\**\x.txt"/>
<MyFiles>
<Dest>x_$([System.String]::Copy('%(RecursiveDir)').Replace('\', '_')).txt</Dest>
</MyFiles>
</ItemGroup>
<Message Text="%(MyFiles.Identity) -> %(MyFiles.Dest)"/>
</Target>

Get list of dlls in a directory with MSBuild

The following gives me only 1 file, the exe:
<ItemGroup>
<AssembliesToMerge Include="$(MSBuildProjectDirectory)\App\bin\Release\*.*" Condition="'%(Extension)'=='.dll'"/>
<AssembliesTomerge Include="$(MSbuildProjectDirectory)\App\bin\Release\App.exe"/>
</ItemGroup>
If I remove the Condition attribute, AssembliesToMerge contains all the files in the directory--dlls and otherwise. What am I doing wrong?
I am testing this via the ILMerge MSBuildCommunityExtensions Task. If there is a way to directly print the items in the ItemGroup, then that might help to ensure it's an issue with the Condition attribute.
Just use a wildcard in Include to filter dll files (Items wildcard)
<ItemGroup>
<AssembliesToMerge Include="$(MSBuildProjectDirectory)\App\bin\Release\*.dll"/>
<AssembliesTomerge Include="$(MSbuildProjectDirectory)\App\bin\Release\App.exe"/>
</ItemGroup>
I think it doesn't work using the Condition attribute because Item metadatas aren't set yet during creation, so %(Extension) is empty.

How do I generate an MsBuild itemList from another item list based on the current item's directory name?

i'm taking a list of files *.config and copying them to a list of directories. The directories are relative to a path C:\branches\ have a name and then the name.UnitTest.
so the copy looks like this without being refactored/batched:
<Target Name="CopyClientConfigs">
<ItemGroup>
<ClientConfigDestinations Include="$(LocalSourcePath)\Module1\Module1.UnitTest\;
$(LocalSourcePath)\Module2\Module2.UnitTest\;
$(LocalSourcePath)\CommonControls\Module3\Module3.UnitTest\;
$(LocalSourcePath)\Administration\Module4\Module4.UnitTest\;
$(LocalSourcePath)\IndividualControls\Configuration\Module5\Module5.UnitTest\" />
<ClientConfigs
Include="$(ClientConfigPath)\*.config"
Exclude="$(ClientConfigPath)\P*.config" >
</ClientConfigs>
</ItemGroup>
<Copy
SourceFiles="#(ClientConfigs)"
DestinationFolder="%(ClientConfigDestinations.FullPath)"
/>
What I want is to be able to use this ItemGroup
<ItemGroup>
<MyModules Include="$(LocalSourcePath)\Module1;
$(LocalSourcePath)\Module2;
$(LocalSourcePath)\CommonControls\Module3;
$(LocalSourcePath)\Administration\Module4;
$(LocalSourcePath)\IndividualControls\Configuration\Module5"
/>
So the task would be like
Copy
SourceFiles="#(ClientConfigs)"
DestinationFolder="%(ClientConfigDestinations.FullPath)\*.UnitTest\"
/>
Or better
Copy
SourceFiles="#(ClientConfigs)"
DestinationFolder="%(ClientConfigDestinations.FullPath)\%(ClientConfigDestinations.NameOnly).UnitTest\"
/>
How do I refactor or properly batch this operation?
If I read your question right, I think you are trying to do a cross-product copy: copy all items in one ItemGroup to all the folders in a different group.
I actually have a neat target that I use to do this, as I hate the way TeamBuild puts all the binaries into a single folder - I want projects to be able to specify that their output is a "bundle" and that the output will also be copied to one or more locations.
To do this, I have two itemgroups: BundleFiles (which is the set of files that I want to copy) and BundleFolders which are the set of folders that I want to copy to.
<ItemGroup>
<BundleOutDir Include="FirstFolder;SecondFolder" />
<BundleFiles Include="file1;file2" />
</ItemGroup>
My target then contains two tasks like this:
<ItemGroup>
<FilesByDirsCrossProduct Include="#(BundleFiles)">
<BundleOutDir>%(BundleOutDir.FullPath)</BundleOutDir>
</FilesByDirsCrossProduct>
</ItemGroup>
This creates an uber item group containing a cross product of files by folders.
The copy is then pretty simple:
<Copy SourceFiles="#(FilesByDirsCrossProduct)"
DestinationFiles="#(FilesByDirsCrossProduct -> '%(BundleOutDir)\%(Filename)%(Extension)' ) "
SkipUnchangedFiles="true" />
This then copies the files to the folder specified within their meta data.
My target is actually a little more clever in that I can declare that bundles will go to sub folders and/or I can rename a file during the copy through meta data, but that's a different story
<Target Name="CopyClientConfigsBatched" Outputs="%(MyModules.FullPath)">
<Message Text="#(MyModules -> '%(FullPath)\%(FileName).UnitTest')"/>
<ItemGroup>
<ClientConfigs
Include="$(ClientConfigPath)\*.config"
Exclude="$(ClientConfigPath)\P*.config" >
</ClientConfigs>
</ItemGroup>
<Copy SourceFiles="#(ClientConfigs)" DestinationFolder="#(MyModules -> '%(FullPath)\%(FileName).UnitTest')"
SkipUnchangedFiles="true"/>
</Target>
target batching seems to have done it!