MsBuild: Item: Remove with absolute path - msbuild

This comes from a Shared project:
<Content Include="$(MSBuildThisFileDirectory)MyFile1" />
Let's say it's equivalent to
<Content Include="c:\MyProject\MyFile1" />
This comes from a normal project:
<Content Include="MyFile2" />
One is an absolute path, the other is a relative one.
Which makes issues when trying to Remove them, because eg the one with the relative path can't be Remove-d with its absolute path.
It seems Remove does string matching and not Path matching.
Maybe I have to type into Remove the exact same string as it was Include-ed?
I can remove these files with:
<Content Remove="MyFile2" />
<Content Remove="c:\MyProject\MyFile1" />
I can't remove them with:
<Content Remove="c:\MyProject\MyFile2" />
<Content Remove="MyFile1" />
Is there a way to tell MsBuild these are paths and Remove them always by absolute path?

Items have a set of metadata - see MSBuild well-known item metadata.
%(Identity) will be the string that was used for the item in the Include. But if the Item is a file that exists, then %(FullPath) will have a value.
e.g. Your example items are:
Identity
FullPath
c:\MyProject\MyFile1
c:\MyProject\MyFile1
MyFile2
c:\MyProject\MyFile2
You need to use Identity for the Remove but you can create a new ItemGroup that is a subset of Content by testing the FullPath and then use that ItemGroup as the value of your Remove.
An example might be
<Target Name="Test">
...
<ItemGroup>
<FilesToRemove Include="#(Content)" Condition="'%(FullPath)' == 'c:\MyProject\MyFile1' or '%(FullPath)' == 'c:\MyProject\MyFile2'" />
</ItemGroup>
<ItemGroup>
<Content Remove="#(FilesToRemove)" />
</ItemGroup>
...
</Target>
The FilesToRemove ItemGroup is using task batching and must be within a Target.
Another example of the same approach but testing the file extension:
<Target Name="Test">
...
<ItemGroup>
<FilesToRemove Include="#(Content)" Condition="'%(Extension)' == '.js'" />
</ItemGroup>
<ItemGroup>
<Content Remove="#(FilesToRemove)" MatchOnMetadata="Extension" />
</ItemGroup>
...
</Target>
The Remove attribute accepts wildcards but
<Content Remove="**\*.js" />
won't work because it will not match items added without using a wildcard.

Related

MSBuild fails to find directory with huge sub-directory

I want to create an MSBuild item that includes all files in a sub-directory, recursively, except the "node_modules" directory and a few others.
<MyItem Include="MyDir\**\*.*" Exclude="MyDir\node_modules\**\*.*;MyDir\tmp\**\*.*" />
In the build log I see this:
Input file "MyDir\**\*.*" does not exist.
When I delete the "node_modules" directory it works. I suspect MSBuild fails to process it because it contains over 30,000 files and a deep sub-directory structure. Is there a way I can work around this without listing all the sub-directories I want to include, which may change from time to time?
OK, I figured out a way using another target with CreateItems:
<ItemGroup>
<InputDirs Include="MyDir" />
<InputExcludedSubDirs Include="MyDir\tmp;MyDir\node_modules" />
</ItemGroup>
<Target Name="PrepareForMyTarget">
<ItemGroup>
<InputSubDirs Include="$([System.IO.Directory]::GetDirectories("%(InputDirs.Identity)"))" Exclude="#(InputExcludedSubDirs)" />
</ItemGroup>
<CreateItem Include="%(InputSubDirs.Identity)\**\*.*">
<Output TaskParameter="Include" ItemName="Inputs" />
</CreateItem>
</Target>
<Target Name="MyRealTarget" DependsOnTargets="PrepareForMyTarget">
... use Inputs item here as normal ...
</Target>

Copy subfolders and files in msbuild

I'm unable to copy subfolders and files with this code:
<ItemGroup>
<Compile Include="C:\Test\Folder1\text.txt"/>
<Compile Include="C:\Test\text1.txt"/>
</ItemGroup>
<Copy SourceFiles="#(Compile)" DestinationFiles="#(Compile->'C:\Destination\%(RecursiveDir)%(Filename)%(Extension)')" />
I get this error: Could not find a part of the path.
How to copy C:\Test\ files and subfolders to C:\Destination\ with msbuild ?
Thanks in advance for your help.
In order for the RecursiveDir metadata to be populated, you must specify a recursive wildcard (double asterisks) in your items' paths. The ** wildcard will mark the relative point at which the RecursiveDir should be applied. In your example, it sounds like you'd want to add the ** wildcard after C:\Test, so your code would need to look like the following example:
<ItemGroup>
<Compile Include="C:\Test\**\Folder1\text.txt"/>
<Compile Include="C:\Test\**\text1.txt"/>
</ItemGroup>
<Copy SourceFiles="#(Compile)" DestinationFiles="#(Compile->'C:\Destination\%(RecursiveDir)%(Filename)%(Extension)')" />
Adding the wildcard as shown above will copy the files to the following locations:
C:\Destination\text1.txt
C:\Destination\Folder1\text.txt
The same as in response above, but without additional list transformation:
<ItemGroup>
<Compile Include="C:\Test\**\Folder1\text.txt"/>
<Compile Include="C:\Test\**\text1.txt"/>
</ItemGroup>
<Copy SourceFiles="#(Compile)" DestinationFolder="C:\Destination\%(RecursiveDir)" />

