MSBuild Post-Build - c#

I've got an MSBuild script that is just about doing everything that I need it to do apart from my post-build step (see a previous question that I asked: MSBuild conditional Exec?).
What I'm looking to do is build many csproj files and optionally perform post-build steps if and only if the project was built. I don't want to perform my post-build step all the time or else the timestamp on my final output will be modified unnecessarily (and it makes the build process very time consuming for no reason).
In my MSBuild script I've got something like the following for each of my csproj files:
<Target Name="ProjectName">
<MSBuild Projects="PathToProject" Properties="Configuration=$(buildtype)" />
</Target>
Edit:
I think what I really want to do is detect when the CoreCompile task runs for each project. If there were some way to check for this in a condition?
Any ideas?
I'm new to MSBuild so maybe I'm on completely the wrong track!
Thanks,
Alan

You can also do it based on the configuration selected in your build process. For CI, you should always use "Release" or "Production" (you can define your own).
<Exec Condition="'$(ConfigurationName)'=='Release'" Command="your command goes here ..."/>

After much searching for a simple solution to this problem I didn't find one and ended up coming up with a solution of my own that works but may not be the best solution. However, I wanted to share it with anyone else that is having the same problem so that you can at least have a working solution and hopefully saving you a lot of head banging.
To recap, what I wanted to do was run a command line tool after my project was built but only if the assembly was updated (i.e. the timestamp changed). I didn't want to put this into the post-build section of every project because I only wanted the post-build to happen on our build server (not development machines).
I didn't find any way of doing this externally in my main .proj file and did end up altering the post-build section of each .csproj file. However, I prefixed it with an if condition something like this:
if '$(ExecuteCommand)' == 'true' command.exe
This means that the command will never be executed on the development machine but when I invoke the build from my .proj file I can set that flag to true like this:
<!-- Define common properties -->
<PropertyGroup>
<ExecuteCommand>true</ExecuteCommand>
</PropertyGroup>
<Target Name="YourTarget">
<!-- Build project -->
<MSBuild Projects="Path to project" Properties="ExecuteCommand=$(ExecuteCommand)" />
</Target>
As I said, I don't think it is the most graceful solution but it certainly works and will be sufficient for me for the time being. However, I'd still be interested to hear what the proper way of achieving this is so that I can improve my script.
Thanks,
Alan

If you can add the following to each of your projects:
<Target Name="DoStuffWithNewlyCompiledAssembly">
<Exec Command="command.exe" />
</Target>
... then you only need to add a property:
<Target Name="Name">
<MSBuild Projects="" Properties="TargetsTriggeredByCompilation=DoStuffWithNewlyCompiledAssembly" />
</Target>
This works because someone smart at Microsoft added the following line at the end of the CoreCompile target in Microsoft.[CSharp|VisualBasic][.Core].targets (the file name depends on the language and MSBuild/Visual Studio version).
<CallTarget Targets="$(TargetsTriggeredByCompilation)" Condition="'$(TargetsTriggeredByCompilation)' != ''"/>
So if you specify a target name in the TargetsTriggeredByCompilation property, your target will run if CoreCompile runs-- and your target will not run if CoreCompile is skipped (e.g. because the output assembly is already up-to-date with respect to the code).

Related

How to properly package a Nuget distributed custom tool that generates source code as part of the build

