How do I copy files in a build chain in msbuild - msbuild

Say I have project A and project B where B depends on A. Project A contains some .py files. A copies those py files to output folder $(Outdir) with the common PostProcessBuild target. B then copies its files and As output files to B's $(Outdir) with PostProcessBuild. Currently in order for the B's output to contain both A and B's files, I must run msbuild twice.
How can I update my target to run msbuild only once and output both A and B's files in B's output directory?
Copy target:
<Target Name="PostProcessBuild"
Inputs="#(ProjectOutput)"
Outputs="#(ProjectOutput->'%(Destination)%(FileName)%(Extension)')">
<Message Text="Post-Processing Build" />
<Copy SourceFiles="#(ProjectOutput)" DestinationFolder="%(ProjectOutput.Destination)"/>
</Target>
Project A:
<Project Sdk="Microsoft.Build.NoTargets" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectOutput Include="$(MSBuildProjectDirectory)\pySpark\*" Destination="$(OutDir)\%(RecursiveDir)\"/>
<ProjectOutput Include="$(MSBuildProjectDirectory)\pySpark\**" Destination="$(OutDir)\%(RecursiveDir)\"/>
</ItemGroup>
</Project>
Project B:
<Project Sdk="Microsoft.Build.NoTargets" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectReference Include="$(INETROOT)\sources\dev\Projects\xplat\A\A.xpproj" />
<ProjectOutput Include="$(AllProjectsRoot)\A\*;$(AllProjectsRoot)\A\**" Destination="$(OutDir)\%(RecursiveDir)"/>
</ItemGroup>
</Project>

Some questions:
Are the two projects in a solution (.sln file) and are you building the .sln?
Have you run MSBuild with the -v:d or -v:diag switch to increase the verbosity of the logging output?

Related

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.

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>

Creating a Release drop

