Assembly Version Captured Before "BeforeBuild" Target - msbuild

I am using MSBuild.Community.Tasks to help with two things, namely adjusting the version and zipping up a file. I am not married to this, so an alternate approach is welcome provided it produces what I'm looking for. The goal is to increment the build number before the build, then ZIP up a new DLL (with a couple of other files) after the build. The ZIP file should be named according to the build.
I am almost there, however, my version number in my DLL is always one step behind my version.txt file (auto-gen'd from the Version task). Here is what I have in the BeforeBuild target:
<Target Name="BeforeBuild" BeforeTargets="PrepareForBuild">
<Message Text=" --=== Before Build ===--"></Message>
<ItemGroup>
<PreviousFiles Include="$(MSBuildProjectDirectory)\BuildPackage\$(AssemblyName).*.zip">
<InProject>false</InProject>
</PreviousFiles>
</ItemGroup>
<Delete Files="#(PreviousFiles)"></Delete>
<Delete Files="$(MSBuildProjectDirectory)\BuildPackage\$(AssemblyName).dll"></Delete>
<Version VersionFile="version.txt" RevisionType="Increment">
<Output TaskParameter="Major" PropertyName="Major" />
<Output TaskParameter="Minor" PropertyName="Minor" />
<Output TaskParameter="Build" PropertyName="Build" />
<Output TaskParameter="Revision" PropertyName="Revision" />
</Version>
</Target>
This deletes any files from the previous build, then increments the version.txt file correctly.
Next, in my AfterBuild target I have put this together:
<Target Name="AfterBuild" AfterTargets="Build">
<Message Text=" --=== After Build ==--"></Message>
<AssemblyInfo CodeLanguage="CS" OutputFile="AssemblyVersion.cs" AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)" AssemblyFileVersion="$(Major).$(Minor).$(Build).$(Revision)" />
<ItemGroup>
<ProjectOutputFiles Include="bin\$(AssemblyName).dll">
<InProject>false</InProject>
</ProjectOutputFiles>
<ZipFiles Include="$(MSBuildProjectDirectory)\BuildPackage\*.*" Exclude="$(MSBuildProjectDirectory)\BuildPackage\*.zip">
<InProject>false</InProject>
</ZipFiles>
</ItemGroup>
<Copy SourceFiles="#(ProjectOutputFiles)" DestinationFolder="$(MSBuildProjectDirectory)\StorePackage" />
<Zip Files="#(ZipFiles)" WorkingDirectory="$(MSBuildProjectDirectory)\BuildPackage" ZipFileName="$(MSBuildProjectDirectory)\BuildPackage\$(AssemblyName).$(Major)-$(Minor)-$(Build)-$(Revision).zip" ZipLevel="9" />
</Target>
Basically, I'm updating AssemblyInfo.cs and specifying some file groups. I then copy the project output over and finally ZIP up the required files.
This all works great, except my DLL version is always 1 revision number behind my actual revision number - i.e., what is stored in version.txt and what the name of the .ZIP file is saved out as.
Am I missing something obvious here? It's like the version is captured before the build process even starts or something.
Thanks in advance.

GOSH, yes, I am missing something obvious. As soon as I re-read the question, right before posting, it became clear what I had done incorrectly. I even spelled it out.
The fix was to move the AssemblyInfo task up to the BeforeBuild target. This is the task that outputs the .cs file containing the attributes required to inject the correct versioning information into the DLL. Leaving the update of this file until after the build has completed meant that my DLL was going to be losing the race in perpetuity.
I'm going to go ahead and post this anyways, in case someone else runs into something similar.
Cheers.
Edit For what it's worth, the final version of these targets required moving the ZIP operation out into a separate target (I called it ZipProjectOutput) because of a race condition that I couldn't resolve: the DLL was never showing up in the ZIP file, and I think that the timing of the OS releasing a lock on the file or something might have been to blame.
I used the AfterTargets="AfterBuild" to have the ZipProjectOutput target execute when the other was complete. I'm not entirely happy about this as I'm not certain I'm just "winning" the race here, rather than solving the problem, but this is working for me now rather slickly. #WFM

Related

