MSBuild Select partial RecursiveDir - msbuild

I have a project that looks something like this:
src\ModuleA\code\somepath\test.js
src\ModuleA\code\ignorethis.txt
src\ModuleB\code\awesome.js
src\ModuleB\code\testing\file.js
And I would like to grab all these js files from the modules using an ItemGroup like this:
<ItemGroup>
<_Scripts Include="..\..\..\src\**\code\**\*.js"/>
</ItemGroup>
But when I copy them into the output I would like to strip the Module and code paths. If I use a Copy like this
<Copy SourceFiles="#(_Scripts)" DestinationFiles="#(Scripts->'..\tmp\Test\%(RecursiveDir)%(Filename)%(Extension)')"/>
The result looks like this (because the RecursiveDir contains the whole path starting at the first **)
tmp\Test\ModuleA\code\somepath\test.js
tmp\Test\ModuleB\code\awesome.js
tmp\Test\ModuleB\code\testing\file.js
However I'm trying to get a result like this:
tmp\Test\somepath\test.js
tmp\Test\awesome.js
tmp\Test\testing\file.js
I have tried by adding Metadata with functions but I can't seem to find the proper escaping needed to make this work. My last attempt was like this (if I can strip at least 1 level of folder, I assumed stripping the second level would be a simple repetition in yet another Meta element...)
<Scripts Include="#(_Scripts)">
<NewDir>$("%(_Scripts.RecursiveDir)".Substring(0, "%(_Scripts.RecursiveDir)".IndexOf("\")))</NewDir>
</Scripts>
Which yields a Error MSB4184: The expression <cut for brevity> cannot be evaluated.
If there is a simpler method or anyone can tell me what the right escape sequence is to get this working?

After struggling around with escaping and getting the values parsed and processed I managed to resolve it using the following:
<_ScriptsFiles Include="..\..\..\src\*\code\**\*.js"/>
<_Scripts Include="#(_ScriptsFiles)">
<FirstSlash>$([System.String]::new('%(_ScriptsFiles.RecursiveDir)').IndexOf('\'))</FirstSlash>
<DirMinusOne>$([System.String]::new('%(_ScriptsFiles.RecursiveDir)').Substring(%(_Scripts.FirstSlash)))</DirMinusOne>
<SecondSlash>$([System.String]::new('%(_Scripts.DirMinusOne)').IndexOf('\', 1))</SecondSlash>
<DirMinusTwo>$([System.String]::new('%(_Scripts.DirMinusOne)').Substring(%(_Scripts.SecondSlash)))</DirMinusTwo>
</_Scripts>
<Copy SourceFiles="#(_Scripts)" DestinationFiles="#(_Scripts->'..\..\..\tmp\%(DirMinusTwo)\%(Filename)%(Extension)')"/>

Related

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>

In MSBuild, why isn't Item Metadata, within a property, being resolved?

Below is a portion of a MSBuild file that I'm working on:
<ItemGroup>
<Tests Include="$(SolutionDir)\**\bin\$(TestPlatform)\$(Configuration)\*.Tests.dll" />
</ItemGroup>
<PropertyGroup>
<TestProperties>/testcontainer:%(Tests.FullPath)</TestProperties>
</PropertyGroup>
I want to have a property that holds a command line switch. However, when I try to use $(TestProperties) in an Exec Command string, %(Tests.FullPath) is never resolved to the absolute path of the Tests item. Instead, it's always processed literally, as "%(Tests.FullPath)".
Am I doing something wrong or is this a standard MSBuild behavior? If the latter, is there a way for me to workaround this?
Thanks
P.S. - I realize I probably don't need to access the FullPath property since my Include value is an absolute path. However, I'd still like to understand the issue, along with how to handle it.
You have a syntax error. Item lists are referenced via the # character and item meta data is referenced via %. Reference the MSBuild Special Character Reference for details. To access the well known item metadata, you need to apply a transform inside the Property itself.
<ItemGroup>
<Tests Include="MyFile.txt" />
</ItemGroup>
<PropertyGroup>
<TestProperties>/testcontainer:#(Tests->'%(FullPath)')</TestProperties>
</PropertyGroup>
You can find more help here

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\**'

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.

Skipping MSBuild target

Is there a way to use MSBuild syntax to skip a specific target? I have a file consisting of a lot of properties (lines containing /property:X=Y) that I want to pass on to a recursively called instance of MSBuild, but this file also contains a /target:X line, that I do not want to have any effect. I don't have the option to modify the file.
I suppose you are able to edit .proj file. You can manage MSBuild targets executing by the Condition. Your target, which you want to exclude, could contain something like this:
<Target
Name="SomeTarget"
Condition="'$(SomeProperty)'=='true'"
DependsOnTargets="SomeAnotherTarget"/>
SomeProperty can be passed in the calling:
MSBuild.exe build.proj /p:SomeProperty=false
Regards