Repository: nvaccess/nvda

Issue:
Winget package manager treats NVDA Alpha as older than version 2020.4
Note, it is likely that we have to file this issue against WinGet, but filing it here as well for reference.

### Steps to reproduce:
1. Install [WinGet](https://docs.microsoft.com/en-us/windows/package-manager/winget/)
2. Execute winget upgrade
3. Note the following output
```
Name    Id                    Version              Available Source
-------------------------------------------------------------------
NVDA    NVAccess.NVDA         alpha-22934,e05715d3 2020.4    winget
```

### Actual behavior:
2020.4 is treated as an update from Alpha, which is certainly not the case.

### Expected behavior:
2020.4 should not be treated as an update from Alpha

### System configuration
#### NVDA installed/portable/running from source:
Installed

#### NVDA version:
alpha-22934,e05715d3

#### Windows version:
Windows 10 21H1

#### Name and version of other software in use when reproducing the issue:
Winget v1.0.11451


Discussion:
I opened https://github.com/microsoft/winget-cli/issues/1209

In short, there are plans to improve Winget with regard to version matching, but NVDA will need a change regardless. I see several options:

1. Change the DisplayVersion that's displayed in add/remove programs to the form year.major.minor.build. This ensures Winget and other package managers can treat this version semantically.
https://github.com/nvaccess/nvda/blob/b1a52939179ed3aaac41626a3720e04720559eac/source/installer.py#L231

2. Change the display version to the form 2021.2alpha-23168,b1a52939
3. Leave DisplayVersion as is, but enrich add/remove programs metadata with additional keys (e.g. `Version`, `VersionMajor`, `VersionMinor`. Version 1.1 of the winget manifest schema should allow us to tell Winget what registry value to use. I think this should be done regardless of 1.

I think this also puts us in question whether our current version naming of alpha and beta snapshots is actually practical and whether our `comparePreviousInstall` function still suffices. How ought someone to know that alpha-23168 is part of the 2021.2 alpha range? Why is an Alpha released on the 24th june considered older than a beta released on the 25th, yet the code in beta lags behind alpha? I think option 2 can make this a bit easier to follow.

Thoughts @seanbudd  and @feerrenrut?
I discovered the same problem. I think it would be good to prefix every build with the release it will become, so that we could rely on major.minor versions for sorting. If we have 2 alpha/beta builds of the same release we couldn't tell which one should be considered newer anyway. It depends on what you want, upgrades from alpha to beta are a logical path, but if alpha contains some stuff you would like to test before it gets into beta, the opposite is also a reasonable upgrade path. So, without knowing what the intent to upgrade is, it's not possible to say which one should be considered newer/preferred.
Why are alpha versions even submitted to winGet? My initial thought is this isn't something we should support. Alpha versions aren't official releases. I'd like to close this as 'won't fix'. 
@feerrenrut this issue does raise a question about which I have wondered.
Do alphas have to be named as they are? Is there some advantage to not including 
the version number for what they are an alpha of?

If a PR to start naming alphas in a more version-related form would be 
considered, I believe I have a method for accomplishing it with minimal effort, 
and would be happy to put it forward.

> Why are alpha versions even submitted to winGet? My initial thought is this isn't something we should support. Alpha versions aren't official releases.

I'm afraid you misunderstood the issue. Alpha versions are *not* submitted to winget, but winget investigates whether an update is available by looking at the current version of NVDA that's installed on the system (version set in the registry). If an alpha version is installed, winget always thinks that there's an update, regardless whether the most recent of NVDA is older than the installed alpha version.
@XLTechie Curious to know your solutions.

Ideally, Winget would parse the major and minor version strings in the registry, unfortunately they don't yet. Having said that, adding the current development cycle to the version name would certainly be an advantage for readability too.
@XLTechie 
> Do alphas have to be named as they are?

Not necessarily, but the version string serves our purposes, and changing it requires work (in several systems) that would take us away from more important things.

> Is there some advantage to not including the version number for what they are an alpha of?

Not necessarily an advantage, but it would be misleading. A released version isn't a released version till the release.
A change introduced in an alpha snapshot may be added to, or removed from a release.

The alpha snapshot version strings tell us the build order and the commit. 


@leonardder, yes I misunderstood. Thanks for pointing that out.
This sounds like an issue in winget then. If the version string scheme for the installed copy isn't recognized, they should probably halt. Or as you said look in one of the other locations for version number.

I still don't think this is something we should fix on our side.
> Not necessarily an advantage, but it would be misleading. A released version isn't a released version till the release.

I don't think this has to be a show stopper. Firefox for example uses major version + 1 for their beta versions and major version + 2 for their alpha versions I believe. The fact whether you're dealing with an alpha or beta is clear from the version string as well there.

Resolving patch:
diff --git a/source/buildVersion.py b/source/buildVersion.py
--- a/source/buildVersion.py
+++ b/source/buildVersion.py
@@ -77,5 +77,6 @@ def formatVersionForGUI(year, major, minor):
 except ImportError:
 	_updateVersionFromVCS()
 
+version_detailed = formatBuildVersionString()
 # A test version is anything other than a final or rc release.
 isTestVersion = not version[0].isdigit() or "alpha" in version or "beta" in version or "dev" in version
diff --git a/source/installer.py b/source/installer.py
--- a/source/installer.py
+++ b/source/installer.py
@@ -21,6 +21,10 @@
 import easeOfAccess
 import COMRegistrationFixes
 import winKernel
+from typing import (
+	Dict,
+	Union,
+)
 
 _wsh=None
 def _getWSH():
@@ -227,16 +231,23 @@ def removeOldProgramFiles(destPath):
 					log.warning(f"Couldn't remove file: {path!r}")
 
 
-uninstallerRegInfo={
-	"DisplayName":versionInfo.name,
-	"DisplayVersion":versionInfo.version,
-	"DisplayIcon":u"{installDir}\\images\\nvda.ico",
-	"InstallDir":u"{installDir}",
-	"Publisher":versionInfo.publisher,
-	"UninstallDirectory":u"{installDir}",
-	"UninstallString":u"{installDir}\\uninstall.exe",
-	"URLInfoAbout":versionInfo.url,
-}
+def getUninstallerRegInfo(installDir: str) -> Dict[str, Union[str, int]]:
+	"""
+	Constructs a dictionary that is written to the registry for NVDA to show up
+	in the Windows "Apps and Features" overview.
+	"""
+	return dict(
+		DisplayName=f"{versionInfo.name} {versionInfo.version}",
+		DisplayVersion=versionInfo.version_detailed,
+		DisplayIcon=os.path.join(installDir, "images", "nvda.ico"),
+		# EstimatedSize is in KiB
+		EstimatedSize=getDirectorySize(installDir) // 1024,
+		InstallDir=installDir,
+		Publisher=versionInfo.publisher,
+		UninstallDirectory=installDir,
+		UninstallString=os.path.join(installDir, "uninstall.exe"),
+		URLInfoAbout=versionInfo.url,
+	)
 
 
 def getDirectorySize(path: str) -> int:
@@ -259,11 +270,8 @@ def registerInstallation(
 		startOnLogonScreen: bool,
 		configInLocalAppData: bool = False
 ) -> None:
-	calculatedUninstallerRegInfo = uninstallerRegInfo.copy()
-	# EstimatedSize is in KiB
-	estimatedSize = getDirectorySize(installDir)
-	log.debug(f"Estimated install size {estimatedSize}")
-	calculatedUninstallerRegInfo.update(EstimatedSize=estimatedSize // 1024)
+	calculatedUninstallerRegInfo = getUninstallerRegInfo(installDir)
+	log.debug(f"Estimated install size: {calculatedUninstallerRegInfo.get('EstimatedSize')} KiB")
 	with winreg.CreateKeyEx(
 		winreg.HKEY_LOCAL_MACHINE,
 		r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NVDA",
@@ -272,21 +280,18 @@ def registerInstallation(
 	) as k:
 		for name, value in calculatedUninstallerRegInfo.items():
 			if isinstance(value, int):
-				winreg.SetValueEx(
-					k,
-					name,
-					None,
-					winreg.REG_DWORD,
-					value
-				)
+				regType = winreg.REG_DWORD
+			elif isinstance(value, str):
+				regType = winreg.REG_SZ
 			else:
-				winreg.SetValueEx(
-					k,
-					name,
-					None,
-					winreg.REG_SZ,
-					value.format(installDir=installDir)
-				)
+				raise NotImplementedError("Unexpected value from dictionary in getUninstallerRegInfo")
+			winreg.SetValueEx(
+				k,
+				name,
+				None,
+				regType,
+				value
+			)
 	with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe",0,winreg.KEY_WRITE) as k:
 		winreg.SetValueEx(k,"",None,winreg.REG_SZ,os.path.join(installDir,"nvda.exe"))
 	with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, config.RegistryKey.NVDA.value, 0, winreg.KEY_WRITE) as k:
diff --git a/source/versionInfo.py b/source/versionInfo.py
--- a/source/versionInfo.py
+++ b/source/versionInfo.py
@@ -21,7 +21,7 @@
 aboutMessage = _(
 	# Translators: "About NVDA" dialog box message
 	u"""{longName} ({name})
-Version: {version}
+Version: {version} ({version_detailed})
 URL: {url}
 {copyright}
 


Test patch: