4

I'm trying to build upon this question:

Reading a single value from a file in MSBuild

My goal is to have a single place to put the version number that's used in several projects, and I also want a portion of the version number in the DLL file name for one of the projects.

Based on the question above, I already got the first part, but I'm having difficulty with the second part and would appreciate some guidance.

In my solution, I set up a plain text file called Version.txt containing my full version number only:

1.1.0.0

In both of my projects, I opened their AssemblyInfo.cs files and removed the AssemblyVersion and AssemblyFileVersion items, then modified both projects to generate them in a separate file as described in the question above.

<ItemGroup>
    <VersionFile Include="..\Version.txt" />
</ItemGroup>
<Target Name="BeforeBuild">
    <ReadLinesFromFile File="@(VersionFile)">
        <Output TaskParameter="Lines" PropertyName="VersionNumber" />
    </ReadLinesFromFile>
    <Delete Files="Properties\Version.cs" />
    <WriteLinesToFile File="Properties\Version.cs" Lines="using System.Reflection%3B&#xD;&#xA;&#xD;&#xA;[assembly: AssemblyVersion("$(VersionNumber)")]&#xD;&#xA;[assembly: AssemblyFileVersion("$(VersionNumber)")]" />
</Target>

Now when I build, I get a generated Properties\Version.cs file for each project, which is used to build the EXE/DLL and shows up as "1.1.0.0" in their file properties. This is exactly what I want.

For the DLL, I would like to name the assembly "filename.v1.1.dll", where the "1.1" comes from the first two components in Version.txt above. I'm flexible on the format of Version.txt as long as I can get the full "1.1.0.0" in the EXE/DLL properties and "1.1" in the DLL file name.

To try this out, I modified the DLL's csproj file to have:

<RootNamespace>dllfile</RootNamespace>
<AssemblyName>dllfile.v$(VersionNumber)</AssemblyName>

Of course, this will insert the full version number in the file name, which I don't want.

Does anyone have any tips on how to proceed?

Thanks.

EDIT: I have been able to extract the major/minor components of the version number by adding the following to my .csproj BeforeBuild target:

<ReadLinesFromFile File="@(VersionFile)">
    <Output TaskParameter="Lines" PropertyName="VersionNumber" />
</ReadLinesFromFile>
<PropertyGroup>
    <VersionNumberFirstDotIndex>$(VersionNumber.IndexOf('.'))</VersionNumberFirstDotIndex>
    <VersionNumberMajorStart>0</VersionNumberMajorStart>
    <VersionNumberMajorLen>$(VersionNumberFirstDotIndex)</VersionNumberMajorLen>
    <VersionNumberMinorStart>$([MsBuild]::Add(1, $(VersionNumberFirstDotIndex)))</VersionNumberMinorStart>
    <VersionNumberSecondDotIndex>$(VersionNumber.IndexOf('.', $(VersionNumberMinorStart)))</VersionNumberSecondDotIndex>
    <VersionNumberMinorLen>$([MSBuild]::Subtract($([MSBuild]::Subtract($(VersionNumberSecondDotIndex), $(VersionNumberFirstDotIndex))), 1))</VersionNumberMinorLen>
    <VersionNumberMajor>$(VersionNumber.Substring($(VersionNumberMajorStart), $(VersionNumberMajorLen)))</VersionNumberMajor>
    <VersionNumberMinor>$(VersionNumber.Substring($(VersionNumberMinorStart), $(VersionNumberMinorLen)))</VersionNumberMinor>
    <VersionNumberShort>$(VersionNumberMajor).$(VersionNumberMinor)</VersionNumberShort>
</PropertyGroup>
<Message Text="DEBUG1 VersionNumberFull=$(VersionNumber)" Importance="High" />
<Message Text="DEBUG2 VersionNumberAbbrev=$(VersionNumberShort)" Importance="High" />
<Delete Files="Properties\Version.cs" />
<WriteLinesToFile File="Properties\Version.cs" Lines="using System.Reflection%3B&#xD;&#xA;&#xD;&#xA;[assembly: AssemblyVersion(&quot;$(VersionNumber)&quot;)]&#xD;&#xA;[assembly: AssemblyFileVersion(&quot;$(VersionNumber)&quot;)]" />

The only piece I'm missing now is how to get this VersionNumberShort into the DLL file name. Unless someone has a better idea, I can take Peter's suggestion and use Move tasks:

<Target Name="AfterBuild">
    <Move SourceFiles="$(OutputPath)$(AssemblyName).pdb" DestinationFiles="$(OutputPath)$(AssemblyName).v$(VersionNumberShort).pdb" />
    <Move SourceFiles="$(OutputPath)$(AssemblyName).dll" DestinationFiles="$(OutputPath)$(AssemblyName).v$(VersionNumberShort).dll" />