Deploying from MSBuild without overwriting specific files

So here's what I want to do:
I want a build script that will xcopy deploy build outputs for a legacy winform app to a given directory. I want to specify a list of files to not overwrite (some config files).
I would rather have the list of files to not overwrite be passed as a parameter than hard code them.
This seems to be really unexpectedly hard. Here's what I have so far:
<!-- A property that is passed a semicolon delimited list of file names -->
<PropertyGroup>
<ProtectedFiles/>
</PropertyGroup>
<--! An ItemGroup to pick up the files>
<ItemGroup>
<FilesToDelete Include=$(DeploymentTargetFolder)\*.* Exclude="#(ProtectedFiles->'$(DeployTargetFolder)\%(identity)')"
<ItemGroup/>
<--! the delete isn't working, so I will stop just with that to keep the code brief -->
<Delete Files="#(FilesToDelete)"/>
The delete just ignores the exclude files and deletes everything
Is there a better way to do this? It doesn't seem too crazy -- I just want to
Delete all files from the target directory, except for the config files
Copy all of the files from the build outputs to the target directory, without overwriting the config files.
The first problem with your particular markup appears to confuse MsBuild $(properties) with MsBuild %(items) and MsBuild #(itemgroups).
ProtectedFiles is a property:
<!-- A property that is passed a semicolon delimited list of file names -->
<PropertyGroup>
<ProtectedFiles/>
</PropertyGroup>
But it's being treated as an Item and wouldn't have any %item.metadata:
<--! An ItemGroup to pick up the files>
<ItemGroup>
<FilesToDelete Include=$(DeploymentTargetFolder)\*.* Exclude="#(ProtectedFiles->'$(DeployTargetFolder)\%(identity)')"
<ItemGroup/>
Save the following markup locally as "foo.xml", then call "msbuild.exe foo.xml" and observe the output:
<Project ToolsVersion="4.0" DefaultTargets="foo" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<FilesProp>FileA.txt;FileB.txt</FilesProp>
</PropertyGroup>
<ItemGroup>
<ProtectedFiles Include="FileA.txt" />
<ProtectedFiles Include="FileA.txt" />
</ItemGroup>
<Target Name="foo">
<Message Importance="high" Text="ProtectedFiles ItemGroup: #(ProtectedFiles)" />
<Message Importance="high" Text="ProtectedFiles ItemGroup transform: #(ProtectedFiles->'%(Identity)')" />
<Message Importance="high" Text="FilesProp Property: $(FilesProp)" />
<Message Importance="high" Text="FilesProp Property: #(FilesProp->'%(FilesProp.Identity)')" />
</Target>
</Project>
Will yield the following output:
foo:
ProtectedFiles ItemGroup: FileA.txt;FileA.txt
ProtectedFiles ItemGroup transform: FileA.txt;FileA.txt
FilesProp Property: FileA.txt;FileB.txt
FilesProp Property:
If you're unable to change the design and need to convert a Property comprising a semi-colon delimited list of file paths, use the MsBuild <CreateItem /> task.
Add this markup to foo.xml occurring after the Foo target, then invoke msbuild again, but using the "bar" target (e.g. msbuild.exe foo.xml /t:bar)
<Target Name="bar">
<CreateItem Include="$(FilesProp)">
<Output TaskParameter="Include" ItemName="TheFiles"/>
</CreateItem>
<Message Text="TheFiles ItemGroup: #(TheFiles)" Importance="high" />
<Message Text="Output each item: %(TheFiles.Identity)" Importance="high" />
</Target>
Will yield the following output:
bar:
TheFiles ItemGroup: FileA.txt;FileB.txt
Output each item: FileA.txt
Output each item: FileB.txt
Next you should rethink some of your assumptions. I don't believe the file extension should be the determining factor when deciding which files to update, rather you should rely on MsBuild's ability to build tasks incrementally allowing it to perform a task only if the inputs are newer than the outputs. You can do this by using an MsBuild <Copy /> task configured to skip unchanged files.
Add this markup to the above Xml file, then modify the $(SourceFolder) and $(TargetFolder) to point to a source folder you'd like to copy recursively, and a destination folder to place the files. Build using "msbuild.exe foo.xml /t:Deployment" and observe the output.
<Target Name="Deployment">
<PropertyGroup>
<SourceFolder>c:\sourcefolder\</SourceFolder>
<TargetFolder>c:\destinationfolder\</TargetFolder>
</PropertyGroup>
<CreateItem Include="$(SourceFolder)\**\*.*">
<Output TaskParameter="Include" ItemName="FilesToCopy" />
</CreateItem>
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="$(TargetFolder)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
Without modifying any of the source files, run the command again and note that no files were copied.
Modify a file in the source folder, then run the command again. Notice that only the updated files were copied?
I hope this gets you on the right track.
There seems to be an already existing post, similar to this. Please check this Trying to exclude certain extensions doing a recursive copy (MSBuild)

