MSBuild several metadata requests in the same expression - msbuild

I am writing my first MSBuild script and ran into a problem.
I have several projects, defined in an itemgroup
<ItemGroup>
<Projects Include="Project1Dir\Project1.csproj"/>
<Projects Include="Project2Dir\Project2.csproj"/>
</ItemGroup>
Then, on deployment step, I am trying to do this:
The following should collect all the files for deployment into separate itemgroups for each project ("Project1deploymentFiles" and "Project2deploymentFiles")
<CreateItem Include="$(WebPublishDir)\%(Projects.Filename)\**\*.*">
<Output ItemName="%(Projects.Filename)deploymentFiles" TaskParameter="Include"/>
</CreateItem>
Thes line, should copy each project's files into separate folder
<Copy SourceFiles="#(%(Projects.Filename)deploymentFiles)" DestinationFolder="$(DeploymentDir)\%(Projects.Filename)\%(RecursiveDir)\" />
But it seems that MSBuild resolves %(RecursiveDir) metadata to empty string, as all the files are copied to the same root folder (different for each project).
Any suggestions what am I doing wrong here?

I've found a solution myself:
<CreateItem Include="$(WebPublishDir)\%(Projects.Filename)\**\*.*" AdditionalMetadata="ProjectDir=%(Projects.Filename)\">
<Output ItemName="deploymentFiles" TaskParameter="Include"/>
</CreateItem>
<Copy SourceFiles="#(deploymentFiles)" DestinationFolder="$(DeploymentDir)\%(ProjectDir)\%(RecursiveDir)\" />
Main idea here is to use one item for all projects and just add AdditionalMetadata to values, containing projectname

Related

In MSBuild, is there a way to extract part of a path and save that for use [duplicate]

I use the following to get a list of project files that need to be compiled. Each project is stored in a subdirectory of the projects directory.
<ItemGroup>
<dprs Include="c:\projects\**\*.dpr" />
</ItemGroup>
Is there a task that I can use to extract to extract the directory that each project file is in? I know I can write my own task to do this but I was hoping that one already exists and that I simply have not found it yet.
If I understand the question correctly, you shouldn't need a task - you can do this with well-known meta data. Does this do the trick?
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">
<ItemGroup>
<dprs Include="c:\projects\**\*.dpr" />
</ItemGroup>
<Target Name="Default">
<CreateItem Include="%(dprs.RelativeDir)">
<Output ItemName="_ProjectFileLocations" TaskParameter="Include" />
</CreateItem>
<Message Text="#(_ProjectFileLocations->'%(FullPath)', '%0D%0A')" />
</Target>
</Project>
From the tests I ran, it shouldn't list a directory twice in the new item group.

Remove Files and Folders Copied From AfterBuild Target

I would like to avoid hard coding the dll and folder names in the AfterClean target, is there a dynamic way to do this? Ideally it would only delete the files and folders created by the Copy in the AfterBuild target.
I tried to simplify this by changing the DestinationFolder to include a subdirectory in the OutputPath. The AfterClean target would only have to remove that subdirectory at this point. However, some of the library's DLLImport paths don't take that subdirectory into consideration which results in a crash.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="AfterBuild">
<ItemGroup>
<NativeLibs Include="$(MSBuildThisFileDirectory)..\lib\native\**\*.*" />
</ItemGroup>
<Copy SourceFiles="#(NativeLibs)" DestinationFolder="$(OutputPath)\%(RecursiveDir)" />
</Target>
<Target Name="AfterClean">
<Delete Files="$(OutputPath)\LumiAPI.dll" />
<Delete Files="$(OutputPath)\LumiCore.dll" />
<Delete Files="$(OutputPath)\LumiInOpAPI.dll" />
<RemoveDir Directories="$(OutputPath)\SPM" />
<RemoveDir Directories="$(OutputPath)\plugin" />
</Target>
</Project>
Project Structure:
src
ConsumingProject
ConsumingProject.csproj
ConsumingProject.sln
packages
my-project.5.7.0.12
build
lib
native
plugin
VenusDvc.dll
SPM
sSPM_1.bin
LumiAPI.dll
LumiCore.dll
LumiInOpAPI.dll
net45
my-project.5.7.0.12.nupkg
Essentially I want to delete all the files and folders that were copied from the native folder to the output of the project (ie LumiAPI.dll, LumiCore.dll, SPM (folder), eSPM_1.bin, etc). However I want it to be generic enough so that if I add another folder to the native directory, it will delete those folders/files as well.
Use a seperate target which lists input and output files, then use that list in both other targets. Note this uses the DestinationFiles attribute from the Copy task instead of DestinationFolders. And it might print some messages about non-existing directories being passed to RemoveDir because the top directory gets removed already before child directories.
update since you don't want to remove the root output directory as it still has files, figured applying the 'only remove output directory if it's empty' principle for any destination directory is probably the safest way to go. Credits go to the answer here.
<Target Name="GetMyOutputFiles">
<ItemGroup>
<NativeLibs Include="$(MSBuildThisFileDirectory)..\lib\native\**\*.*" />
<!--Now add some metadata: output dir and output file-->
<NativeLibs>
<DestinationDir>$(OutputPath)\%(RecursiveDir)</DestinationDir>
<Destination>$(OutputPath)\%(RecursiveDir)%(FileName)%(Extension)</Destination>
</NativeLibs>
</ItemGroup>
</Target>
<Target Name="AfterBuild" DependsOnTargets="GetMyOutputFiles">
<!--Copy one-to-one-->
<Copy SourceFiles="#(NativeLibs)" DestinationFiles="#(NativeLibs->'%(Destination)')" />
</Target>
<Target Name="AfterClean" DependsOnTargets="GetMyOutputFiles">
<Delete Files="#(NativeLibs->'%(Destination)')" />
<!--Find number of files left in each destination directory-->
<ItemGroup>
<NativeLibs>
<NumFiles>0</NumFiles>
<!--Condition is to avoid errors when e.g. running this target multiple times-->
<NumFiles Condition="Exists(%(DestinationDir))">$([System.IO.Directory]::GetFiles("%(DestinationDir)", "*", System.IO.SearchOption.AllDirectories).get_Length())</NumFiles>
</NativeLibs>
</ItemGroup>
<!--Only remove empty directories, use 'Distinct' to skip duplicate directories-->
<RemoveDir Directories="#(NativeLibs->'%(DestinationDir)'->Distinct())" Condition="%(NumFiles)=='0'" />
</Target>

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>

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)