</Target>
<Target Name="AfterClean" DependsOnTargets="Common">
    <Delete Files="$(OutputPath)$(AssemblyName).v$(VersionNumberShort).pdb" ContinueOnError="true" />
    <Delete Files="$(OutputPath)$(AssemblyName).v$(VersionNumberShort).dll" ContinueOnError="true" />
</Target>

Since I needed the same property definitions as before, I moved the snippet above into a "Common" target and referenced it in both the build and clean tasks shown here.

Peter - If you want to move your comment as an answer, I'll accept it.

Thanks!

EDIT: Following jdlugosz's answer, I tried setting the AssemblyName inside my task. Unfortunately, this still didn't seem to have any effect based on the original example listed at the top:

<Target Name="BeforeBuild">
    ...
    <WriteLinesToFile ... />
    <PropertyGroup>
      <AssemblyName>dllfile.v$(VersionNumber)</AssemblyName>
    </PropertyGroup>
</Target>

I tried running this with MSBuild from a Visual Studio Developer Command Prompt:

msbuild /target:clean projfile.csproj
msbuild /verbosity:diag projfile.csproj > out.txt

Prior to this, I renamed the at the top of my csproj file and in the "redefinition" to something unique to make it easy to search (e.g. "dllfileoriginal" vs. "dllfilemodified").

Looking through the output log, I can't find any reference to the modified text; it's still dllfileoriginal everywhere in the output.

Following the WriteLinesToFile task, it looks like the following targets were built:

  • IncrementalClean (finished)
  • PostBuildEvent
  • CoreBuild
  • AfterBuild
  • Build

There's no reference to either DLL name inside these.

It looks like the is currently my best bet still.

Community
  • 1
  • 1
jia103
  • 1,116
  • 2
  • 13
  • 20
  • 1
    Why not add a element at the end of a target or a AfterBuild target to rename the binary? – Peter Ritchie Sep 24 '14 at 16:16
  • @PeterRitchie - it will not work if assembly is signed. Plus the question is about constructing that new path, less when to do so... – Alexei Levenkov Sep 24 '14 at 16:19
  • 1
    Why do you want the version in the assembly name? What benefit are you getting from that? – Bob Horn Sep 24 '14 at 16:35
  • 1
    In our case, we've observed the need for deploying our software with multiple versions of the same DLL. For example, suppose DLLs A and B both depend on DLL X version 1.0. Then we produce version 1.1 of DLL X, and we get around to updating DLL B to use X 1.1, but A still uses X 1.0 with no plans for updates. In that case, we need to deploy the final product with A, B, X 1.0, and X 1.1 simultaneously. Anyway, that's not something we have control over; that's the way it is. What I'd like to be able to do is to centralize that number into a single spot. Thanks. – jia103 Sep 24 '14 at 18:46
  • Will adding a linked AssemblyVersionl.cs not work? – Mike Cheel Sep 24 '14 at 21:41
  • This is an extraordinary bad idea. For every 1000 programmers/build engineers that want to automate version numbering, there are 999 that take control away from the programmer that *really* knows whether his changes are breaking. That programmer never needs help, he already knows how to edit AssemblyInfo.cs – Hans Passant Sep 24 '14 at 22:46
  • Mike, please clarify AssemblyVersionI.cs. Hans, please clarify exactly which part of this is such a bad idea. – jia103 Sep 25 '14 at 14:10
  • To clarify, this isn't for the build engineer at all, and it isn't for automating any version numbering. This is to centralize the version number so that when the version number changes, only one file needs to be updated instead of two or three. You're right that the programmer knows how to edit AssemblyInfo.cs; the problem we've experienced many times over the years in multiple environments--not just C#/.NET/Visual Studio--is that when there are two or more places to update, someone sometimes forgets to update some of those places. – jia103 Sep 25 '14 at 14:10

2 Answers2

1

The Target Name is is shown on the General page under the Configuration Properties tab in the IDE Property Page editor. I don't have one handy myself to look up the name for you, but you can do it by changing the blank in the IDE to something like XXXX and save. Then view the diff in the version control commit reviewer and see what the name of the Property is. In this case, then edit the line to change XXXX to $(OutputPath)$(AssemblyName).v$(VersionNumberShort)

enter image description here

Oh, check out the FormatVersion task, which might help. I think there are some premade tasks that manipulate a version assembly similar to what you show, too.

What I'm doing for versions is passing the pieces in via #defines as /D command line arguments. I guess you don't have that in C# though, IIRC.

