How do I exclude the contents of a directory but not the directory itself in MSBuild - msbuild

I'm using a Web Deployment Project 2008 to build my web application. I'd like to exclude the contents of several folders from the build but keep the blank directory itself. However, if I do this
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\ImageCache\**\*.*" />
it will exclude the ImageCache directory itself. So how do I keep the directory? Thanks in advance?

I'm afraid you must do this in two lines :
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\ImageCache\**\*.*" />
<IncludeFromBuild Include="$(SourceWebPhysicalPath)\ImageCache\" />
But that could not work because The "Copy" task does not support copying directories.
Thus, what I suggest is that you exclude the files like you did and create an empty directory in the AfterMerge target :
<ItemGroup>
<ExcludeFromBuild Include="$(SourceWebPhysicalPath)\ImageCache\**\*.*" />
<ItemGroup>
<...>
<Target Name="AfterMerge">
<MakeDir Directories="$(SourceWebPhysicalPath)\ImageCache" />
</Target>

Related

csproj include (recursive) folders based on a list (batching)

I want to add some files recursively to a single project that are located in folders inside my solution root.
Right now, I found a working solution to add those files by specifying each folder manually:
<ItemGroup>
<Content Include="..\Folder1\**\*">
<Link>Folder1\%(RecursiveDir)\%(Filename)%(Extension)</Link>
</Content>
<Content Include="..\Folder2\**\*">
<Link>Folder2\%(RecursiveDir)\%(Filename)%(Extension)</Link>
</Content>
<Content Include="..\Folder3\**\*">
<Link>Folder3\%(RecursiveDir)\%(Filename)%(Extension)</Link>
</Content>
</ItemGroup>
As those can be a handfull folders and they can be different depending on my solution, I want to have an easy list to define the folder names.
Something like this:
<PropertyGroup>
<DefinedResourceFolders>Folder1;Folder2;Folder3</DefinedResourceFolders>
</PropertyGroup>
This would also allow me to input this as a property directly when calling msbuild to extend the files that are going to the build output, maybe.
I tried to look into the MSBuild Batching documentation and several other online sources, but I could not figure it out. Most batching examples I found were working with Targets, not Includes into the solution items.
Is this possible? How would I define the <ItemGroup> for the content then?
P.S.: I don't care about wildcard issues with .csproj files when new files are added, etc. This will either be a single "Resources" project only containing those displayed files, or I am using my external .props file that I am importing into each .csproj file anyway.
Suppose you want to include all files in folders root/Folder1, root/Folder2 and root/Folder3 recursively.
This is how the builds (both VS and CLI) do what you want. However, VS will not show the files as part of the project.
<Project Sdk="Microsoft.NET.Sdk" InitialTargets="__AddBatchedContent;$(InitialTargets)">
<!-- the usual properties -->
<!-- You could of course define the folder array directly in an Item,
but this is what you wanted :-) -->
<PropertyGroup>
<Lookups>Folder1;Folder2;Folder3</Lookups>
</PropertyGroup>
<ItemGroup>
<LookupDir Include="$(Lookups)" />
</ItemGroup>
<Target Name="__AddBatchedContent">
<ItemGroup>
<!-- save the original source folder of the globbed file name in custom
metadata "Folder" so we can use it later as its output base folder -->
<__BatchedFiles Include="..\%(LookupDir.Identity)\**\*" Folder="%(LookupDir.Identity)" />
<Content Include="#(__BatchedFiles)" Link="%(Folder)\%(RecursiveDir)\%(Filename)%(Extension)" CopyToOutputDirectory="Always" />
</ItemGroup>
</Target>
</Project>
Note that if you want to put this in a .targets file to use this technique in multiple projects, you should replace the ..\ with $(MSBuildThisFileDirectory)..\ so that the path is relative to the (known) location of the .targets file instead of the (unknown) location of the importing project.
Thanks to #Ilya Kozhevnikov for the basis of this answer.
You are not the only one who would like this to be simpler :-): https://github.com/dotnet/msbuild/issues/3274

MSBuild - Copy file to output directory if the file isn't in the output directory

