Include additional files in build using MSBuild ProjectReference? - msbuild

I have a class library that has several files that it depends on, and those files must be packaged up for deployment with any project that depends on this library. Right now this means that I must customize each one of those dependent projects to ensure that they copy the files, in addition to adding the library as an MSBuild PrjectReference. When more files are added, all of the projects must be updated.
I've been looking through Microsoft.Common.targets for a way to include these files with the outputs of the library's own project file, so any project that has a ProjectReference to the library will automatically get the files when doing a build. I haven't gotten anything working yet, but I'm curious more generally if this is possible. It seems like it should be, and the _CopyFilesMarkedCopyLocal target even respects an otherwise-unused %(DestinationSubDirectory) metadata item that would allow for customized placement of those files, which would be perfect.
I believe what I'm missing is, for building a project A that depends on project B, the piece that adds the project outputs of project B into the items for project A's build.
EDIT: Leo's comment, I hadn't noticed that files marked with CopyToOutputDirectory are also copied to dependent project output directories because we use ItemGroups with names other than Content, EmbeddedResource, None, etc. Digging deeper, the target that uses those is GetCopyToOutputDirectoryItems and it appears to recursively call the MSBuild task to determine the project outputs, so I should be able to define some custom target that can be imported into our projects that adds our custom ItemGroups in the GetCopyToOutputDirectoryItems target, so that we don't have to use Content/None, etc.
However, the target that does the copying though is _CopyOutOfDateSourceItemsToOutputDirectoryAlways, which doesn't respect %(DestinationSubDirectory) unfortunately, and so all of these files are copied directly to $(OutDir).
My new goal is to see if there's some way to add custom files into the ReferenceCopyLocalPaths ItemGroup of dependent projects so that they are copied instead by the _CopyFilesMarkedCopyLocal target, which does utilize %(DestinationSubDirectory).

Adds custom build action for ItemGroups in the _CopyOutOfDateSourceItemsToOutputDirectoryAlways target
The above title should be closer to bwerks's goal. As test we could to know that the custom build action for ItemGroups would not copied to the Output directory, so we need to our custom build action to the target _CopyOutOfDateSourceItemsToOutputDirectoryAlways.
To accomplish this, open the file Microsoft.Common.CurrentVersion.targets in the MSBuild 15.0 folder C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\Microsoft.Common.CurrentVersion.targets(Make sure you have sufficient permissions and back up it), find the target _CopyOutOfDateSourceItemsToOutputDirectoryAlways:
<Copy
SourceFiles = "#(_SourceItemsToCopyToOutputDirectoryAlways)"
DestinationFiles = "#(_SourceItemsToCopyToOutputDirectoryAlways->'$(OutDir)%(TargetPath)')"
...
>
<Output TaskParameter="DestinationFiles" ItemName="FileWrites"/>
</Copy>
Then we could to know the copy source file is #(_SourceItemsToCopyToOutputDirectoryAlways), search _SourceItemsToCopyToOutputDirectoryAlways this in the targets, you will find:
<ItemGroup>
<_SourceItemsToCopyToOutputDirectoryAlways KeepMetadata="$(_GCTODIKeepMetadata)" Include="#(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='Always'"/>
<_SourceItemsToCopyToOutputDirectory KeepMetadata="$(_GCTODIKeepMetadata)" Include="#(ContentWithTargetPath->'%(FullPath)')" Condition="'%(ContentWithTargetPath.CopyToOutputDirectory)'=='PreserveNewest'"/>
</ItemGroup>
Could to know the source file is #(ContentWithTargetPath), keeping search the ContentWithTargetPath in the target, finally we got following:
<AssignTargetPath Files="#(Content)" RootFolder="$(MSBuildProjectDirectory)">
<Output TaskParameter="AssignedFiles" ItemName="ContentWithTargetPath" />
</AssignTargetPath>
So, we could to know how the target is to copy the default build action file to the output directory.
Now, go to our custom build action, we just need add our custom build action to the ItemName="ContentWithTargetPath", so add following in the file Microsoft.Common.CurrentVersion.targets:
<AssignTargetPath Files="#(MyBuildAction)" RootFolder="$(MSBuildProjectDirectory)">
<Output TaskParameter="AssignedFiles" ItemName="ContentWithTargetPath" />
</AssignTargetPath>
Save it.
For the file in the project file .csproj:
<ItemGroup>
<MyBuildAction Include="TextFile1.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</MyBuildAction>
</ItemGroup>
Hope this helps.