JDługosz
  • 5,592
  • 3
  • 24
  • 45
  • I'm afraid I can't find any General tab. I looked both on the Property pages of the project as well as the Properties pane in Visual Studio 2012. FormatVersion looks like it'll append numbers together according to the MSDN page, not take them apart. I was thinking about defines as well; I believe C# does support passing them in, but I don't need to do anything with them in the source file since I already generated the Version.cs file with WriteLinesToFile, but it seems like another good idea. Thanks. – jia103 Sep 25 '14 at 14:06
  • @jia103 I clarified the location in the answer above. – JDługosz Sep 25 '14 at 15:09
  • Thanks for the update. That screen looks familiar; it looks like you're using a different version of Visual Studio; I'm on 2012 and I don't see that. I also don't see anything like it in the .csproj file; all I have is there. – jia103 Sep 25 '14 at 16:09
  • Maybe `` is what you need? Try changing it to XXX and see if that shows up where you expect. – JDługosz Sep 25 '14 at 16:14
  • Well, the problem with is that I already tried inserting the property in there: dllname.v$(VersionNumber). The problem with this is that VersionNumber doesn't appear to get a value until later; at the time this is resolved, it is apparently still empty end I end up with a DLL named dllname.v.dll, which is not what I want. So far it's still looking like is my only way with the restriction that it might not work for signed assemblies. – jia103 Sep 25 '14 at 16:47
  • Right, the static `` is done early, and the version stuff is done as part of a Task. So update `AssemblyName` within the same task you added to determine the version! It will take effect globally once that task is completed. Sometimes the order is hard to fathom because it gets the right answer due to leaving the macro unevaluated until it's *used*, but when that works is hazy; I think involving Items. Just changing it is easy. – JDługosz Sep 26 '14 at 00:07
  • I don't have time to try this out right now, but would I use the task for this as described here http://stackoverflow.com/a/1367160/3290955? Thanks. – jia103 Sep 26 '14 at 15:02
  • No, don't use . That is an old hack, from the original MSBuild feature set. Just declare a property group and property inside the task. – JDługosz Sep 27 '14 at 00:40
  • I believe I tried what you suggested; it still didn't seem to work. I posted the snippet to the end of my post above so you can see. It looks like my change in the didn't have any effect because I still get the original DLL file name. Thanks. – jia103 Sep 28 '14 at 00:13
  • You have to look at the build script to see how it names the file. I don't know if it uses `Link` like other languages or it has some equivilent for .NET, or if the `CS` compiler produces a DLL directly? Whatever, the output file name is probably passed a parameter to the step that creates it. Use `/verbosity:d` and see what the last Tasks are, working backwards. At some level of verbosity it gives all the parameters and commands; you can grep for the DLL name in the output. – JDługosz Sep 28 '14 at 03:34
  • Please explain further. How do I get to this build script? I currently have a Visual Studio 2012 solution with a couple .csproj files. Is there something I can run that can generate this build script to which you're referring? Thanks. – jia103 Sep 28 '14 at 03:51
  • The build script is the entire bunch of proj files that MSBuild is running when you say Go. That is, your .csproj file (and all the include files it pulls in). Just add the verbose switch to the MSBuild command line you are using now, and look at the Targets it reports. – JDługosz Sep 29 '14 at 04:20
  • Updated above. Thanks for the additional help, but still not seeing it. – jia103 Sep 29 '14 at 13:44
  • @jia103 are you building by running MSBuild at a command prompt? Add `/verbosity:d` and `>toomuchinformation.txt` to save the details to a file. Then, look through toomuchinformation.txt, towards the end, for which Targets are actually being triggered. One of those is involved with naming the final DLL file, and by showing everything it did (not just the highlights) you can find which. Once you find that, you can examine its definition and see where it gets the dll name. – JDługosz Sep 30 '14 at 20:39
0

This works for me, and it solves the seemingly simple problem of appending the version info string to a filename at build.

First, the post-build event:

(Right-Click Project -> Properties -> Build Events -> Edit Post-build...)

$(TargetPath) "version" > $(TargetDir)text.txt
set /p version= <$(TargetDir)text.txt
copy $(TargetPath) $(TargetDir)$(TargetName)_%version%.exe
del $(TargetDir)text.txt

Now, the trick: Overload sub main to return the version info, and call it in a post-build event on the exe that was just built.

here is an example in F#:

[<EntryPoint>]
let main argv = 

    let version = argv.Length = 1 && argv.[0] = "version"
    if version then
        let version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString()
        do stdout.WriteLine(version)
        do stdout.Flush()
    else 
        try
        //...

The post-build event above

1) calls the newly built exe with a "version" arg, and writes the output to a txt file

2) reads the text file contents into a local variable

3) renames the newly built exe by adding the version info

3) copies the newly built exe adding the version info to the name

4) cleans up the temp file

*changed "move" to "copy" so that Visual Studio can still F5 the project

chuckc
  • 181
  • 6