diff --git a/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md b/src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md index b0f6e28a69..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 ] [] +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.12.*" -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.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 cdf5f16d51..f6ca0465df 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -88,12 +88,40 @@ 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)) + { + this.Write(StreamType.Verbose, $"Attempting to resolve version '{expectedVersion}'"); + 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 + { + this.Write(StreamType.Verbose, "No version specified."); + } + await this.RepairStateMachineAsync(expectedVersion, allUsers, force); return true; }); @@ -138,7 +166,7 @@ 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: @@ -156,6 +184,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: @@ -197,10 +228,17 @@ 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 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); - appxModule.RegisterAppInstaller(toRegisterVersion); + await appxModule.RegisterAppInstallerAsync(toRegisterVersion, allUsers); } private void RepairEnvPath() 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 e8b8d1ad09..0e85774bcd 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"; @@ -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; @@ -112,21 +117,32 @@ public AppxModuleHelper(PowerShellCmdlet pwshCmdlet) /// /// Calls Get-AppxPackage Microsoft.DesktopAppInstaller. /// + /// Whether to get for all users. + /// Result of Get-AppxPackage. + public PSObject? GetAppInstallerObject(bool allUsers = false) + { + return this.GetAppxObject(AppInstallerName, allUsers); + } + + /// + /// Calls Get-AppxPackage Microsoft.Winget.Source. + /// /// Result of Get-AppxPackage. - public PSObject? GetAppInstallerObject() + public PSObject? GetWinGetSourceObject() { - return this.GetAppxObject(AppInstallerName); + return this.GetAppxObject(WinGetSourceName); } /// /// 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(); @@ -139,15 +155,26 @@ 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. /// /// 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 +184,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 +196,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); } /// @@ -231,6 +236,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, options: null); + } + /// /// Gets the Xaml dependency package name and release tag based on the provided WinGet release tag. /// @@ -278,6 +292,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 +338,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 +833,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/Helpers/GitHubClient.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs index be2eddd2ec..0d73a50736 100644 --- a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs +++ b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs @@ -6,8 +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. @@ -63,7 +68,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 +77,104 @@ 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); + } + + /// + /// 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(); + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/WinGetVersion.cs index 19d4704dd5..e9fece7ea9 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,12 @@ 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); + } } else { 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..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 @@ - +