From 059a28395aa6c368ce08a1454ef2af6057972bbd Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:14:52 -0700 Subject: [PATCH 1/8] Register --- .../Commands/WinGetPackageManagerCommand.cs | 8 +- .../Helpers/AppxModuleHelper.cs | 90 +++++++++++-------- .../Microsoft.WinGet.Client.Engine.csproj | 2 +- ...crosoft.WinGet.Configuration.Engine.csproj | 2 +- 4 files changed, 62 insertions(+), 40 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index cdf5f16d51..e10ae952bb 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -138,11 +138,13 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers this.RepairEnvPath(); break; case IntegrityCategory.AppInstallerNotRegistered: - this.Register(expectedVersion); + await this.RegisterAsync(expectedVersion, allUsers); break; case IntegrityCategory.AppInstallerNotInstalled: case IntegrityCategory.AppInstallerNotSupported: case IntegrityCategory.Failure: + System.Diagnostics.Debugger.Launch(); + System.Diagnostics.Debugger.Break(); await this.InstallAsync(expectedVersion, allUsers, force); break; case IntegrityCategory.AppInstallerNoLicense: @@ -197,10 +199,10 @@ private async Task InstallAsync(string toInstallVersion, bool allUsers, bool for await appxModule.InstallFromGitHubReleaseAsync(toInstallVersion, allUsers, false, force); } - private void Register(string toRegisterVersion) + private async Task RegisterAsync(string toRegisterVersion, bool allUsers) { var appxModule = new AppxModuleHelper(this); - appxModule.RegisterAppInstaller(toRegisterVersion); + await appxModule.RegisterAppInstallerAsync(toRegisterVersion, allUsers); } private void RepairEnvPath() diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs index e8b8d1ad09..5845be1ba2 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs @@ -16,7 +16,6 @@ namespace Microsoft.WinGet.Client.Engine.Helpers using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.WinGet.Client.Engine.Common; - using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Extensions; using Microsoft.WinGet.Common.Command; using Newtonsoft.Json; @@ -61,6 +60,7 @@ internal class AppxModuleHelper private const string Register = "Register"; private const string DisableDevelopmentMode = "DisableDevelopmentMode"; private const string ForceTargetApplicationShutdown = "ForceTargetApplicationShutdown"; + private const string AllUsers = "AllUsers"; private const string AppInstallerName = "Microsoft.DesktopAppInstaller"; private const string AppxManifest = "AppxManifest.xml"; @@ -112,21 +112,23 @@ public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) /// /// Calls Get-AppxPackage Microsoft.DesktopAppInstaller. /// + /// Whether to get for all users. /// Result of Get-AppxPackage. - public PSObject? GetAppInstallerObject() + public PSObject? GetAppInstallerObject(bool allUsers = false) { - return this.GetAppxObject(AppInstallerName); + return this.GetAppxObject(AppInstallerName, allUsers); } /// /// Gets the string value a property from the Get-AppxPackage object of AppInstaller. /// /// Property name. + /// Whether to get for all users. /// Value, null if doesn't exist. - public string? GetAppInstallerPropertyValue(string propertyName) + public string? GetAppInstallerPropertyValue(string propertyName, bool allUsers = false) { string? result = null; - var packageObj = this.GetAppInstallerObject(); + var packageObj = this.GetAppInstallerObject(allUsers); if (packageObj is not null) { var property = packageObj.Properties.Where(p => p.Name == propertyName).FirstOrDefault(); @@ -143,11 +145,13 @@ public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) /// Calls Add-AppxPackage to register with AppInstaller's AppxManifest.xml. /// /// Release tag of GitHub release. - public void RegisterAppInstaller(string releaseTag) + /// Whether to register for all users. + /// A representing the asynchronous operation. + public async Task RegisterAppInstallerAsync(string releaseTag, bool allUsers) { if (string.IsNullOrEmpty(releaseTag)) { - string? versionFromLocalPackage = this.GetAppInstallerPropertyValue(Version); + string? versionFromLocalPackage = this.GetAppInstallerPropertyValue(Version, allUsers); if (versionFromLocalPackage == null) { @@ -157,11 +161,11 @@ public void RegisterAppInstaller(string releaseTag) var packageVersion = new Version(versionFromLocalPackage); if (packageVersion.Major == 1 && packageVersion.Minor > 15) { - releaseTag = $"1.{packageVersion.Minor - 15}.{packageVersion.Build}"; + releaseTag = $"v1.{packageVersion.Minor - 15}.{packageVersion.Build}"; } else { - releaseTag = $"{packageVersion.Major}.{packageVersion.Minor}.{packageVersion.Build}"; + releaseTag = $"v{packageVersion.Major}.{packageVersion.Minor}.{packageVersion.Build}"; } } @@ -169,31 +173,9 @@ public void RegisterAppInstaller(string releaseTag) // If dependencies are missing, a provisioned package can appear to only need registration, // but will fail to register. `InstallDependenciesAsync` checks for the packages before // acting, so it should be mostly a no-op if they are already available. - this.InstallDependenciesAsync(releaseTag).Wait(); - - string? packageFullName = this.GetAppInstallerPropertyValue(PackageFullName); - - if (packageFullName == null) - { - throw new ArgumentNullException(PackageFullName); - } - - string appxManifestPath = System.IO.Path.Combine( - Utilities.ProgramFilesWindowsAppPath, - packageFullName, - AppxManifest); + await this.InstallDependenciesAsync(releaseTag); - _ = this.ExecuteAppxCmdlet( - AddAppxPackage, - new Dictionary - { - { Path, appxManifestPath }, - }, - new List - { - Register, - DisableDevelopmentMode, - }); + this.RegisterAppInstallerInternal(allUsers); } /// @@ -278,6 +260,10 @@ await this.httpClientHelper.DownloadUrlWithProgressAsync( .AddParameter(ErrorAction, Stop) .Invoke(); }); + + // Register the package after provisioning so that it is + // available immediately. + this.RegisterAppInstallerInternal(allUsers: true); } catch (RuntimeException e) { @@ -320,14 +306,21 @@ private async Task AddAppInstallerBundleAsync(string releaseTag, bool downgrade, } } - private PSObject? GetAppxObject(string packageName) + private PSObject? GetAppxObject(string packageName, bool allUsers = false) { + var options = new List(); + if (allUsers) + { + options.Add(AllUsers); + } + return this.ExecuteAppxCmdlet( GetAppxPackage, new Dictionary { { Name, packageName }, - }) + }, + options) .FirstOrDefault(); } @@ -808,5 +801,32 @@ private bool IsStubPackageOptionPresent() return result; } + + private void RegisterAppInstallerInternal(bool allUsers = false) + { + string? packageFullName = this.GetAppInstallerPropertyValue(PackageFullName, allUsers); + + if (packageFullName == null) + { + throw new ArgumentNullException(PackageFullName); + } + + string appxManifestPath = System.IO.Path.Combine( + Utilities.ProgramFilesWindowsAppPath, + packageFullName, + AppxManifest); + + _ = this.ExecuteAppxCmdlet( + AddAppxPackage, + new Dictionary + { + { Path, appxManifestPath }, + }, + new List + { + Register, + DisableDevelopmentMode, + }); + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj b/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj index 110fe45120..e54da55956 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj @@ -21,7 +21,7 @@ enable - + $(DefineConstants);USE_PROD_CLSIDS diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj index 70f53cc531..83c29cfe17 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj @@ -12,7 +12,7 @@ Debug;Release;ReleaseStatic - + $(DefineConstants);USE_PROD_CLSIDS From 00efc143cc0f4fc6f64505ed313171b8a7081516 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:19:13 -0700 Subject: [PATCH 2/8] Install winget source --- .../Commands/WinGetPackageManagerCommand.cs | 11 ++++++- .../Common/IntegrityCategory.cs | 7 +++- .../Common/WinGetIntegrity.cs | 7 ++++ .../Helpers/AppxModuleHelper.cs | 33 +++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index e10ae952bb..a711e97b60 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -144,7 +144,6 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers case IntegrityCategory.AppInstallerNotSupported: case IntegrityCategory.Failure: System.Diagnostics.Debugger.Launch(); - System.Diagnostics.Debugger.Break(); await this.InstallAsync(expectedVersion, allUsers, force); break; case IntegrityCategory.AppInstallerNoLicense: @@ -158,6 +157,9 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers throw new WinGetRepairException(e); } + break; + case IntegrityCategory.WinGetSourceNotInstalled: + await this.InstallWinGetSourceAsync(); break; case IntegrityCategory.AppExecutionAliasDisabled: case IntegrityCategory.Unknown: @@ -199,6 +201,13 @@ private async Task InstallAsync(string toInstallVersion, bool allUsers, bool for await appxModule.InstallFromGitHubReleaseAsync(toInstallVersion, allUsers, false, force); } + private async Task InstallWinGetSourceAsync() + { + this.Write(StreamType.Verbose, "Installing winget source"); + var appxModule = new AppxModuleHelper(this); + await appxModule.InstallWinGetSourceAsync(); + } + private async Task RegisterAsync(string toRegisterVersion, bool allUsers) { var appxModule = new AppxModuleHelper(this); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/IntegrityCategory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/IntegrityCategory.cs index b026070109..22fdebb85f 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/IntegrityCategory.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/IntegrityCategory.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -65,5 +65,10 @@ public enum IntegrityCategory /// No applicable license found. /// AppInstallerNoLicense, + + /// + /// WinGet source is not installed. + /// + WinGetSourceNotInstalled, } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs index 9669da8dbc..7139d2314a 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -79,6 +79,13 @@ public static void AssertWinGet(PowerShellCmdlet pwshCmdlet, string expectedVers expectedVersion)); } } + + // Verify that the winget source is installed. + var appxModule = new AppxModuleHelper(pwshCmdlet); + if (!appxModule.IsWinGetSourceInstalled()) + { + throw new WinGetIntegrityException(IntegrityCategory.WinGetSourceNotInstalled); + } } private static IntegrityCategory GetReason(PowerShellCmdlet pwshCmdlet) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs index 5845be1ba2..e9558d75d4 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs @@ -94,6 +94,11 @@ internal class AppxModuleHelper private const string XamlPackage27 = "Microsoft.UI.Xaml.2.7"; private const string XamlReleaseTag273 = "v2.7.3"; + // WinGet Source + private const string WinGetSourceName = "Microsoft.Winget.Source"; + private const string WinGetSourceMsixName = "source2.msix"; + private const string WinGetSourceUrl = "https://cdn.winget.microsoft.com/cache/source2.msix"; + private readonly PowerShellCmdlet pwshCmdlet; private readonly HttpClientHelper httpClientHelper; private Lazy> frameworkArchitectures; @@ -119,6 +124,16 @@ public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) return this.GetAppxObject(AppInstallerName, allUsers); } + /// + /// Calls Get-AppxPackage Microsoft.Winget.Source. + /// + /// Whether to get for all users. + /// Result of Get-AppxPackage. + public PSObject? GetWinGetSourceObject(bool allUsers = false) + { + return this.GetAppxObject(WinGetSourceName, allUsers); + } + /// /// Gets the string value a property from the Get-AppxPackage object of AppInstaller. /// @@ -141,6 +156,15 @@ public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) return result; } + /// + /// Checks if winget source is installed. + /// + /// True if installed. + public bool IsWinGetSourceInstalled() + { + return this.GetWinGetSourceObject() is not null; + } + /// /// Calls Add-AppxPackage to register with AppInstaller's AppxManifest.xml. /// @@ -213,6 +237,15 @@ public async Task InstallFromGitHubReleaseAsync(string releaseTag, bool allUsers } } + /// + /// Installs the WinGet source by downloading and adding package. + /// + /// A representing the asynchronous operation. + public async Task InstallWinGetSourceAsync() + { + await this.DownloadPackageAndAddAsync(WinGetSourceUrl, WinGetSourceMsixName, null); + } + /// /// Gets the Xaml dependency package name and release tag based on the provided WinGet release tag. /// From 04ba820a4bf72f1108d0eff251166d423ab2d022 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:14:26 -0700 Subject: [PATCH 3/8] Version pattern --- .../Common/WinGetPackageManagerCmdlet.cs | 3 + .../RepairWinGetPackageManagerCmdlet.cs | 4 +- .../Commands/WinGetPackageManagerCommand.cs | 67 ++++++++++++++++++- .../Helpers/GitHubClient.cs | 12 +++- .../Helpers/WinGetVersion.cs | 8 ++- 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/WinGetPackageManagerCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/WinGetPackageManagerCmdlet.cs index 3d2f4d16d7..daddb3c243 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/WinGetPackageManagerCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/Common/WinGetPackageManagerCmdlet.cs @@ -36,6 +36,9 @@ public abstract class WinGetPackageManagerCmdlet : PSCmdlet [Parameter( ParameterSetName = Constants.IntegrityLatestSet, ValueFromPipelineByPropertyName = true)] + [Parameter( + ParameterSetName = Constants.IntegrityVersionSet, + ValueFromPipelineByPropertyName = true)] public SwitchParameter IncludePrerelease { get; set; } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs index b3e6ae4df2..d60bc088e9 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -49,7 +49,7 @@ protected override void ProcessRecord() } else { - this.command.Repair(this.Version, this.AllUsers.ToBool(), this.Force.ToBool()); + this.command.Repair(this.Version, this.AllUsers.ToBool(), this.Force.ToBool(), this.IncludePrerelease.ToBool()); } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index a711e97b60..8e54a38d71 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -8,6 +8,7 @@ namespace Microsoft.WinGet.Client.Engine.Commands { using System; using System.Collections.Generic; + using System.Linq; using System.Management.Automation; using System.Threading.Tasks; using Microsoft.WinGet.Client.Engine.Commands.Common; @@ -88,18 +89,81 @@ public void RepairUsingLatest(bool preRelease, bool allUsers, bool force) /// The expected version, if any. /// Install for all users. Requires admin. /// Force application shutdown. - public void Repair(string expectedVersion, bool allUsers, bool force) + /// Include prerelease versions when matching version. + public void Repair(string expectedVersion, bool allUsers, bool force, bool includePrerelease) { this.ValidateWhenAllUsers(allUsers); var runningTask = this.RunOnMTA( async () => { + if (!string.IsNullOrWhiteSpace(expectedVersion)) + { + var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + var allReleases = await gitHubClient.GetAllReleasesAsync(); + var allWinGetReleases = allReleases.Select(r => new WinGetVersion(r.TagName)); + var latestVersion = GetLatestMatchingVersion(allWinGetReleases, expectedVersion, includePrerelease); + if (latestVersion == null) + { + this.Write(StreamType.Warning, $"No matching version found for {expectedVersion}"); + } + else + { + expectedVersion = latestVersion.TagVersion; + this.Write(StreamType.Verbose, $"Matching version found: {expectedVersion}"); + } + } + else + { + this.Write(StreamType.Verbose, "No version specified."); + } + await this.RepairStateMachineAsync(expectedVersion, allUsers, force); return true; }); this.Wait(runningTask); } + private static WinGetVersion GetLatestMatchingVersion(IEnumerable versions, string pattern, bool includePrerelease) + { + pattern = string.IsNullOrWhiteSpace(pattern) ? "*" : pattern; + + var parts = pattern.Split('.'); + string? major = parts[0]; + string? minor = parts.Length > 1 ? parts[1] : null; + string? build = parts.Length > 2 ? parts[2] : null; + string? revision = parts.Length > 3 ? parts[3] : null; + + if (!includePrerelease) + { + versions = versions.Where(v => !v.IsPrerelease); + } + + versions = versions + .Where(v => + VersionPartMatch(major, v.Version.Major) && + VersionPartMatch(minor, v.Version.Minor) && + VersionPartMatch(build, v.Version.Build) && + VersionPartMatch(revision, v.Version.Revision)) + .OrderBy(f => f.Version); + + return versions.Count() == 0 ? null : versions.Last(); + } + + private static bool VersionPartMatch(string? partPattern, int partValue) + { + if (string.IsNullOrWhiteSpace(partPattern)) + { + return true; + } + + if (partPattern.EndsWith("*")) + { + return partValue.ToString().StartsWith(partPattern.TrimEnd('*')); + } + + return partPattern == partValue.ToString(); + } + private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers, bool force) { var seenCategories = new HashSet(); @@ -143,7 +207,6 @@ private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers case IntegrityCategory.AppInstallerNotInstalled: case IntegrityCategory.AppInstallerNotSupported: case IntegrityCategory.Failure: - System.Diagnostics.Debugger.Launch(); await this.InstallAsync(expectedVersion, allUsers, force); break; case IntegrityCategory.AppInstallerNoLicense: diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs index be2eddd2ec..e47c8de390 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs @@ -6,6 +6,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { + using System.Collections.Generic; using System.Threading.Tasks; using Octokit; @@ -63,7 +64,7 @@ public async Task GetLatestReleaseAsync(bool includePrerelease) if (includePrerelease) { // GetAll orders by newest and includes pre releases. - release = (await this.gitHubClient.Repository.Release.GetAll(this.owner, this.repo))[0]; + release = (await this.GetAllReleasesAsync())[0]; } else { @@ -72,5 +73,14 @@ public async Task GetLatestReleaseAsync(bool includePrerelease) return release; } + + /// + /// Gets all releases. + /// + /// All releases. + public async Task> GetAllReleasesAsync() + { + return await this.gitHubClient.Repository.Release.GetAll(this.owner, this.repo); + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs index 19d4704dd5..35c32bdb17 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs @@ -20,9 +20,9 @@ internal class WinGetVersion /// String Version. public WinGetVersion(string version) { - if (string.IsNullOrEmpty(version)) + if (string.IsNullOrWhiteSpace(version)) { - throw new ArgumentNullException(); + throw new ArgumentNullException(nameof(version)); } string toParseVersion = version; @@ -32,6 +32,10 @@ public WinGetVersion(string version) { this.TagVersion = version; toParseVersion = toParseVersion.Substring(1); + if (toParseVersion.Length > 0 && toParseVersion[0] == '-') + { + toParseVersion = toParseVersion.Substring(1); + } } else { From cf58d52071af8fb6020b840033fcfe8aa80f748f Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:48:10 -0700 Subject: [PATCH 4/8] CE --- .../Commands/WinGetPackageManagerCommand.cs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index 8e54a38d71..0965fe2427 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -98,19 +98,8 @@ public void Repair(string expectedVersion, bool allUsers, bool force, bool inclu { if (!string.IsNullOrWhiteSpace(expectedVersion)) { - var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); - var allReleases = await gitHubClient.GetAllReleasesAsync(); - var allWinGetReleases = allReleases.Select(r => new WinGetVersion(r.TagName)); - var latestVersion = GetLatestMatchingVersion(allWinGetReleases, expectedVersion, includePrerelease); - if (latestVersion == null) - { - this.Write(StreamType.Warning, $"No matching version found for {expectedVersion}"); - } - else - { - expectedVersion = latestVersion.TagVersion; - this.Write(StreamType.Verbose, $"Matching version found: {expectedVersion}"); - } + this.Write(StreamType.Verbose, $"Attempting to resolve version '{expectedVersion}'"); + expectedVersion = await this.ResolveVersionAsync(expectedVersion, includePrerelease); } else { @@ -164,6 +153,32 @@ private static bool VersionPartMatch(string? partPattern, int partValue) return partPattern == partValue.ToString(); } + private async Task ResolveVersionAsync(string version, bool includePrerelease) + { + try + { + var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + var allReleases = await gitHubClient.GetAllReleasesAsync(); + var allWinGetReleases = allReleases.Select(r => new WinGetVersion(r.TagName)); + var latestVersion = GetLatestMatchingVersion(allWinGetReleases, version, includePrerelease); + if (latestVersion == null) + { + this.Write(StreamType.Warning, $"No matching version found for {version}"); + return version; + } + else + { + this.Write(StreamType.Verbose, $"Matching version found: {latestVersion.TagVersion}"); + return latestVersion.TagVersion; + } + } + catch (Exception e) + { + this.Write(StreamType.Warning, $"Could not resolve version '{version}': {e.Message}"); + return version; + } + } + private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers, bool force) { var seenCategories = new HashSet(); From 60ae0d8f3d98e9b09a9ad78d1df18c6f05521f5c Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 8 Sep 2025 13:01:25 -0700 Subject: [PATCH 5/8] CE --- .../Helpers/AppxModuleHelper.cs | 2 +- .../Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs | 2 ++ .../Microsoft.WinGet.Client.Engine.csproj | 4 ++-- .../Microsoft.WinGet.Configuration.Engine.csproj | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs index e9558d75d4..40750a6c1e 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs @@ -243,7 +243,7 @@ public async Task InstallFromGitHubReleaseAsync(string releaseTag, bool allUsers /// A representing the asynchronous operation. public async Task InstallWinGetSourceAsync() { - await this.DownloadPackageAndAddAsync(WinGetSourceUrl, WinGetSourceMsixName, null); + await this.DownloadPackageAndAddAsync(WinGetSourceUrl, WinGetSourceMsixName, options: null); } /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs index 35c32bdb17..e9fece7ea9 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs @@ -32,6 +32,8 @@ public WinGetVersion(string version) { this.TagVersion = version; toParseVersion = toParseVersion.Substring(1); + + // Handle v-0.2*, v-0.3*, v-0.4* if (toParseVersion.Length > 0 && toParseVersion[0] == '-') { toParseVersion = toParseVersion.Substring(1); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj b/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj index e54da55956..31048e86c0 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Microsoft.WinGet.Client.Engine.csproj @@ -1,4 +1,4 @@ - + @@ -21,7 +21,7 @@ enable - + $(DefineConstants);USE_PROD_CLSIDS diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj index 83c29cfe17..70f53cc531 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Microsoft.WinGet.Configuration.Engine.csproj @@ -12,7 +12,7 @@ Debug;Release;ReleaseStatic - + $(DefineConstants);USE_PROD_CLSIDS From 654be227943b346dec0a4ca964c0b5b8e9c81e88 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 8 Sep 2025 15:08:12 -0700 Subject: [PATCH 6/8] Fix build and add docs --- .../Repair-WinGetPackageManager.md | 15 +++++-- .../Commands/WinGetPackageManagerCommand.cs | 45 +++++++++++++------ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md index b0f6e28a69..51d79e214f 100644 --- a/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md @@ -17,7 +17,7 @@ Repairs the installation of the WinGet client on your computer. ### IntegrityVersionSet (Default) ``` -Repair-WinGetPackageManager [-AllUsers] [-Force] [-Version ] [] +Repair-WinGetPackageManager [-AllUsers] [-Force] [-Version ] [] [-IncludePreRelease] ``` ### IntegrityLatestSet @@ -54,6 +54,16 @@ This example shows how to repair they WinGet client by installing the latest ver it functions properly. The **Force** parameter shuts down the version that is currently running so that it can update the application files. +### Example 3: Install a version with wildcards + +```powershell +Repair-WinGetPackageManager -Version "1.*.1*" -Force +``` + +This example shows how to repair the WinGet client by installing a version that matches the +specified version pattern. The **Force** parameter shuts down the version that is currently running +so that it can update the application files. + ## PARAMETERS ### -AllUsers @@ -123,8 +133,7 @@ Accept wildcard characters: False ``` ### -Version - -Use this parameter to specify the specific version of the WinGet client to install. +Specifies the version of the WinGet client to install or repair. You can provide an exact version number or use wildcard characters (for example, `"1.*.1*"`) to match and install the latest version that fits the pattern. ```yaml Type: System.String diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index 0965fe2427..ef6525184c 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -112,15 +112,28 @@ public void Repair(string expectedVersion, bool allUsers, bool force, bool inclu this.Wait(runningTask); } - private static WinGetVersion GetLatestMatchingVersion(IEnumerable versions, string pattern, bool includePrerelease) + /// + /// Tries to get the latest version matching the pattern. + /// + /// + /// Pattern only supports trailing wildcards. + /// - For example, the pattern can be: 1.11.*, 1.11.3* + /// - But it cannot be: 1.*1.1 or 1.*1*.1. + /// + /// List of versions to match against. + /// Pattern to match. + /// Include prerelease versions. + /// The resulting version. + /// True if a matching version was found. + private static bool TryGetLatestMatchingVersion(IEnumerable versions, string pattern, bool includePrerelease, out WinGetVersion result) { pattern = string.IsNullOrWhiteSpace(pattern) ? "*" : pattern; var parts = pattern.Split('.'); - string? major = parts[0]; - string? minor = parts.Length > 1 ? parts[1] : null; - string? build = parts.Length > 2 ? parts[2] : null; - string? revision = parts.Length > 3 ? parts[3] : null; + string major = parts[0]; + string minor = parts.Length > 1 ? parts[1] : string.Empty; + string build = parts.Length > 2 ? parts[2] : string.Empty; + string revision = parts.Length > 3 ? parts[3] : string.Empty; if (!includePrerelease) { @@ -135,10 +148,17 @@ private static WinGetVersion GetLatestMatchingVersion(IEnumerable VersionPartMatch(revision, v.Version.Revision)) .OrderBy(f => f.Version); - return versions.Count() == 0 ? null : versions.Last(); + if (!versions.Any()) + { + result = null!; + return false; + } + + result = versions.Last(); + return true; } - private static bool VersionPartMatch(string? partPattern, int partValue) + private static bool VersionPartMatch(string partPattern, int partValue) { if (string.IsNullOrWhiteSpace(partPattern)) { @@ -160,17 +180,14 @@ private async Task ResolveVersionAsync(string version, bool includePrere var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); var allReleases = await gitHubClient.GetAllReleasesAsync(); var allWinGetReleases = allReleases.Select(r => new WinGetVersion(r.TagName)); - var latestVersion = GetLatestMatchingVersion(allWinGetReleases, version, includePrerelease); - if (latestVersion == null) - { - this.Write(StreamType.Warning, $"No matching version found for {version}"); - return version; - } - else + if (TryGetLatestMatchingVersion(allWinGetReleases, version, includePrerelease, out var latestVersion)) { this.Write(StreamType.Verbose, $"Matching version found: {latestVersion.TagVersion}"); return latestVersion.TagVersion; } + + this.Write(StreamType.Warning, $"No matching version found for {version}"); + return version; } catch (Exception e) { From 0defac8f2b7ac8eac7568271bd96c580381106e9 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:24:44 -0700 Subject: [PATCH 7/8] Addressing comments --- .../Repair-WinGetPackageManager.md | 4 ++-- .../Commands/WinGetPackageManagerCommand.cs | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md index 51d79e214f..24a82423f9 100644 --- a/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md +++ b/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md @@ -17,7 +17,7 @@ Repairs the installation of the WinGet client on your computer. ### IntegrityVersionSet (Default) ``` -Repair-WinGetPackageManager [-AllUsers] [-Force] [-Version ] [] [-IncludePreRelease] +Repair-WinGetPackageManager [-AllUsers] [-Force] [-Version ] [-IncludePreRelease] [] ``` ### IntegrityLatestSet @@ -57,7 +57,7 @@ that it can update the application files. ### Example 3: Install a version with wildcards ```powershell -Repair-WinGetPackageManager -Version "1.*.1*" -Force +Repair-WinGetPackageManager -Version "1.12.*" -Force ``` This example shows how to repair the WinGet client by installing a version that matches the diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index ef6525184c..6d1dbd7283 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -116,9 +116,9 @@ public void Repair(string expectedVersion, bool allUsers, bool force, bool inclu /// Tries to get the latest version matching the pattern. /// /// - /// Pattern only supports trailing wildcards. - /// - For example, the pattern can be: 1.11.*, 1.11.3* - /// - But it cannot be: 1.*1.1 or 1.*1*.1. + /// Pattern only supports leading and trailing wildcards. + /// - For example, the pattern can be: 1.11.*, 1.11.3*, 1.11.*3 + /// - But it cannot be: 1.*1*.1 or 1.1*1.1. /// /// List of versions to match against. /// Pattern to match. @@ -130,10 +130,10 @@ private static bool TryGetLatestMatchingVersion(IEnumerable versi pattern = string.IsNullOrWhiteSpace(pattern) ? "*" : pattern; var parts = pattern.Split('.'); - string major = parts[0]; - string minor = parts.Length > 1 ? parts[1] : string.Empty; - string build = parts.Length > 2 ? parts[2] : string.Empty; - string revision = parts.Length > 3 ? parts[3] : string.Empty; + var major = parts.ElementAtOrDefault(0); + var minor = parts.ElementAtOrDefault(1); + var build = parts.ElementAtOrDefault(2); + var revision = parts.ElementAtOrDefault(3); if (!includePrerelease) { @@ -158,14 +158,19 @@ private static bool TryGetLatestMatchingVersion(IEnumerable versi return true; } - private static bool VersionPartMatch(string partPattern, int partValue) + private static bool VersionPartMatch(string? partPattern, int partValue) { if (string.IsNullOrWhiteSpace(partPattern)) { return true; } - if (partPattern.EndsWith("*")) + if (partPattern!.StartsWith("*")) + { + return partValue.ToString().EndsWith(partPattern.TrimStart('*')); + } + + if (partPattern!.EndsWith("*")) { return partValue.ToString().StartsWith(partPattern.TrimEnd('*')); } From af4e87406e9548c9c5d61fbda467fe8d57411577 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 29 Sep 2025 12:39:45 -0700 Subject: [PATCH 8/8] Addressing comments --- .../Commands/WinGetPackageManagerCommand.cs | 109 +++--------------- .../Helpers/AppxModuleHelper.cs | 5 +- .../Helpers/GitHubClient.cs | 94 +++++++++++++++ 3 files changed, 114 insertions(+), 94 deletions(-) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs index 6d1dbd7283..f6ca0465df 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -8,7 +8,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands { using System; using System.Collections.Generic; - using System.Linq; using System.Management.Automation; using System.Threading.Tasks; using Microsoft.WinGet.Client.Engine.Commands.Common; @@ -99,7 +98,24 @@ public void Repair(string expectedVersion, bool allUsers, bool force, bool inclu if (!string.IsNullOrWhiteSpace(expectedVersion)) { this.Write(StreamType.Verbose, $"Attempting to resolve version '{expectedVersion}'"); - expectedVersion = await this.ResolveVersionAsync(expectedVersion, includePrerelease); + var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + try + { + var resolvedVersion = await gitHubClient.ResolveVersionAsync(expectedVersion, includePrerelease); + if (!string.IsNullOrEmpty(resolvedVersion)) + { + this.Write(StreamType.Verbose, $"Matching version found: {resolvedVersion}"); + expectedVersion = resolvedVersion!; + } + else + { + this.Write(StreamType.Warning, $"No matching version found for {expectedVersion}"); + } + } + catch (Exception ex) + { + this.Write(StreamType.Warning, $"Could not resolve version '{expectedVersion}': {ex.Message}"); + } } else { @@ -112,95 +128,6 @@ public void Repair(string expectedVersion, bool allUsers, bool force, bool inclu this.Wait(runningTask); } - /// - /// Tries to get the latest version matching the pattern. - /// - /// - /// Pattern only supports leading and trailing wildcards. - /// - For example, the pattern can be: 1.11.*, 1.11.3*, 1.11.*3 - /// - But it cannot be: 1.*1*.1 or 1.1*1.1. - /// - /// List of versions to match against. - /// Pattern to match. - /// Include prerelease versions. - /// The resulting version. - /// True if a matching version was found. - private static bool TryGetLatestMatchingVersion(IEnumerable versions, string pattern, bool includePrerelease, out WinGetVersion result) - { - pattern = string.IsNullOrWhiteSpace(pattern) ? "*" : pattern; - - var parts = pattern.Split('.'); - var major = parts.ElementAtOrDefault(0); - var minor = parts.ElementAtOrDefault(1); - var build = parts.ElementAtOrDefault(2); - var revision = parts.ElementAtOrDefault(3); - - if (!includePrerelease) - { - versions = versions.Where(v => !v.IsPrerelease); - } - - versions = versions - .Where(v => - VersionPartMatch(major, v.Version.Major) && - VersionPartMatch(minor, v.Version.Minor) && - VersionPartMatch(build, v.Version.Build) && - VersionPartMatch(revision, v.Version.Revision)) - .OrderBy(f => f.Version); - - if (!versions.Any()) - { - result = null!; - return false; - } - - result = versions.Last(); - return true; - } - - private static bool VersionPartMatch(string? partPattern, int partValue) - { - if (string.IsNullOrWhiteSpace(partPattern)) - { - return true; - } - - if (partPattern!.StartsWith("*")) - { - return partValue.ToString().EndsWith(partPattern.TrimStart('*')); - } - - if (partPattern!.EndsWith("*")) - { - return partValue.ToString().StartsWith(partPattern.TrimEnd('*')); - } - - return partPattern == partValue.ToString(); - } - - private async Task ResolveVersionAsync(string version, bool includePrerelease) - { - try - { - var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); - var allReleases = await gitHubClient.GetAllReleasesAsync(); - var allWinGetReleases = allReleases.Select(r => new WinGetVersion(r.TagName)); - if (TryGetLatestMatchingVersion(allWinGetReleases, version, includePrerelease, out var latestVersion)) - { - this.Write(StreamType.Verbose, $"Matching version found: {latestVersion.TagVersion}"); - return latestVersion.TagVersion; - } - - this.Write(StreamType.Warning, $"No matching version found for {version}"); - return version; - } - catch (Exception e) - { - this.Write(StreamType.Warning, $"Could not resolve version '{version}': {e.Message}"); - return version; - } - } - private async Task RepairStateMachineAsync(string expectedVersion, bool allUsers, bool force) { var seenCategories = new HashSet(); diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs index 40750a6c1e..0e85774bcd 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs @@ -127,11 +127,10 @@ public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) /// /// Calls Get-AppxPackage Microsoft.Winget.Source. /// - /// Whether to get for all users. /// Result of Get-AppxPackage. - public PSObject? GetWinGetSourceObject(bool allUsers = false) + public PSObject? GetWinGetSourceObject() { - return this.GetAppxObject(WinGetSourceName, allUsers); + return this.GetAppxObject(WinGetSourceName); } /// diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs index e47c8de390..0d73a50736 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs @@ -6,9 +6,13 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { + using System; using System.Collections.Generic; + using System.Linq; using System.Threading.Tasks; + using Microsoft.WinGet.Common.Command; using Octokit; + using static Microsoft.WinGet.Client.Engine.Common.Constants; /// /// Handles GitHub interactions. @@ -82,5 +86,95 @@ public async Task> GetAllReleasesAsync() { return await this.gitHubClient.Repository.Release.GetAll(this.owner, this.repo); } + + /// + /// Resolve a version string to the latest matching version from GitHub releases. + /// + /// Version string to resolve. Can include wildcards (*). + /// Whether to include prerelease versions in the search. + /// Resolved version string or null if no match found. + public async Task ResolveVersionAsync(string version, bool includePrerelease) + { + var allReleases = await this.GetAllReleasesAsync(); + var allWinGetReleases = allReleases.Select(r => new WinGetVersion(r.TagName)); + if (TryGetLatestMatchingVersion(allWinGetReleases, version, includePrerelease, out var latestVersion)) + { + return latestVersion.TagVersion; + } + + return null; + } + + /// + /// Tries to get the latest version matching the pattern. + /// + /// + /// Pattern only supports leading and trailing wildcards. + /// - For example, the pattern can be: 1.11.*, 1.11.3*, 1.11.*3 + /// - But it cannot be: 1.*1*.1 or 1.1*1.1. + /// + /// List of versions to match against. + /// Pattern to match. + /// Include prerelease versions. + /// The resulting version. + /// True if a matching version was found. + private static bool TryGetLatestMatchingVersion(IEnumerable versions, string pattern, bool includePrerelease, out WinGetVersion result) + { + pattern = string.IsNullOrWhiteSpace(pattern) ? "*" : pattern; + + var parts = pattern.Split('.'); + var major = parts.ElementAtOrDefault(0); + var minor = parts.ElementAtOrDefault(1); + var build = parts.ElementAtOrDefault(2); + var revision = parts.ElementAtOrDefault(3); + + if (!includePrerelease) + { + versions = versions.Where(v => !v.IsPrerelease); + } + + versions = versions + .Where(v => + VersionPartMatch(major, v.Version.Major) && + VersionPartMatch(minor, v.Version.Minor) && + VersionPartMatch(build, v.Version.Build) && + VersionPartMatch(revision, v.Version.Revision)) + .OrderBy(f => f.Version); + + if (!versions.Any()) + { + result = null!; + return false; + } + + result = versions.Last(); + return true; + } + + /// + /// Checks if a version part matches a pattern. + /// + /// Version part pattern. + /// Version part value. + /// True if the part matches the pattern. + private static bool VersionPartMatch(string? partPattern, int partValue) + { + if (string.IsNullOrWhiteSpace(partPattern)) + { + return true; + } + + if (partPattern!.StartsWith("*")) + { + return partValue.ToString().EndsWith(partPattern.TrimStart('*')); + } + + if (partPattern!.EndsWith("*")) + { + return partValue.ToString().StartsWith(partPattern.TrimEnd('*')); + } + + return partPattern == partValue.ToString(); + } } }