Thanks to this awesome article by Nate McMaster, I know how to package a .NET core console application as a Nuget package that automatically installs itself as a (pre, in this instance) build task.
To test if everything works, I simply had my custom tool write out a public C# class.
Here is the complete and runnable sample on Github.
However, the file that my custom tool adds isn't really part of the build (the first one that actually generates the file) and therefore the introduced class is not in the assembly after the first build (see Line 38 here). However, because the .NET core projects now automatically include all .cs files alongside the project, it builds the new class into the output on subsequent builds (see Line 57 here).
The generated files don't go away on clean, though and generally don't behave like something an MSBuild task would output. However, because the exec happens in a targets file, we ought to have access to all the machinery to make this happen. So my question is:
How do I correctly execute a custom build tool (console app) that needs to examine the project, its files and generate source code (preferably in obj/ as say <foo>.g.cs that gets compiled into the resulting assembly as part of a single build? Ideally, this generated file(s) shouldn't appear in the solution explorer, either.
Help!
When generating the intermediate file (CustomTool.g.cs) in the intermediate folder (you'll need to resolve it, see example in Refit library: https://github.com/reactiveui/refit/blob/5b4e14aaf8a1fcc27396b7c08171d100aba1b97d/Refit/targets/refit.targets#L11); you need to explicitly add it as a compile item.
Taking your example targets file (https://github.com/aniongithub/CustomTool/blob/master/CustomTool/RunCustomTool.targets#L13):
<Project>
<PropertyGroup>
<IntermediateOutputPath Condition="$(IntermediateOutputPath) == '' Or $(IntermediateOutputPath) == '*Undefined*'">$(MSBuildProjectDirectory)obj\$(Configuration)\</IntermediateOutputPath>
<!-- Command to invoke CustomTool -->
<CustomTool>dotnet "$(MSBuildThisFileDirectory)/netcoreapp2.2/CustomTool.dll"</CustomTool>
<!-- Other variables -->
<CustomVariable>"$(MSBuildProjectDir)"</CustomVariable>
</PropertyGroup>
<Target Name="CustomTool" BeforeTargets="CoreCompile" DependsOnTargets="PrepareForBuild">
<Exec Command="$(CustomTool) $(ProjectPath) $(IntermediateOutputPath)CustomTool.g.cs" />
<!-- add generated file as a compile item, otherwise it won't get picked up -->
<ItemGroup Condition="Exists('$(IntermediateOutputPath)\CustomTool.g.cs')">
<Compile Include="$(IntermediateOutputPath)\CustomTool.g.cs" />
</ItemGroup>
</Target>
</Project>

How to specify output folder for the referenced nuget packages?

I have a project with some nuget packages referenced.
In output folders (bin\Debug or bin\Release), all referenced libraries lie next to the executable.
How to specify output folder for libraries?
I want all nuget libraries in bin\Release\Libs and executable in bin\Release.
I woke up early this morning and decided to have a go at doing it myself. Turned out to be pretty quick, but that may be because of my (unfortunate) experience with looking into MSBuild files. Writing this post took me far longer than writing the target.
From your question, I assume you're using a traditional project, since SDK style projects only create the project's assembly in the bin directory. However, I much prefer SDK style projects because use can quickly and easily use the dotnet cli to create test projects and the csproj is much more easily editable. So, I'll give you my steps to find my solution for SDK style projects, and you need to follow along to do something similar with a traditional project.
So, we want to change where a files are being copied, which means we need to modify some items. Everything in MSBuild runs in a target, so we'll need to know when to run our custom target, what items to modify and probably what metadata of those items to modify. I created a new project, added some NuGet references then ran dotnet msbuild -t:publish -bl and opened the msbuild.binlog file.
What metadata to change
Searching for the name of a dll that came from a nuget package, I find a message saying copied from ... to ..., so I click on it to go to the entry, and follow the tree back to the task, which I see is the built-in Copy task. The target path to the task is Publish -> _PublishBuildAlternative -> ComputeAndCopyFilesToPublisDirectory -> CopyFilesToPublishDIrectory -> _CopyResolvedFilesToPublishAlways. Double clicking the copy task I see
<Copy SourceFiles = "#(_ResolvedFileToPublishAlways)"
DestinationFiles="#(_ResolvedFileToPublishAlways->'$(PublishDir)%(RelativePath)')"
OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
Retries="$(CopyRetryCount)"
RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
UseHardlinksIfPossible="$(CreateHardLinksForPublishFilesIfPossible)"
UseSymboliclinksIfPossible="$(CreateSymbolicLinksForPublishFilesIfPossible)">
So, I can guess I need to modify the RelativePath metadata of an _ResolvedFileToPublishAlways item.
What item to change
Side note: MSBuild doesn't have public/private modifies, so instead a convention is generally used. Anything starting with an underscore should be considered to be an implementation detail that could change between releases, so it's better to use things that do not start with an underscore, and the teams who maintain the targets file should try harder not to break compatibility.
So, since _ResolvedFileToPublishAlways starts with an underscore, let's find out where it was created. Searching for it takes me to a target where the binlog tells me it was added, in a target called _ComputeResolvedFilesToPublishTypes, and its definition is
<Target Name="_ComputeResolvedFilesToPublishTypes">
<ItemGroup>
<_ResolvedFileToPublishPreserveNewest Include="#(ResolvedFileToPublish)"
Condition="'%(ResolvedFileToPublish.CopyToPublishDirectory)'=='PreserveNewest'" />
<_ResolvedFileToPublishAlways Include="#(ResolvedFileToPublish)"
Condition="'%(ResolvedFileToPublish.CopyToPublishDirectory)'=='Always'" />
</ItemGroup>
</Target>
So, I can see that it's simply copying ResolvedFileToPublish items to new item names. Looking for where those items are created, it's in a target named ComputeFilesToPublish, and expanding the tree to see all the items created and their metadata, I'm going to guess the items I want to modify all have AssetType = runtime, which is perfect for a condition we're going to need to use.
When to run our target
Ideally I would run just before CopyFilesToPublishDirectory, however looking at its definition I see
<Target Name="CopyFilesToPublishDirectory"
DependsOnTargets="_CopyResolvedFilesToPublishPreserveNewest;
_CopyResolvedFilesToPublishAlways" />
The problem is that when MSBuild executes a target it runs in this order:
Any targets listed in DependsOnTargets
Any target that lists the current target as BeforeTargets
The current target
Any targets that lists the current target as AfterTargets
So, while I want to run BeforeTargets='CopyFilesToPublishDirectory', the DependsOnTargets will run before my target, so I can't do that. So I'll choose to run AfterTargets="ComputeFilesToPublish". There are other targets that run in between those, and one sounds like that it might add ResolvedFileToPublish items, but with my current project the target doesn't run because of conditions, so my custom target might not be generic enough to work for all projects.
Writing our custom target
So now we know when our target will run, which items it will modify and how we will modify their metadata.
<Target Name="RedirectRuntimeFilesToBinDirectory" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<ResolvedFileToPublish Condition=" '%(ResolvedFileToPublish.AssetType)' == 'runtime' ">
<RelativePath>lib\%(RelativePath)</RelativePath>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
Unfortunately the binlog doesn't show the details about the metadata being modified, which is a real pain in the arse when trying to debug build issues and why some items have unexpected values, but in any case I've now successfully changed the destination of NuGet dependencies, and probably project to project references, to a lib\ directory.
Grace to the zivkan's investigation I found the answer. Traditional project has target CopyFilesToOutputDirectory which depends on _CopyFilesMarkedCopyLocal target. In this last one we have task Copy:
<Copy
SourceFiles="#(ReferenceCopyLocalPaths)"
DestinationFiles="#(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')"
SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
Retries="$(CopyRetryCount)"
RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
UseHardlinksIfPossible="$(CreateHardLinksForCopyLocalIfPossible)"
UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyLocalIfPossible)"
Condition="'$(UseCommonOutputDirectory)' != 'true'"
>
And here I found metadata DestinationSubDirectory which is exactly what I need to change.
So finally
First, we need to change csproj file and add these lines:
<ItemDefinitionGroup>
<ReferenceCopyLocalPaths>
<DestinationSubDirectory>lib\</DestinationSubDirectory>
</ReferenceCopyLocalPaths>
</ItemDefinitionGroup>
Second, we need to change app.config file to let the assembly know the path to the libraries:
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="lib;libs" />
</assemblyBinding>
</runtime>
That's all. All referenced libraries will be copied into subfolder lib

Using msbuild.exe to build a project with a dependency requiring a different Platform property

I have two fairly straightforward C# projects: An executable that can build as either x86 or AnyCPU, which references (via <ProjectReference>) a DLL project that only has an AnyCPU configuration. This all works as expected within Visual Studio.
I am trying to build the x86 version of the executable project (and its dependencies) from the command line, with /p:Platform="x86". This causes the build of the DLL project to fail. (Whereas /p:Platform="AnyCPU" works, presumably because it is valid for both projects.)
The full command line I am using is:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\msbuild MyProject\MyProject.csproj /t:Build /p:Configuration="Release" /p:Platform="x86"
What are my options for getting this build to work from the command line? Preferably without modifying the DLL project at all, or modifying the projects in ways that interfere with using them normally in Visual Studio.
(The ultimate goal here is a batch file that can build a clean version of the project for distribution.)
Additional info:
Both projects have "Debug" and "Release" configurations. The executable project has "x86" and "AnyCPU" available under Platform. The DLL project has only "AnyCPU" available under Platform. The "Platform target" option matches the "Platform" in all cases. (There is no "Prefer 32-bit" option, as I am on VS2010.)
The error seems to be a compilation-related error ("no unsafe code allowed") in the DLL, which -- although I am not 100% sure -- seems to be because none of the <PropertyGroup> elements in the DLL project are being matched (due to Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' " etc) that would specify necessary parameters for compilation (eg AllowUnsafeBlocks and DefineConstants).
Answering my own question... First of all, MSBuild can build solution files. This does exactly what you'd expect:
msbuild MySolution.sln /p:Configuration="Release" /p:Platform="x86"
The result is an x86 executable with an AnyCPU DLL (as the solution specifies).
There are a few people out on the internet suggesting that its behaviour is not a perfect match for Visual Studio in some obscure cases. But it seems to work just fine for my purposes. (I think they were having issues with the order things get built in.)
I knew that MSBuild could build a solution file, but -- oops -- I neglected to test it on my simple reproduction case, after it failed on the more complex thing I'm working on.
The above, alone, isn't enough for a fully satisfying answer, particularly if there is a need to customise things. The way MSBuild builds solution files is to create a dummy project file, based on the solution file. This can be inspected by first setting an environment variable like so:
set MSBuildEmitSolution=true
This will emit the dummy project file next to the solution file, which can then be inspected.
I haven't completely analysed what it is doing, but it looks fairly straightforwardly like it is using the <MSBuild> task with the Projects parameter that is itself passing in the solution-specified Configuration and Platform appropriate for each project. According to the documentation it seems to be using the ones specified in AdditionalProperties. (This also seems useful to know.)
For reference, here is some relevant code extracted from the generated project file:
<Target Name="Build" Outputs="#(CollectedBuildOutput)">
<MSBuild Projects="#(ProjectReference)" BuildInParallel="True" Properties="BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" SkipNonexistentProjects="%(ProjectReference.SkipNonexistentProjects)">
<Output TaskParameter="TargetOutputs" ItemName="CollectedBuildOutput" />
</MSBuild>
</Target>
Where #(ProjectReference) is grabbing data from:
<ItemGroup>
<ProjectReference Include="X:\Solution\MyProject\MyProject.csproj">
<ToolsVersion>
</ToolsVersion>
<SkipNonexistentProjects>False</SkipNonexistentProjects>
<AdditionalProperties>Configuration=Release; Platform=x86; VisualStudioVersion=10.0</AdditionalProperties>
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectReference>
<ProjectReference Include="X:\Solution\DLLProject\DLLProject.csproj">
<ToolsVersion>
</ToolsVersion>
<SkipNonexistentProjects>False</SkipNonexistentProjects>
<AdditionalProperties>Configuration=Release; Platform=AnyCPU; VisualStudioVersion=10.0</AdditionalProperties>
<Configuration>Release</Configuration>
<Platform>AnyCPU</Platform>
</ProjectReference>
</ItemGroup>
(Note the different AdditionalProperties.)

How to build dependent project first with msbuild

I have just started looking into msbuild, because I want to make my own build scripts. For now I am able to create build scripts that compiles only one project, but how do I handle dependencies?
For example what if I have two projects that gets build with these two msbuild scripts?
projectA.xml
projectB.xml
How do I tell msbuild that when I am executing projectB.xml that it should first execute projectA.xml?
I have googled alot on this, but it does not seem to get anything that a starter like me understands. I would be more than happy with a link to an article describing this, or maybe just a small code example.
The reason why I want this control is because of a library I am building. The library consists of several projects. A developer should be able to pull the source code for the library down and build only the libraries that he wants.
Actually I want to be able to build .net modules from the different projects. That is why I want to be able to run a customized msbuild script.
If you create a solution with the two projects you can target the .sln file with msbuild, rather than directly building the projects, it should take care of project dependencies :)
But that's if you're using standard .csproj projects...
Ok I looked at a project I'm working on, and it's like this:
<ItemGroup>
<ProjectReference Include="..\SomeFolder\SomeProject.csproj">
<Project>{1A94B405-2D01-4A09-90D5-A5B31180A03B}</Project>
<Name>SomeProjectNamespace</Name>
</ProjectReference>
</ItemGroup>
And here's an MSDN page about references. Scroll down till you find ProjectReference...
I setup my build scripts so that I have a few common targets that do not do anything, but use DependsOnTargets to setup project dependencies and run the build.
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- ************************************************************************************************ -->
<!-- Targets that run the builds -->
<!-- ************************************************************************************************ -->
<Target Name="AutoBuild" DependsOnTargets="BuildProject1;BuildProject2;BuildInstallers">
<OnError ExecuteTargets="NotifyFailure" />
</Target>
<Target Name="FullCompile" DependsOnTargets="BuildProject1;BuildProject2">
<OnError ExecuteTargets="NotifyFailure" />
</Target>
<!-- Build Project 1 -->
<Target Name="BuildProject1">
<!-- Use MSBuild task and point it to build project1.csproj, project1.sln or whatever your projects is -->
</Target>
<!-- Build Project 2 -->
<Target Name="BuildProject2">
<!-- Use MSBuild task and point it to build project2.csproj, project2.sln or whatever your projects is -->
</Target>
<Target Name="BuildInstallers">
<!-- Whatever logic you have for building installers -->
</Target>
</Project>
In MSBuild issue #2887 a similar situation is discussed. The thread also reveals a link to official ProjectReference Protocol.
You dont need to build using the sln. If you use project references in your csproj then the dependency order is taken care of by MSBuild.
Try it. Automajically.
You do not need to sort the dependency order in your msbuild script.

MS-Build BeforeBuild not firing

I'm customizing a .csproj project to run some custom tasks before the main build. However, I can't get the tasks to execute at all.
I uncommented the <Target Name="BeforeBuild" /> element in the .csproj file and added a simple Message task, but when I build, the message doesn't appear in my output, so it seems the task isn't running. So this fragment does not output the message;
Listing 1: No Message Appears
<Target Name="BeforeBuild">
<Message Text="About to build ORM layer" Importance="normal" />
</Target>
However, if I screw with some of the attributes, I can get the .csproj to fail to execute at all;
Listing 2: An MSBuild configuration error
<Target Name="BeforeBuild">
<Message Text="About to build ORM layer" XXImportance="normal" />
</Target>
Note the XXImportance attribute. The build error I get is
My.csproj(83,46): error MSB4064: The "XXImportance" parameter is not supported by the "Message" task. Verify the parameter exists on the task, and it is a settable public instance property.
This suggests that the XML is being parsed, that the Message class has been found, and that the class is being reflected over for the available properties.
Why would the task not execute?
I'm using the 3.5 framework.
UPDATE 1: On #Martin's advice, I tried to run MSBuild on the console, and got this error;
c:\path\to\my.csproj(74,11): error MSB4019: The imported
project "C:\Microsoft.CSharp.targets" was not found. Confirm
that the path in the declaration is correct, and that
the file exists on disk.
Line 74 reads;
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
UPDATE 2: I'm compiling in VS2008, which uses the C#3 compiler, but the project I'm compiling is a framework 2.0 project. When run from the command line (see UPDATE 1) the build seems to fail because there is a confusion as to where the Microsoft.CSharp.targets file is specified.
Had the same problem today and found the way to make it work.
The BeforeBuild target in your .csproj file is intended to be a redefinition of a target defined (and referenced) in the Microsoft.Common.targets file, which is imported by the Microsoft.CSharp.targets file, which in turn is imported by your .csproj.
Your problem is that the line in your .csproj that imports Microsoft.CSharp.targets is after your definition of the BeforeBuild target. Move the import line to above your BeforeBuild target and everything should work fine.
Hope that helps,
The event is firing, but you might need to change your settings in VS:
Tools->Options->Projects and
Solutions->Build and Run:
And set MSBUild verbosity to minimal or normal.
Also, if you compile through msbuild in the console you will see the message without having to change the above settings.

Categories