So, I have used MSBuild but this was years ago.
I want to create a Release build for a solution where once built, it will copy all files into a variable set folder "ReleaseDrop" and zip up the contents.
Before zipping, I want to make sure it copies only the necessary files (i.e no pdb, no sln, no csproj, no .cs files (but .cshtml is allowed) or only certain directories and exclude other directories within a directory.
how can I do this?
This should be a start. It specifies a bunch of files to include in a release, copies them to a directory and zips them. For the zip part I used MSBuild Extension Pack since I have it installed anyway, but you could just as well use a prtable version of 7z or so and incoke it with the Exec task.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" >
<Import Project="$(MSBuildExtensionsPath)\ExtensionPack\4.0\MSBuild.ExtensionPack.tasks"/>
<!--default values for properties if not passed-->
<PropertyGroup>
<ProjectDir Condition="'$(ProjectDir) == ''">C:\Projects\MyProject</ProjectDir>
<ReleaseDrop Condition="'$(ReleaseDrop) == ''">c:\Projects\MyProject\ReleaseDrop</ReleaseDrop>
</PropertyGroup>
<!--build list of files to copy-->
<ItemGroup>
<SourceFiles Include="$(ProjectDir)\bin\*.exe" Exclude="$(ProjectDir)\bin\*test*.exe"/>
<SourceFiles Include="$(ProjectDir)\bin\*.cshtml" />
</ItemGroup>
<!--copy files-->
<Target Name="CopyFiles">
<MakeDir Directories="$(ReleaseDrop)" />
<Copy SourceFiles="#(SourceFiles)" DestinationFolder="$(ReleaseDrop)" />
</Target>
<!--after files are copied, list them then zip them-->
<Target Name="MakeRelease" DependsOnTargets="CopyFiles">
<ItemGroup>
<ZipFiles Include="$(ReleaseDrop)\*.*"/>
</ItemGroup>
<Zip ZipFileName="$(ReleaseDrop)\release.zip" Files="#(ZipFiles)" WorkingDirectory="$(ReleaseDrop)"/>
</Target>
</Project>
can be invoked like
msbuild <name of project file> /t:MakeRelease /p:ProjectDir=c:\projects

Move AfterBuild target to a .targets file

I have a AfterBuild target that I would like to use for multiple projects in a solution. Is there a way that I can put that target into a .targets file and reference the file in each project.
Below is what I tried which does not seem to work.
Project File:
<Import Project="..\debug.targets"/>
.Targets File:
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="AfterBuild">
<PropertyGroup>
<WebsiteDirectory>C:\Inetpub\wwwroot</WebsiteDirectory>
</PropertyGroup>
<ItemGroup>
<output Include=".\**\*.dll" Exclude=".\**\obj\**" />
<output Include=".\**\*.pdb" Exclude=".\**\obj\**" />
<output Include=".\**\*.svc" />
<output Include=".\**\*.xap" />
<output Include=".\**\*.aspx" />
<output Include=".\**\*.js" />
<output Include=".\**\*.config" />
</ItemGroup>
<PropertyGroup>
<VirtualDirectoryPath>$(WebsiteDirectory)\$(RootNamespace)</VirtualDirectoryPath>
</PropertyGroup>
<copy SourceFiles="#(output)" DestinationFiles="#(output->'$(VirtualDirectoryPath)\%(RecursiveDir)%(Filename)%(Extension)')" />
</Target>
Use this
<Import Project="$(MSBuildThisFileDirectory)\debug.targets"/>
$(MSBuildThisFile) = The current project file.
$(MSBuildThisFileDirectory) = The directory that contains current project file.
Relative paths in project files are difficult to use depending on what is invoking the project file. Using msbuild directly and the relative path will resolve to the project file. Use VS and the relative path will use the solution file as the base path.
Using $(MSBuildThisFileDirectory) will force the relative path to use a pre-determined beginning path. All you need to do is fill in the rest of the relative path.
What you are doing is fundamentally correct, but ensure that your Import statement is the last Import in the project file.
To verify that the target is being invoked correctly, run msbuild in diag mode from the command line and note the output regarding your target.
msbuild myproj.proj /v:diag

MSBuild project file: Copy item to specific location in output directory

In the process of cleaning up the folder/file structure on a project I inherited, I'm running into a problem with organizing the required external libraries. I want to keep them in their own .\dll\ folder, but they're not being copied to the build directory properly. They should be in the root build directory, but they're being moved to a subfolder instead.
My .csproj file contains the following xml:
<Project>
<ItemGroup>
<None Include="dlls\libraryA.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Then, on build, the libraryA.dll file is copied to the bin\Debug\dll\ folder, but I want it in the bin\Debug\ folder.
I tried this and msbuild always wants to copy the files using their directory path, but there is a workaround...
Edit the csproj file and after this line:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
Add these lines:
<PropertyGroup>
<PrepareForRunDependsOn>$(PrepareForRunDependsOn);MyCopyFilesToOutputDirectory</PrepareForRunDependsOn>
</PropertyGroup>
<Target Name="MyCopyFilesToOutputDirectory">
<Copy SourceFiles="#(None)" DestinationFolder="$(OutDir)" />
</Target>
The copy of the output files happens in the PrepareForRun target. This adds your own target to the list of targets that are executed as part of PrepareForRun.
This example copies all items in the None item group. You could create your own item group (e.g. MyFiles) and do the copy on that item group if you have other "None" files you don't want copied. When I tried this I had to change the item group name by editing the csproj file directly. Visual Studio did not allow me to set the item group of a file from the UI, but after I edited the csproj and changed it, Visual Studio displayed my custom item group name correctly.
If you only want to change it for one file, it may be easier to use the property:
<None Include="dlls\libraryA.dll">
<Link>%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Including content files in .csproj that are outside the project cone
This approach works
If you need to force copy of a specific file/nuget package into an asp.net core project (2.2), add at the end of your csproj :
<!-- Force copy MathNet because we need it in compilation -->
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="Build">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\MathNet.Numerics.4.8.1\lib\netstandard2.0\MathNet.Numerics.dll')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MathNet.Numerics.4.8.1\lib\netstandard2.0\MathNet.Numerics.dll'))" />
</Target>
<ItemGroup>
<ContentWithTargetPath Include="..\packages\MathNet.Numerics.4.8.1\lib\netstandard2.0\MathNet.Numerics.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>MathNet.Numerics.dll</TargetPath>
</ContentWithTargetPath>
</ItemGroup>
In SDK-style csproj you can write something like:
<Target Name="CopyFilesTargetName" AfterTargets="Build">
<Copy SourceFiles="$(OutDir)\dlls\Some.dll;$(OutDir)\dlls\SomeOther.dll" DestinationFolder="$(OutDir)" />
</Target>
You can also use <Move instead of <Copy to move files