I have a nuget with a .targets file that tells the consuming project to copy all files within a "Dependencies" folder to the output directory.
<ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)/../contentFiles/Dependencies/*.*" />
</ItemGroup>
<Target Name="CopyDependencies" AfterTargets="Build">
<Copy SourceFiles="#(Files)"
DestinationFolder="$(TargetDir)" />
</Target>
This nuget is consumed by two projects: Project A and Project B. For this question, let's say we have a System.Runtime.InteropServices.RuntimeInformation.dll that is one of the dependencies within this nuget. The output directory of Project A does not already have System.Runtime.InteropServices.RuntimeInformation.dll, so it gets copied to the output directory when the project is built. Project B however already contains System.Runtime.InteropServices.RuntimeInformation.dll in the output directory. This causes a runtime issue at startup since the targets file is trying to overwrite the existing DLL of the same name with the System.Runtime.InteropServices.RuntimeInformation.dll file from within the nuget (which is a dependency of other files within the output directory).
So, how can I adjust my .targets file to only copy in files that do not already exist within the output directory based on name, and not size or date modified?
There are several ways but probably the most succinct change to your example code would be the following:
<ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)/../contentFiles/Dependencies/*.*" />
</ItemGroup>
<Target Name="CopyDependencies" AfterTargets="Build">
<Copy SourceFiles="#(Files)" DestinationFolder="$(TargetDir)" Condition="!Exists('$(TargetDir)/%(Filename)%(Extension)')" />
</Target>
The change is adding a Condition on the Copy that is using the metadata of the #(Files) collection to test that the file does not exist in $(TargetDir).
Because of the use of metadata, the Copy is a task batch. Essentially the #(Files) collection is divided into batches by %(Filename)%(Extension) and there is a separate Copy task invoked for each batch.
If there is a large number of files in the Dependencies folder, the following variant may provide better performance.
<ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)/../contentFiles/Dependencies/*.*" />
</ItemGroup>
<Target Name="CopyDependencies" AfterTargets="Build">
<ItemGroup>
<FilesToCopy Include="#(Files)" Condition="!Exists('$(TargetDir)/%(Filename)%(Extension)')" />
</ItemGroup>
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="$(TargetDir)" />
</Target>
The task batching is moved to the definition of a new ItemGroup collection and the Copy task is invoked once for the set of files. The potential performance improvement is that the implementation of the Copy task tries to parallelize copies, which it can't do when invoked per file.

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>

How to change build Action for files using MSBuild

We are using NHibernate in our project and need all .hbm.xml files to be embedded resources.
To automate this boring task we added this target definition to our project file:
<Target Name="BeforeBuild">
<ItemGroup>
<EmbeddedResource Include="**\*.hbm.xml" />
</ItemGroup>
</Target>
It works only if all hbm.xml files have build action None. But in our project some files are manually set to Embedded Resource and there is no chance to change all of them right now.
So we get build error "The item was specified more than once in the "Resources" parameter and both items had the same value for the "LogicalName" metadata. Duplicate items are not supported by the "Resources" parameter unless they have different values for the "LogicalName" metadata."
Is it possible to write a target that changes build action only for .hbm.xml files with build action None (or NOT Embedded Resource)?
Will it work to first remove the ones already set to Embedded resource, before adding them all in?
<ItemGroup>
<EmbeddedResource Remove="**\*.hbm.xml" />
<EmbeddedResource Include="**\*.hbm.xml" />
</ItemGroup>

msbuild fileupdate setup project

I'm looking to use the fileUpdate task from msbuildtasks.tigris.org to modify image src's as part of our web setup project so that they will point to a static img sub domain (or later a CDN)
I can run a task within a given project with:
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
<Target Name="AfterBuild">
<FileUpdate
Files="basic.css"
Regex="/images/([^\)]*)"
ReplacementText="http://img.domain.com/images/$1" />
</Target>
However, I dont want to overwrite the original css source file, but want to run this as part of our deployment project that produces an msi.
This is done using a web setup project (.vdproj) which also uses a custom actions project which is just a standard .csproj
My questions are:
How can I run this task in the setup project so that I replace content in the files that go into the .msi?
Is there a way to use wildcards for the files - ideally I want to say do this for ALL .css files?
In order to achieve this you need to use an item group to create the list for you
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
<Target Name="AfterBuild">
<ItemGroup>
<CssFiles Include='$(SolutionRoot)\**\*.css' />
</ItemGroup>
<FileUpdate
Files="#(CssFiles)"
Regex="/images/([^\)]*)"
ReplacementText="http://img.domain.com/images/$1" />
</Target>