Related

Run target once for built project

I have a msbuild script that compiles the files and then copies them to a different directory. To ensure only the freshly built files are in the destination, I want to delete the destination folder before.
I tried running it before the Build target like this
<Target Name="RemoveRelease64" BeforeTargets="Build" Condition="'$(Platform)' == 'x64' and '$(Configuration)' == 'Release'">
<RemoveDir Directories="$(SolutionDir)bin64" />
</Target>
But this is called for every project that my main project depends on. When starting a build for Project A, it first builds Project B, C and D. For every built project the target is called and the folder deleted. As such it deletes the folder too often.
Is there a way to call a target only once for the project that the build got started for?
Is there a way to call a target only once for the project that the
build got started for?
I think you use Directory.Build.props or Directory.Build.targets as the msbuild script and write the RemoveRelease64 in that file.
If you put the file in the solution folder, as its scope, it will works for the projects from current folder and any sub folders.
So as a suggestion, if you want the msbuild script to only work for project A rather than any referenced projects B,C,D, you should put the msbuild script only in the project folder of A where A.csproj file exists .
Suggestion
1) If you use Directory.Build.props or Directory.Build.targets, delete it at the solution folder or any parent directories, add it only in the folder of Project A where A.csproj file exists.
2) Besides, you could abandon using Directory.Build.props/targets file. Instead, you could rename it as RemoveRelease64.targets and please remember not to use those two names.
Use msbuild import node in A.csproj file and then it will only executes for Project A rather than B,C,D:
Add this in A.csproj file and remove any Directory.Build.props or Directory.Build.targets in solution folders and project folders.
<Import Project="xxx\RemoveRelease64.targets" />

MSBuild - Project-specific targets for solution does not work

I have a solution that has multiple projects in it, including a web application. I want MSBuild to execute "WebPublish" target against the web application project and "default target" for all other projects in the solution.
This MSDN article says that I can do it specifying the command line
msbuild SlnFolders.sln /t:NotInSlnfolder:Rebuild;NewFolder\InSolutionFolder:Clean
But I never could make it work - MSBuild return an error, something like "NotInSlnFolder:Rebuild" target does not exist. It does not matter what target to specify, Build, Rebuild or Clean - it does not work in any case.
How can I achieve my goal of specifying project-specific targets for a solution?
The MSDN documentation does not work. Or have I missed something?
NOTE: This workaround is not officially supported by Microsoft, so there is no guarantee that it will work forever.
Short Answer
In folder with the SLN file, create the file before.{YourSolution}.sln.targets, with the following content: (Replace what in curly brackets to whatever you need.)
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="{MyCompany_MyProduct_WebApp:WebPublish}">
<MSBuild
Condition="'%(ProjectReference.Identity)' == '{$(SolutionDir)MyCompany.MyProduct.WebApp\MyCompany.MyProduct.WebApp.csproj}'"
Projects="#(ProjectReference)"
Targets="{WebPublish}"
BuildInParallel="True"
ToolsVersion="4.0"
Properties="BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)"
SkipNonexistentProjects="%(ProjectReference.SkipNonexistentProjects)" />
</Target>
</Project>
After that you can execute the command line:
msbuild {YourSolution}.sln /t:{MyCompany_MyProduct_WebApp:WebPublish}
Long Answer
If you add environment variable MSBUILDEMITSOLUTION, setting its value to 1, MSBuild will not delete temporary files generated for the solution and projects.
This will allow you to find {YourSolution}.sln.metaproj and {YourSolution}.sln.metaproj.tmp files generated in the solution folder, which are just standard MSBuild project files.
For MSBuild 3.5, the generated file is {YourSolution}.sln.cache and is retained regardless of environment variables. Analyzing those files, you will understand low-level details of the process and to see the customization opportunities available.
After executing MSBuild with some project-specific target in the .Metaproj file you will find out that the list of project-specific targets is hardcoded and only standard targets are supported (Build, Rebuild, Clean, Compile, Publish; note: Publish and WebPublish are not the same). MSBuild 3.5 only generates Clean, Rebuild and Publish targets as well as a target with just the project's name that means "Build".
You also can see that NotInSlnfolder:Rebuild is just a name of an autogenerated target. In reality MSBuild does not parse it and does not care about project names and location. Also note that the autogenerated target names specify the project name with solution folders hierarchy if it's in one, e.g. SolFolder\SolSubfolder\ProjectName:Publish.
One more critically important thing you will find: The MSBuild Target Name does not support dots. All dots in project names are replaced with underscores. For example, for a project named MyCompany.MyProduct.Components you will have to specify in the command line:
/t:MyCompany_MyProduct_Components:Rebuild
That's why even standard project-specific target Build didn't work - my project name contained dots.
Analyzing file {YourSolution}.sln.metaproj.tmp, you will find out that at runtime it tries to import targets from file named before.{YourSolution}.sln.targets and after.{YourSolution}.sln.targets, if those files exist. This has a key to the workaround for this MSBuild limitation/bug.
You can open your solution file in text editor and check whether following line is exist or not if not then you can add
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> inside the <Project> tag.
Hope this help you.