.Net Core MS Build Package Creation Lifecycle

MSBuild quite powerful and important tool, but sometimes configuration of it is the same hard as making a spaceship or flying to Mars. Answers on next questions can help makes it a little bit more usefull and developer friendly:
- How the MSBuild create a Package(zip)?
- Does it use some temporary folders or direct from builds output ?
- Why can the output folder content differ from a package content
- What events can be used to add some custom logic?
The answers to this question could help me with the next long story short:
I'm working with a .Net Core solution, that has the dependency on some other solutions DLLs files( that are not referenced directly, but required to be in the solution folder). On post-build event CL there is the option added as an entry point "\TaskEP" that is a starting point of the next pipeline :
<PropertyGroup>
<PipelineCopyAllFilesToOneFolderForMsdeployDependsOn>
TaskEP;
Task2;
Task3;
$(PipelineCopyAllFilesToOneFolderForMsdeployDependsOn);
</PipelineCopyAllFilesToOneFolderForMsdeployDependsOn>
<RunPostBuildEvent>Always</RunPostBuildEvent>
Tasks are mostly done for add extra DLLs to package and in general looks like :
<Target Name="TaskEP" Condition="$(SomeConditions) == 'True'">
<Message Importance="high" Text="TaskEP: Copying some files..." />
<ItemGroup>
<ItemsName Include="$(MSBuildProjectDirectory)\..\..somepath..\Extra.dll;....dll" />
<FilesForPackagingFromProject Include="%(ItemsName.Identity)">
<DestinationRelativePath>bin\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
</Target>
Basically, for me, this instruction to MSBuild to add "Extra.dll" to output package.
Pubxml is quite usual :
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
<_PackagePathShortened Condition="'$(_PackagePathShortened)' == ''">SuperWebSite</_PackagePathShortened>
<ExcludeApp_Data>False</ExcludeApp_Data>
<PackageLocation>..\..\..\..\BIN\WEB\Release\Publish\Super.WebApplication.zip</PackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<TargetFramework>netcoreapp2.1</TargetFramework> ....
And one instruction that replaces the long long path in the package to Short and defined one:
<Target Name="AddReplaceRuleForAppPath" BeforeTargets="BeforePublish">
<Message Text="Adding replace rules for application path '$(PublishIntermediateOutputPath)' replace with '$(_PackagePathShortened)'" Importance="high" />
<EscapeTextForRegularExpressions Text="$(PublishIntermediateOutputPath)">
<Output PropertyName="_PackagePathRegex" TaskParameter="Result" />
</EscapeTextForRegularExpressions>
<!-- Add a replace rule for VSMSDeploy resp. MSdeploy to update the path -->
<ItemGroup>
<MsDeployReplaceRules Include="replaceFullPath">
<Match>$(_PackagePathRegex)</Match>
<Replace>$(_PackagePathShortened)</Replace>
</MsDeployReplaceRules>
</ItemGroup>
I also duplicated the same "Copy" tasks here in .pubxml file , play with different events "AfterBuild", "BeforePublish" but seems that all of the just ignoring. I can see the "Extra" DLLs files in the build output directory and all Info Messages on publishing, but not in the final "Super.WebApplication.zip" file !
The solution I found to copy extra files in .Net Core is taken from this documentation
<ItemGroup>
<DotnetPublishFiles Include="$(MSBuildProjectDirectory)\..\..somepath..\Extra.dll;....dll" >
<DestinationRelativePath>bin\x86\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</DotnetPublishFiles>
</ItemGroup>

How to generate files during build using msbuild