Filter an existing itemgroup so it only includes files that match some condition

How do you filter an existing ItemGroup based on a specific condition, such as file extension or the item's metadata?
For this example, I'll use the file extension. I'm trying to filter the 'None' ItemGroup defined by VS so that my target can operate on all files of a given extension.
For example, the following may be defined:
<ItemGroup>
<None Include="..\file1.ext" />
<None Include="..\file2.ext" />
<None Include="..\file.ext2" />
<None Include="..\file.ext3" />
<None Include="..\file.ext4" />
</ItemGroup>
I want to filter the 'None' ItemGroup above so it only includes the ext extension. Note that I do not want to specify all the extensions to exclude, as they'll vary per project and I'm trying to make my target reusable without modification.
I've tried adding a Condition within a target:
<Target Name="Test">
<ItemGroup>
<Filtered
Include="#(None)"
Condition="'%(Extension)' == 'ext'"
/>
</ItemGroup>
<Message Text="None: '%(None.Identity)'"/>
<Message Text="Filtered: '%(Filtered.Identity)'"/>
</Target>
But sadly, it doesn't work. I get the following for output:
Test:
None: '..\file1.ext'
None: '..\file2.ext'
None: '..\file.ext2'
None: '..\file.ext3'
None: '..\file.ext4'
Filtered: ''
<ItemGroup>
<Filtered Include="#(None)" Condition="'%(Extension)' == '.ext'" />
</ItemGroup>
For advanced filtering I suggest using the RegexMatch from MSBuild Community Tasks.
In this Example we will Filter for Versionnumbers
<RegexMatch Input="#(Items)" Expression="\d+\.\d+\.\d+.\d+">
<Output ItemName ="ItemsContainingVersion" TaskParameter="Output" />
</RegexMatch>
Install MSBuild Community Tasks via Nuget: PM> Install-Package MSBuildTasks or download it here
Then Import it in your MSBuild Script:
<PropertyGroup>
<MSBuildCommunityTasksPath>..\.build\</MSBuildCommunityTasksPath>
</PropertyGroup>
<Import Project="$(MSBuildCommunityTasksPath)MsBuild.Community.Tasks.Targets" />

Trying to include a transformed App.config in a XAP file

I have a Silverlight project with multiple configuration files, and am using the transformation approach shown here:
App.Config Transformation for projects which are not Web Projects in Visual Studio 2010?
This approach doesn't work as-is for Silverlight projects though. I've re-written the MSBuild project to look like this:
<ItemGroup>
<None Include="App.config" />
<None Include="App.QABuild.config">
<DependentUpon>App.config</DependentUpon>
</None>
</ItemGroup>
....
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v10.0\Web\Microsoft.Web.Publishing.Tasks.dll" />
<Target Name="BeforeCompile" Condition="Exists('App.$(Configuration).config')">
<!-- Generate transformed app config in the output directory -->
<Message Importance="high" Text="Transforming 'App.$(Configuration).config' to output config file..." />
<TransformXml Source="App.config" Destination="$(OutputPath)App.config" Transform="App.$(Configuration).config" />
<ItemGroup>
<Content Include="$(OutputPath)App.config" />
</ItemGroup>
</Target>
<Target Name="BeforeCompile" Condition="!Exists('App.$(Configuration).config')">
<Message Importance="high" Text="Using default 'App.config' as output config file..." />
<Copy SourceFiles="App.config" DestinationFiles="$(OutputPath)App.config" />
<ItemGroup>
<Content Include="$(OutputPath)App.config" />
</ItemGroup>
</Target>
This code generates the correct output file for the correct configuration, however it is never included in the XAP file, even though I am putting the output config into the Content item group. All I need to happen is for the output config to get included in the output XAP but I can't get this to happen.
Can anyone tell me what I'm doing wrong? I'm not an MSBuild expert by any means!
Found the solution by digging into the Silverlight 4 targets. Turns out the XAP packager target actually takes an item called XapFilesInputCollection, which is where the input files come from. The Content item looks likes it is copied to this item before my target runs, so modifying the Content item afterwards is the wrong approach.
All I did was add the transformed files directly to the XapFilesInputCollection item and it worked as I expected.