How to always execute a target in MSBuild

I have an MSBuild file that manipulates the AssemblyInfo file before the application is compiled. At the end of the build, it restores the AssemblyInfo file. It does this by backing up the file, manipulating it, and then after build time, restoring the file.
This works fairly well except when an error occurs during the build. It then does not restore the original file. Is there a way I can tell MSBuild to execute a target at the end of a build no matter if it succeeded or failed?
Based on your last comment to the original question I would take another approach, and forget the approach you are currently taking. You should know that your version info doesn't have to be in the AssemblyInfo.cs file. It can be in any code file, just as long as you only have attributes AssemblyVersion and AssemblyFileVersion defined once each. With that being said what I would do is follow these steps:
Remove AssemblyVersion & AssemblyFileVersion from AssemblyInfo.cs
Create a new file, name it whatever you want want in my case I put it at Properties\VersionInfo.cs. Do not add this file to the project.
Edit the project file to include that file into the list of file to be compiled only when you want it
Let's expand a bit on #3. When you build a .NET project, the project itself is an MSBuild file. Inside that file you will find an item declared Compile. This is the list of files that will be sent to the compiler to be compiled. You can dynamically include/exclude files from that list. In you case you want to include the VersionInfo.cs file only if you are building on the build server (or whatever other condition you define). For this example I defined that condition to be if the project was building in Release mode. So for Release mode VersionInfo.cs would be sent to the compiler, and for other builds not. Here are the contents of VersionInfo.cs
VersionInfo.cs
[assembly: System.Reflection.AssemblyVersion("1.2.3.4")]
[assembly: System.Reflection.AssemblyFileVersion("1.2.3.4")]
In order to hook this into the build process you have to edit the project file. In that file you will find an element (maybe more than 1 depending on project type). You should add a target similar to the following there.
<Target Name="BeforeCompile">
<ItemGroup Condition=" '$(Configuration)'=='Release' ">
<Compile Include="Properties\VersionInfo.cs" />
</ItemGroup>
</Target>
Here what I've done here is to define a target, BeforeCompile, which is a well-known target that you can override. See this MSDN article about other similar targets. Basically this is a target which will always be called before the compiler is invoked. In this target I add the VersionInfo.cs to the Compile item only if the Configuration property is set to release. You could define that property to be whatever you wanted. For instance if you have TFS as your build server then it could be,
<Target Name="BeforeCompile">
<ItemGroup Condition=" '$(TeamFoundationServerUrl)'!='' ">
<Compile Include="Properties\VersionInfo.cs" />
</ItemGroup>
</Target>
Because we know that TeamFoundationServerUrl is only defined when building through TFS.
If you are building form the command line then something like this
<Target Name="BeforeCompile">
<ItemGroup Condition=" '$(IncludeVersionInfo)'=='true' ">
<Compile Include="Properties\VersionInfo.cs" />
</ItemGroup>
</Target>
And when you build the project just do msbuild.exe YourProject.proj /p:IncludeVersion=true. Note: this will not work when building a solution.
What about changing the problem:
Add a "template" AssemblyInfo.cs.template to version control that represents your "ideal" AssemblyInfo.cs with regex hooks in there
Before build, copy the template to the real and apply your regexes
Add some kind of subversion ignore for AssemblyInfo.cs (I'm no svn expert, but I'm pretty sure there is a way you can tell it to ignore certain files)
In the event that your devs need to add some kind of customization that would normally appear in an AssemblyInfo.cs (eg InternalsVisibleTo), then get them to add it to a different .cs file that IS checked in.
As a further refinement, combine Sayed's solution with mine and remove version info stuff from the actual AssemblyInfo.cs and have a VersionInfo.cs.template that is checked in, that creates a VersionInfo.cs in BeforeBuild.
I never used it, but from the documentation it seems that the OnError Element is useful to what you're trying to achieve.
Causes one or more targets to execute,
if the ContinueOnError attribute is
false for a failed task.