Does anyone know how to modify a csproj file in a way to generate code files during build without actually referencing the files?
A process like :
create file,
dynamically reference temporary file during build
compiled assembly has additional members, depending on the files created during build
The purpose of this is to create a way of generating code files using roslyn instead of using t4 templates, which are very awkward to use once you're trying to do something depending on attributes.
Hence i am planning on providing a way to use a special csharp file (for full syntax support) to generate files programatically based on the contents of that special file.
I've spent a couple of weeks looking into resources on the internet (with the topic msbuild), but until now it seems i didn't use the right keywords.
This one has been the most insightful one to me yet:
https://www.simple-talk.com/dotnet/.net-tools/extending-msbuild/
My guess is, that the correct build target for my purpose should be "BeforeCompile" in order to somehow populate the build process with custom code files.
Does anyone have experience with my issue, or is aware of any particular resources which deal with the task?
Solution i got it working with:
<UsingTask TaskName="DynamicCodeGenerator.DynamicFileGeneratorTask" AssemblyFile="..\DynamicCodeGenerator\bin\Debug\DynamicCodeGenerator.dll" />
<Target Name="DynamicCodeGeneratorTarget" BeforeTargets="BeforeBuild;BeforeRebuild">
<DynamicFileGeneratorTask>
<Output ItemName="Generated" TaskParameter="GeneratedFilePaths" />
</DynamicFileGeneratorTask>
<ItemGroup>
<Compile Include="#(Generated)" />
<FileWrites Include="#(Generated)" />
<!-- For clean to work properly -->
</ItemGroup>
</Target>
Unfortunately i did not get it to work with a propertygroup override as suggested
Update: This link is interesting too: https://github.com/firstfloorsoftware/xcc/blob/master/FirstFloor.Xcc/Targets/Xcc.targets
Generating the code file can be achieved by msbuild task or msbuild inline task. It is up to you to generate the proper code. One thing that you must care of is creating output item parameter in order to append it to the #(Compile) item. You can use $(IntDir) location to locate your newly generated file, and add them to the #(FileWrites) item group in order for Clean target work properly.
When you finish writing your task, you must use it in your project like this:
<UsingTask TaskName="TaskTypeFullName" AssemblyFile="YourAssembly.dll"/>
<PropertyGroup>
<!-- Here you need to experiment with [Build/Compile/SomeOther]DependsOn property -->
<BuildDependsOn>
MyCodeGenerator;
$(BuildDependsOn)
</BuildDependsOn>
</PropertyGroup>
<Target Name="MyCodeGenerator">
<YourTaskName>
<Output ItemName="Generated" TaskParameter="GeneratedFiles" />
</YourTaskName>
<ItemGroup>
<Compile Include="#(Generated)" />
<FileWrites Include="#(Generated)" /> <!-- For clean to work properly -->
</ItemGroup>
</Target>
I wanted to use a bash script to generate code for a project using dotnet core on Linux. Here is what worked for me. And thanks #stukselbax, I built this off of your answer.
<Target Name="GenerateProtocolBuffers" BeforeTargets="BeforeBuild;BeforeRebuild">
<Exec Command="./generatecode.sh" Outputs="proto/*.cs">
<Output ItemName="Generated" TaskParameter="Outputs" />
</Exec>
<ItemGroup>
<Compile Include="#(Generated)" />
<FileWrites Include="#(Generated)" />
</ItemGroup>
</Target>
Note that the script I'm using to generate the code is called generatecode.sh. Replace this with your own.

Using XmlUpdate to set Version information from an Assembly

