dotnet / Nerdbank.GitVersioning

Stamp your assemblies, packages and more with a unique version generated from a single, simple version.json file and include git commit IDs for non-official builds.
https://www.nuget.org/packages/Nerdbank.GitVersioning
MIT License
1.32k stars 165 forks source link

AssemblyVersion is missing the build number #1048

Closed swfaust closed 2 months ago

swfaust commented 2 months ago

I'm new to NBGV so sorry if I'm missing something obvious here. I have this version.json file:

{
  "version": "11.0",
  "nuGetPackageVersion": {
    "semVer": 2.0
  },
  "publicReleaseRefSpec": [
    "^refs/heads/main",
    "^refs/heads/rel/\\d+\\.\\d+\\.\\d+"
  ]
}

I'm building a desktop class library multi-targeting .net 8.0 and .net framework 4.8 (don't think that matters but figured I'd say it) in case.

When I build this, I get file version numbers like 11.0.2.12345. I understand that 11.0 is coming from my version.json file and 2 is the height. The last number is basically random from the commit id if I understand correctly. However, I thought that last number was supposed to be dropped for public branch builds is that correct? I'm in the branch named main and building on my local machine. Am I missing something in how this is supposed to work?

Second and probably a bigger deal is the AssemblyVersion. I believe this is what you get when you call Assembly.GetName().Version in code (c#) correct? If so, I'm getting just 11.0.0.0 as that version, it's dropping the height parameter. Is this expected? What I would like to see with a public branch build is for AssemblyVersion and FileVersion to both be 11.0.2.0 or just 11.0.2, is this possible? If it's not then I can probably work with what I have but I thought this was the way it was supposed to work.

AArnott commented 2 months ago

The behavior you see is the default behavior, and there are good reasons for it, but it can be overridden. Let's go over each one.

AssemblyFileVersion is not part of any binding contract. It isn't part of the assembly name. It tends to be 'very precise' as a result because it is the same version that is put into the PE header and what you see in File Properties for the version. Having the full precision volume here is useful because a.b.c versions are often ambiguous as far as which commit built them. But a.b.c.d versions where d includes 15 bits from the commit ID has never failed me at precisely identifying a commit. As a result, given a file version, you can always get back to the commit that produced it.

AssemblyVersion is part of the assembly name, and thus is part of the binding contract on .NET Framework (not so much for .NET 8). You don't want that to change with every commit typically, because that would mean folks who bind against you to either recompile against the newer library for every single commit, or the app that ships your library would have to update its binding redirects for every single commit. If we take a page from semver, an update to the 3rd integer (c in a.b.c.d) is just a servicing update that should not change its API in any way. If we assume you're following that, the assembly version shouldn't change for just a servicing update, as servicing should just be a drop-in replacement rather than requiring binding redirect updates.

So given assembly version and assembly file version have different audiences and different ramifications for their values and changes to them, having the same value across both is not optimal for either value. This is why the default value is as it is.

AArnott commented 2 months ago

That said, if you want the assembly version to be more precise, there is an assemblyVersion property you can set in the version.json file to make it more precise. I'm not sure whether we have a provision to also make the assembly file version less precise, as no one has asked for that IIRC. But you could always do it yourself through manipulating the msbuild properties as documented in our doc/msbuild.md file.

KalleOlaviNiemitalo commented 2 months ago

You don't want that to change with every commit typically, because that would mean folks who bind against you to either recompile against the newer library for every single commit, or the app that ships your library would have to update its binding redirects for every single commit.

If the assembly does not have a strong name, then .NET Framework will happily bind to it even if the assembly version is higher than what was referenced. (Then you cannot install it in the Global Assembly Cache, though.)

AArnott commented 2 months ago

Good point, @KalleOlaviNiemitalo. Also, if you don't strong name, other assemblies that are strong named can't reference yours (on .NET Framework). So IMO every library you mean to share should be strong named to maximize its reach.

swfaust commented 2 months ago

Ok thank you for the detailed explanation. I think I'm tracking with the logic here, but just to explain further and make sure I have it correct...

So the idea is that AssemblyVersion stays the same until I make some actual change that would break things. In that case I update the second value. This allows .NET loading to work without breaking in more situations. FileVersion, however, is intended to identify the commit so the second 2 items are required for that. So I thought the informational version has the commit ID directly, so what is the difference between the file version and the informational version other than that the end of the file version has encoded commit id values and the informational version has the whole thing? What is the FileVersion adding that informational version is missing?

So that does maybe highlight that my use case is a little different here. This is not a NuGet package that will be consumed publicly or by others (side note, should I take the nuget reference out of the version.json file then?). This is an internal project for building commercial software and the different projects in the solution are only referenced internally. So I'm less concerned about consumers breaking (though I still try to update for breaking changes). I may, however, make changes that don't break any API but effect how end users use or see the software. In that case I need them to be able to go to the about dialog and see which release version they have and tell me. It sounds like I can use the FileVersion for this and it will be plenty precise and maybe that's what I do. The only drawback is that the last number is longer for the user to report but that's certainly not a deal killer.

The other option I thought of is that for the about dialog I can use the FileVersion but construct a new version object and just manually set the last value to 0. This would allow the user to see something like 11.0.2.0 which is really easy but then if for some reason I needed more precision I can ask them for file properties.

Thoughts?

KalleOlaviNiemitalo commented 2 months ago

What is the FileVersion adding that informational version is missing?

I recall reading from somewhere in this repository, that the four-integer FileVersion is more reliably included in crash dumps than the more verbose version strings.

You could also make the about dialog display the generated ThisAssembly.GitCommitId.

swfaust commented 2 months ago

@KalleOlaviNiemitalo ok that makes some sense. Not a big deal either way I'm just trying to understand fully to make the best decision on which way I go so appreciate you guys taking the time to respond. As far as the dialog, I would rather not show that long of a string so that users can quickly type out 'I have version 11.0.2.0' or whatever. I know they could take a screen shot of the id but no way they are going to type that out. I had actually missed the ThisAssembly class though so thanks for pointing me to that, that's super handy...

AArnott commented 2 months ago

what is the difference between the file version and the informational version other than that the end of the file version has encoded commit id values and the informational version has the whole thing?

AssemblyFileVersion is also used as the PE header file version, so all tools recognize that version. It is limited to 4 integers. AssemblyInformationalVersion is a .NET metadata unique thing that can only be observed with tools that know how to read managed assembly attributes. It allows free-form text so it's convenient to leverage that to display a longer commit hash as text.

What is the FileVersion adding that informational version is missing?

As I mentioned just above, FileVersion is the only one that is visible to every tool that can display a version for PE files.

should I take the nuget reference out of the version.json file then?

That's up to you. It seems like a waste though if you're not building packages.

I need them to be able to go to the about dialog and see which release version they have and tell me. It sounds like I can use the FileVersion for this and it will be plenty precise.

Yes, show the file version and/or the informational version. If you mean for them to report it back to you, the file version is the most succinct yet precise value you can show.

The other option I thought of is that for the about dialog I can use the FileVersion but construct a new version object and just manually set the last value to 0.

That's up to you. I haven't heard any customer complain about the 4th integer yet, FWIW. And if you do construct your own Version object, don't include the 4th integer at all -- so that they only see 1.2.3 rather than 1.2.3.0. The .0 at the end is misleadingly precise (and longer than necessary) when you force it to zero. Just omit it.

swfaust commented 2 months ago

Ok great, thank you so much for taking the time to educate me, I appreciate it!