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

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>

Related

MSBuild 17: how to generate a temporary folder name

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.

How do I group files by the top folder they are in and have a task act on those files?

I am attempting to link resource files that were organized by locale folders into their own .resources.dll assembly. There are more than 750 locales that are dynamically generated, so it is not practical to hard code them like the docs show.
<ItemGroup>
<ResourceFiles Include="<path_to>/af/af.res;<path_to>/af/feature1.af.res;<path_to>/af/feature2.af.res">
<Culture>af<Culture>
</ResourceFiles>
<ResourceFiles Include="<path_to>/af-NA/af_NA.res;<path_to>/af-NA/feature1.af_NA.res;<path_to>/af-NA/feature2.af_NA.res">
<Culture>af-NA<Culture>
</ResourceFiles>
<ResourceFiles Include="<path_to>/af-ZA/af_ZA.res;<path_to>/af-ZA/feature1.af_ZA.res;<path_to>/af-ZA/feature2.af_ZA.res">
<Culture>af-ZA<Culture>
</ResourceFiles>
</ItemGroup>
The above structure can be used to execute the AL task multiple times for each group of files. As you can see, my files are arranged in folders that are named the same as the culture in .NET.
My question is, how do I build this structure dynamically based on the 750+ locale folders, many which contain multiple files?
What I Tried
I was able to get the grouping to function. However, for some odd reason the list of files is being evaluated as a String rather than ITaskItem[] like it should be. This is the structure that does the correct grouping. It is based on this Gist (although I am not sure whether I am misunderstanding how to use the last bit because the example is incomplete).
<PropertyGroup>
<SatelliteAssemblyTargetFramework>netstandard2.0</SatelliteAssemblyTargetFramework>
<TemplateAssemblyFilePath>$(MSBuildProjectDirectory)/bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll</TemplateAssemblyFilePath>
<ICU4JResourcesDirectory>$(SolutionDir)_artifacts/icu4j-transformed</ICU4JResourcesDirectory>
<ICU4NSatelliteAssemblyOutputDir>$(SolutionDir)_artifacts/SatelliteAssemblies</ICU4NSatelliteAssemblyOutputDir>
<PropertyGroup>
<Target
Name="GenerateOurSatelliteAssemblies"
DependsOnTargets="ExecICU4JResourceConverter"
AfterTargets="AfterBuild"
Condition=" '$(TargetFramework)' == '$(SatelliteAssemblyTargetFramework)' ">
<ItemGroup>
<EmbeddedResources Include="$(ICU4JResourcesDirectory)/*.*" />
<EmbeddedResourcesPaths Include="$([System.IO.Directory]::GetDirectories('$(ICU4JResourcesDirectory)'))" />
<!-- This groups each locale together along with its nested files and root path -->
<FolderInLocale Include="#(EmbeddedResourcesPaths)">
<Culture>$([System.IO.Path]::GetFileName('%(Identity)'))</Culture>
<Files>$([System.IO.Directory]::GetFiles('%(EmbeddedResourcesPaths.Identity)'))</Files>
</FolderInLocale>
</ItemGroup>
<!-- EmbedResources accepts ITaskItem[], but the result
of this transform is a ; delimited string -->
<AL EmbedResources="#(FolderInLocale->'%(Files)')"
Culture="%(FolderInLocale.Culture)"
TargetType="library"
TemplateFile="$(TemplateAssemblyFilePath)"
KeyFile="$(AssemblyOriginatorKeyFile)"
OutputAssembly="$(ICU4NSatelliteAssemblyOutputDir)/%(FolderInLocale.Culture)/$(AssemblyName).resources.dll" />
</Target>
I have attempted numerous ways to replace the semicolon characters with %3B and to split on semicolon (i.e. #(FolderInLocale->'%(Files.Split(';'))'), but in all cases, the transform fails to evaluate correctly.
I have also consulted the docs for MSBuild well-known item metadata to see if there is another way of grouping by folder. Unfortunately, there is no %(FolderName) metadata, which would solve my issue completely. While I was able to get it to group by folder using the below XML, it immediately flattened when trying to get the name of the top level folder, which is where the name of the culture is.
I am using GetFileName() to get the name of the top level folder after stripping the file name from it. But please do tell if there is a better way.
<ItemGroup>
<EmbeddedResourcesLocalizedFiles Include="$(ICU4JResourcesDirectory)/*/*.*"/>
<EmbeddedResourcesLocalized Include="#(EmbeddedResourcesLocalizedFiles)">
<Culture>%(RootDir)%(Directory)</Culture>
</EmbeddedResourcesLocalized>
<!-- Calling GetFileName() like this rolls the files into a single group,
but prior to this call it is grouped correctly. -->
<EmbeddedResourcesLocalized2 Include="#(EmbeddedResourcesLocalized)">
<Culture>$([System.IO.Path]::GetFileName('%(EmbeddedResourcesLocalized.Culture)'))</Culture>
</EmbeddedResourcesLocalized2>
</ItemGroup>

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>

MSBuild exclude syntax not working

I have a test file in MSBuild to create a ZIP. I need exclude certain folders. I have the following working.
<PropertyGroup>
<TestZipPath>C:\path\to\my\folder\</TestZipPath>
<ExcludeList>$(TestZipPath)\**\_svn\**;$(TestZipPath)\**\.svn\**;$(TestZipPath)\**\obj\**;$(TestZipPath)\**\*.config</ExcludeList>
</PropertyGroup>
<ItemGroup>
<ZipFiles Include="$(TestZipPath)\**\*.*" Exclude="$(ExcludeList)" />
</ItemGroup>
<Message Text="%(ZipFiles.FullPath)"/>
That seems hideously verbose to me. Ideally I would want the ExcludeList to be formatted like this:
<ExcludeList>**\_svn\**;**\.svn\**;**\obj\**;**\*.config</ExcludeList>
But it doesn't seem to work. Why do I need to include $(TestZipPath) before every exclude pattern? Is ** not intended to be used at the beginning of a path? Is there a better way to do this?
I figured out the problem. The issue is that I am trying to include files that are not relative to the msbuild file that I'm executing. MSBuild assumes that file paths are relative to this location and gives you no way to change that. Because of this, all of my paths have to be absolute and can't be relative.
Try to add '.\' before every include pattern. Like this:
'.\**\obj\**'

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!