Using MSBuild and MSBuild Community Tasks I am trying to do something very simple:
Get version information from an assembly.
Update a .nuspec file with that version information.
My MSBuild target looks like this:
<Target Name="Package">
<GetAssemblyIdentity AssemblyFiles="%(PackageDir.FullPath)\MyAssembly.dll">
<Output TaskParameter="Assemblies" ItemName="AssemblyIdentity" />
</GetAssemblyIdentity>
<XmlUpdate
Prefix="nu"
Namespace="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"
XmlFileName="%(PackageDir.FullPath)\MyAssembly.nuspec"
XPath="/nu:package/nu:metadata/nu:version"
Value="%(AssemblyIdentity.Version)" />
</Target>
The problem I'm having is that the NuGetPack task runs TWICE: The first time, the Assembly version is missing but the paths are correct, the second time the Assembly version is correct but the paths are missing!
Here is the output:
Updating Xml Document "D:\MyProject\package\MyAssembly.nuspec".
1 node(s) selected for update.
XmlUpdate Wrote: "".
Updating Xml Document "\MyAssembly.nuspec".
D:\MyProject\MyProject.build(64,9): error : Could not find file
'D:\MyAssembly.nuspec'.
Done Building Project "D:\MyProject\MyProject.build" (Package target(s)
) -- FAILED.
I also tried using the NuGetPack task, but got similar results. Help is greatly appreciated!
I seem to have solved it, though I'm still not sure why the code in my original question does not work.
Instead of specifying paths via concatenation (e.g. AssemblyFiles="%(PackageDir.FullPath)\MyAssembly.dll") I put each path into its own item:
<ItemGroup>
...
<PackageVersionAssembly Include=".\build-artifacts\package\MyAssembly.dll"/>
<NuSpecFile Include=".\build-artifacts\package\MyAssembly.nuspec"/>
...
</ItemGroup>
I made the same change in the task and made the same change to references to the .nuspec file.
The new Package target looks like this:
<Target Name="Package">
<GetAssemblyIdentity AssemblyFiles="#(PackageVersionAssembly)">
<Output TaskParameter="Assemblies" ItemName="AssemblyIdentity" />
</GetAssemblyIdentity>
<XmlUpdate
Prefix="nu"
Namespace="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd"
XmlFileName="#(NuSpecFile)"
XPath="/nu:package/nu:metadata/nu:version"
Value="%(AssemblyIdentity.Version)" />
</Target>
I hope this helps others!

How to read property value from external file?

I have AssemblyInfo.cs file automatically generated during build. Here's part of .csproj file:
<PropertyGroup>
<Major>2</Major>
<Minor>3</Minor>
<Build>0</Build>
<Revision>0</Revision>
</PropertyGroup>
<Target Name="BeforeBuild">
<SvnVersion LocalPath="$(MSBuildProjectDirectory)" ToolPath="C:\Program Files\VisualSVN Server\bin">
<Output TaskParameter="Revision" PropertyName="Revision" />
</SvnVersion>
<AssemblyInfo CodeLanguage="CS"
OutputFile="$(MSBuildProjectDirectory)\Properties\VersionInfo.cs"
AssemblyVersion="$(Major).$(Minor).$(Build).$(Revision)"
AssemblyFileVersion="$(Major).$(Minor).$(Build).$(Revision)"/>
</Target>
But I don't know how to specify Major and Minor properties outside .csproj file so I don't have to unload project every time I want to change version. I need either to load them from special text file inside project or to somehow set them in project properties dialog. Any suggestions?
Used ReadLinesFromFile to make version in separate file:
<ReadLinesFromFile File="Properties\Version.txt">
<Output TaskParameter="Lines" ItemName="Ver" />
</ReadLinesFromFile>
<SvnVersion LocalPath="$(MSBuildProjectDirectory)" ToolPath="C:\Program Files (x86)\VisualSVN Server\bin">
<Output TaskParameter="Revision" PropertyName="Revision" />
</SvnVersion>
<Message Text="Version: #(Ver).$(Revision)" />
<AssemblyInfo
CodeLanguage="CS"
OutputFile="$(MSBuildProjectDirectory)\Properties\VersionInfo.cs"
AssemblyVersion="#(Ver).$(Revision)"
AssemblyFileVersion="#(Ver).$(Revision)"/>
It is possible to do this using the ability of PropertyFunctions to call certain .NET functions directly. Essentially, you can call File.ReadAllText() when setting a property value.
<PropertyGroup>
<Version>$([System.IO.File]::ReadAllText("Version.txt"))</Version>
</PropertyGroup>
<SvnVersion LocalPath="$(MSBuildProjectDirectory)" ToolPath="C:\Program Files (x86)\VisualSVN Server\bin">
<Output TaskParameter="Revision" PropertyName="Revision" />
</SvnVersion>
<Message Text="Version: $(Version).$(Revision)" />
<AssemblyInfo
CodeLanguage="CS"
OutputFile="$(MSBuildProjectDirectory)\Properties\VersionInfo.cs"
AssemblyVersion="$(Version).$(Revision)"
AssemblyFileVersion="$(Version).$(Revision)"/>
The Version.txt file would just contain the first three version numbers, i.e. "1.2.3" or whatever.
I'm not sure exactly when this was added to MSBuild, but it was probably after this question was asked and answered.
Use property pages, so you could set these properties on the project property sheets dialogs.
You will need to create a properties file and edit your project file (only once) to add an import directive to your properties file. Here is an example.
You can use your external tools
<Exec Command="newversion incMinor AssemblyInfo.cs > newversion.log" />
If it's locking of the csproj files by VS that's your concern, my answer to this question - How to turn off caching of build definitions in Visual studio might help you.
You could move the content of your BeforeBuild task (including the version propertygroup) into a separate proj file and invoke it with an MSBuild task (using a random filename generated by the example in the linked answer above). This would allow you to manually edit your version number properties without having to unload/load your csproj file.