Output all files from a solution in an MSBuild task

In MSBuild, I would like to call a task that extracts all the files in all the project in a specific solution and hold these files in a property that can be passed around to other tasks (for processing etc.)
I was thinking something along the lines of:
<ParseSolutionFile SolutionFile="$(TheSolutionFile)">
<Output TaskParameter="FilesFound" ItemName="AllFilesInSolution"/>
</ParseSolutionFile>
<Message Text="Found $(AllFilesInSolution)" />
which would output the list of all files in the projects in the solution and I could use the AllFilesInSolution property as input to other analysis tasks. Is this an already existing task or do I need to build it myself? If I need to build it myself, should the task output an array of strings or of ITaskItems or something else?
I don't know about tasks, but there are already properties that hold all items. Just look in your typical project file and you'll see which collection they're being added to.
Note the properties Content, Compile, Folder... any time you add a file to a project, it gets put in one of the main collections like this:
<ItemGroup>
<Content Include="Default.aspx" />
<Content Include="Web.config" />
</ItemGroup>
<ItemGroup>
<Compile Include="Default.aspx.cs">
<SubType>ASPXCodeBehind</SubType>
<DependentUpon>Default.aspx</DependentUpon>
</Compile>
<Compile Include="Default.aspx.designer.cs">
<DependentUpon>Default.aspx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="App_Data\" />
</ItemGroup>
Then you can do stuff like this to put the values from existing properties into your properties (the Condition attribute acts as a filter):
<CreateItem Include="#(Content)" Condition="'%(Extension)' == '.aspx'">
<Output TaskParameter="Include" ItemName="ViewsContent" />
</CreateItem>
Or you can do it manually (the Include attribute uses the existing property OutputPath, but it indicates a path that inclues all files):
<CreateItem Include="$(OutputPath)\**\*">
<Output TaskParameter="Include" ItemName="OutputFiles" />
</CreateItem>
There are more details in the MSDN MSBuild documentation that I read when I was mucking with custom build tasks and stuff that was very helpful. Go read up on the CreateItem task and you'll be able to make more sense out of what I posted here. It's really easy to pick up on.
I use the following for solutions with SSRS projects (which dont build under TFS w/o vs installed on the build box). Basically we require that the RDLs be bundled into a build output so we can mark a build for release.
<Target Name="CopyArtifactstoDropLocation">
<CreateItem Include="$(SolutionRoot)\**\*.*">
<Output TaskParameter="Include" ItemName="YourFilesToCopy" />
</CreateItem>
<Copy
SourceFiles="#(YourFilesToCopy)"
DestinationFiles="#(YourFilesToCopy->'$(DropLocation)\$(BuildNumber)\Release\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
Just replace the usage of the Copy Task with whatever you need to do with your bundle. Granted this is going to get everything in your solution root, but if your using TFS then you should only have buildable artifacts.