How to aggregate outputs from a dynamically generated set of projects into a single folder

I need to collect into a single folder all test assemblies, with their dependencies, and configuration files. The process should preserve the directory structure from the output of each test project. We have a solution that requires manually attaching test projects to a master project, but our solution has far too many projects for this to be maintainable. These should be located automatically based on naming convention (x.UnitTest.csproj, y.IntegrationTest.csproj).
For background, we are working with a build system that passes artifacts (binaries, etc) between agents. We are compiling on one agent, and testing on other agents. The massive duplication of assemblies between test projects is slowing the build process down.
What I have done:
1) I have a csproj that references most of the test projects. This gets binaries and dependencies into one folder.
2) I am able to identify all files to copy using this
<CreateItem
Include="%(ProjectReference.RootDir)%(ProjectReference.Directory)$(OutDir)*.config">
<Output TaskParameter="Include" ItemName="TestConfigurationFiles"/>
</CreateItem>
<Copy
SourceFiles="#(TestConfigurationFiles)"
DestinationFolder="$(OutDir)">
</Copy>
I've attempted most obvious things, such as
MsBuild task: RebaseOutputs attribute, overriding the OutDir property. I can provide the msbuild task with a dynamically generated set of outputs, but can only build them in their default folder.
Hooking into the TargetOutputs of
msbuild task gives only the primary
output assembly (without
dependencies).
I experimented with "Copy Always" for
configuration files. This puts them
in the output directory of the
dependent project as "app.config" not
"dllname.config", and not in the
final project.
Solutions that could make this better might include
Provide an example of adding to the projectreference item array dynamically, before compilation.
Use msbuild TargetOutputs to create a list of all files in the folder (instead of just the primary output) and copy to a destination folder.
Today I'm using msbuild 3.5. Ideally the solution would work with msbuild 3.5. We are transitioning to .NET 4 / MsBuild 4 soon, so, if must be done in .Net 4, that is fine.
Have you considered flattening the folder structure when you export your artifacts?
Something like:
src/*.UnitTest*/bin/**/*.* -> /testlibs
src/*.IntegrationTest*/bin/**/*.* -> /testlibs

MSBUILD Configuration: Copy output to staging area

Ok, I am not the best at MSBUILD. Actually, I don't know anything. I need some help configuring my solution file to tell MSbuild to copy the compiled output into a staging area. The solution has about 35 projects. All I need is around 5 of them the to be copied to the staging folder in release mode. Please assume I know nothing...
Thanks
You could create a Target that looks like this:
<Target Name="CopyFiles" DependsOnTargets="YourBuildTargets">
<CreateItem Include="YourSolutionPath\bin\$(Configuration)\*.*">
<Output ItemName="YourProjectOutputFiles" TaskParameter="Include"></Output>
</CreateItem>
<Copy SourceFiles="#(YourProjectOutputFiles)" DestinationFolder="$(DestinationFolder)"></Copy>
</Target>
This will create a target called CopyFiles which depends on the completion of YourBuildTargets (You can put multiple dependencies in there, separated with semi colons). It describes an Item which includes all of the files (*.*) in the project directory. Then it runs the Copy command, and copies the files described by the item to the destination folder. If you have lots of projects all being build by 1 MSBuild script, you would have a CreateItem node for each project to index the files, and a corresponding Copy node to perform the copy.
Or if you just want to do this for each project separately, just put this block inside each .csproj file.
Now just include the CopyFiles target in the list of targets that gets build by your script.