Determining outputs of a ProjectReference in MSBuild without triggering redundant rebuilds

As part of a solution containing many projects, I have a project that references (via a <ProjectReference> three other projects in the solution, plus some others). In the AfterBuild, I need to copy the outputs of 3 specific dependent projects to another location.
Via various SO answers, etc. the way I settled on to accomplish that was:
<MSBuild
Projects="#(ProjectReference)"
Targets="Build"
BuildInParallel="true"
Condition="'%(Name)'=='ProjectA' OR '%(Name)'=='ProjectB' OR '%(Name)'=='ProjectC'">
<Output TaskParameter="TargetOutputs" ItemName="DependentAssemblies" />
</MSBuild>
<Copy SourceFiles="#(DependentAssemblies)" DestinationFolder="XX" SkipUnchangedFiles="true" />
However, I ran into problems with this. The <MSBuild step's IncrementalClean task ends up deleting a number of the outputs of ProjectC. When running this under VS2008, a build.force file being deposited in the obj/Debug folder of ProjectC which then triggers ProjectC getting rebuilt if I do a Build on the entire solution if the project containing this AfterBuild target, whereas if one excludes this project from the build, it [correctly] doesn't trigger a rebuild of ProjectC (and critically a rebuild of all dependents of ProjectC). This may be VS-specific trickery in this case which would not occur in the context of a TeamBuild or other commandline MSBuild invocation (but the most common usage will be via VS so I need to resolve this either way)
The dependent projects (and the rest of the solution in general) have all been created interactively with VS, and hence the ProjectRefences contain relative paths etc. I've seen mention of this being likely to causing issues - but without a full explanation of why, or when it'll be fixed or how to work around it. In other words, I'm not really interested in e.g. converting the ProjectReference paths to absolute paths by hand-editing the .csproj.
While it's entirely possible I'm doing something stupid and someone will immediately point out what it is (which would be great), be assured I've spent lots of time poring over /v:diag outputs etc. (although I havent tried to build a repro from the ground up - this is in the context of a relatively complex overall build)
As noted in my comment, calling GetTargetPath on the referenced project only returns the Primary output assembly of that project. To get all the referenced copy-local assemblies of the referenced project it's a bit messier.
Add the following to each project that you are referencing that you want to get the CopyLocals of:
<Target
Name="ComputeCopyLocalAssemblies"
DependsOnTargets="ResolveProjectReferences;ResolveAssemblyReferences"
Returns="#(ReferenceCopyLocalPaths)" />
My particular situation is that I needed to recreate the Pipeline folder structure for System.AddIn in the bin folder of my top level Host project. This is kinda messy and I was not happy with the MSDN suggested solutions of mucking with the OutputPath - as that breaks on our build server and prevents creating the folder structure in a different project (eg a SystemTest)
So along with adding the above target (using a .targets import), I added the following to a .targets file imported by each "host" that needs the pipeline folder created:
<Target
Name="ComputePipelineAssemblies"
BeforeTargets="_CopyFilesMarkedCopyLocal"
Outputs="%(ProjectReference.Identity)">
<ItemGroup>
<_PrimaryAssembly Remove="#(_PrimaryAssembly)" />
<_DependentAssemblies Remove="#(_DependentAssemblies)" />
</ItemGroup>
<!--The Primary Output of the Pipeline project-->
<MSBuild Projects="%(ProjectReference.Identity)"
Targets="GetTargetPath"
Properties="Configuration=$(Configuration)"
Condition=" '%(ProjectReference.PipelineFolder)' != '' ">
<Output TaskParameter="TargetOutputs"
ItemName="_PrimaryAssembly" />
</MSBuild>
<!--Output of any Referenced Projects-->
<MSBuild Projects="%(ProjectReference.Identity)"
Targets="ComputeCopyLocalAssemblies"
Properties="Configuration=$(Configuration)"
Condition=" '%(ProjectReference.PipelineFolder)' != '' ">
<Output TaskParameter="TargetOutputs"
ItemName="_DependentAssemblies" />
</MSBuild>
<ItemGroup>
<ReferenceCopyLocalPaths Include="#(_PrimaryAssembly)"
Condition=" '%(ProjectReference.PipelineFolder)' != '' ">
<DestinationSubDirectory>%(ProjectReference.PipelineFolder)</DestinationSubDirectory>
</ReferenceCopyLocalPaths>
<ReferenceCopyLocalPaths Include="#(_DependentAssemblies)"
Condition=" '%(ProjectReference.PipelineFolder)' != '' ">
<DestinationSubDirectory>%(ProjectReference.PipelineFolder)</DestinationSubDirectory>
</ReferenceCopyLocalPaths>
</ItemGroup>
</Target>
I also needed to add the required PipelineFolder meta data to the actual project references. For example:
<ProjectReference Include="..\Dogs.Pipeline.AddInSideAdapter\Dogs.Pipeline.AddInSideAdapter.csproj">
<Project>{FFCD0BFC-5A7B-4E13-9E1B-8D01E86975EA}</Project>
<Name>Dogs.Pipeline.AddInSideAdapter</Name>
<Private>False</Private>
<PipelineFolder>Pipeline\AddInSideAdapter\</PipelineFolder>
</ProjectReference>
Your original solution should work simply by changing
Targets="Build"
to
Targets="GetTargetPath"
The GetTargetPath target simply returns the TargetPath property and doesn't require building.
You may protect your files in ProjectC if you call a target like this first:
<Target Name="ProtectFiles">
<ReadLinesFromFile File="obj\ProjectC.csproj.FileListAbsolute.txt">
<Output TaskParameter="Lines" ItemName="_FileList"/>
</ReadLinesFromFile>
<CreateItem Include="#(_DllFileList)" Exclude="File1.sample; File2.sample">
<Output TaskParameter="Include" ItemName="_FileListWitoutProtectedFiles"/>
</CreateItem>
<WriteLinesToFile
File="obj\ProjectC.csproj.FileListAbsolute.txt"
Lines="#(_FileListWitoutProtectedFiles)"
Overwrite="true"/>
</Target>
My current workaround is based on this SO question, i.e, I have:
<ItemGroup>
<DependentAssemblies Include="
..\ProjectA\bin\$(Configuration)\ProjectA.dll;
..\ProjectB\bin\$(Configuration)\ProjectB.dll;
..\ProjectC\bin\$(Configuration)\ProjectC.dll">
</DependentAssemblies>
</ItemGroup>
This however will break under TeamBuild (where all the outputs end up in one directory), and also if the names of any of the outputs of the dependent projects change.
EDIT: Also looking for any comments on whether there's a cleaner answer for how to make the hardcoding slightly cleaner than:
<PropertyGroup>
<_TeamBuildingToSingleOutDir Condition="'$(TeamBuildOutDir)'!='' AND '$(CustomizableOutDir)'!='true'">true</_TeamBuildingToSingleOutDir>
</PropertyGroup>
and:
<ItemGroup>
<DependentAssemblies
Condition="'$(_TeamBuildingToSingleOutDir)'!='true'"
Include="
..\ProjectA\bin\$(Configuration)\ProjectA.dll;
..\ProjectB\bin\$(Configuration)\ProjectB.dll;
..\ProjectC\bin\$(Configuration)\ProjectC.dll">
</DependentAssemblies>
<DependentAssemblies
Condition="'$(_TeamBuildingToSingleOutDir)'=='true'"
Include="
$(OutDir)\ProjectA.dll;
$(OutDir)\ProjectB.dll;
$(OutDir)\ProjectC.dll">
</DependentAssemblies>
</ItemGroup>