diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index afb5552192..baafb242af 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -265,6 +265,7 @@ PCCERT
PCs
pcwsz
PEGI
+PFM
pfn
pfxpath
Pherson
@@ -282,6 +283,7 @@ processthreads
productcode
pscustomobject
pseudocode
+PSHOST
psobject
ptstr
publickey
@@ -300,6 +302,7 @@ REFIID
regexes
REGSAM
relativefilepath
+remoting
reparse
restsource
rgex
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/AssertWinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/AssertWinGetPackageManagerCommand.cs
new file mode 100644
index 0000000000..6cfd6cea9b
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Commands/AssertWinGetPackageManagerCommand.cs
@@ -0,0 +1,43 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Commands
+{
+ using System.Management.Automation;
+ using Microsoft.WinGet.Client.Commands.Common;
+ using Microsoft.WinGet.Client.Common;
+ using Microsoft.WinGet.Client.Helpers;
+
+ ///
+ /// Assert-WinGetPackageManager. Verifies winget is installed properly.
+ ///
+ [Cmdlet(
+ VerbsLifecycle.Assert,
+ Constants.WinGetNouns.WinGetPackageManager,
+ DefaultParameterSetName = Constants.IntegrityVersionSet)]
+ public class AssertWinGetPackageManagerCommand : BaseIntegrityCommand
+ {
+ ///
+ /// Validates winget is installed correctly. If not, throws an exception
+ /// with the reason why, if any.
+ ///
+ protected override void ProcessRecord()
+ {
+ string expectedVersion = string.Empty;
+ if (this.ParameterSetName == Constants.IntegrityLatestSet)
+ {
+ var gitHubRelease = new GitHubRelease();
+ expectedVersion = gitHubRelease.GetLatestVersionTagName(this.IncludePreRelease.ToBool());
+ }
+ else
+ {
+ expectedVersion = this.Version;
+ }
+
+ WinGetIntegrity.AssertWinGet(this, expectedVersion);
+ }
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseIntegrityCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseIntegrityCommand.cs
new file mode 100644
index 0000000000..59cf7f971f
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Commands/Common/BaseIntegrityCommand.cs
@@ -0,0 +1,41 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Commands.Common
+{
+ using System.Management.Automation;
+ using Microsoft.WinGet.Client.Common;
+
+ ///
+ /// Common parameters for Assert-WinGetPackageManager and Repair-WinGetPackageManager.
+ ///
+ public abstract class BaseIntegrityCommand : BaseCommand
+ {
+ ///
+ /// Gets or sets the optional version.
+ ///
+ [Parameter(
+ ParameterSetName = Constants.IntegrityVersionSet,
+ ValueFromPipelineByPropertyName = true)]
+ public string Version { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether to use latest.
+ ///
+ [Parameter(
+ ParameterSetName = Constants.IntegrityLatestSet,
+ ValueFromPipelineByPropertyName = true)]
+ public SwitchParameter Latest { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to include prerelease winget versions.
+ ///
+ [Parameter(
+ ParameterSetName = Constants.IntegrityLatestSet,
+ ValueFromPipelineByPropertyName = true)]
+ public SwitchParameter IncludePreRelease { get; set; }
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/GetVersionCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/GetVersionCommand.cs
new file mode 100644
index 0000000000..5253713186
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Commands/GetVersionCommand.cs
@@ -0,0 +1,29 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Commands
+{
+ using System.Management.Automation;
+ using Microsoft.WinGet.Client.Commands.Common;
+ using Microsoft.WinGet.Client.Common;
+ using Microsoft.WinGet.Client.Helpers;
+
+ ///
+ /// Get-WinGetVersion. Gets the current version of winget.
+ ///
+ [Cmdlet(VerbsCommon.Get, Constants.WinGetNouns.Version)]
+ [OutputType(typeof(string))]
+ public class GetVersionCommand : BaseCommand
+ {
+ ///
+ /// Writes the winget version.
+ ///
+ protected override void ProcessRecord()
+ {
+ this.WriteObject(WinGetVersion.InstalledWinGetVersion.TagVersion);
+ }
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Commands/RepairWinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client/Commands/RepairWinGetPackageManagerCommand.cs
new file mode 100644
index 0000000000..22c2a8b3f5
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Commands/RepairWinGetPackageManagerCommand.cs
@@ -0,0 +1,183 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Commands
+{
+ using System;
+ using System.Management.Automation;
+ using Microsoft.WinGet.Client.Commands.Common;
+ using Microsoft.WinGet.Client.Common;
+ using Microsoft.WinGet.Client.Helpers;
+ using Microsoft.WinGet.Client.Properties;
+
+ ///
+ /// Repair-WinGetPackageManager. Repairs winget if needed.
+ ///
+ [Cmdlet(
+ VerbsDiagnostic.Repair,
+ Constants.WinGetNouns.WinGetPackageManager,
+ DefaultParameterSetName = Constants.IntegrityVersionSet)]
+ [OutputType(typeof(int))]
+ public class RepairWinGetPackageManagerCommand : BaseIntegrityCommand
+ {
+ private const string EnvPath = "env:PATH";
+ private const int Succeeded = 0;
+ private const int Failed = -1;
+
+ private static readonly string[] WriteInformationTags = new string[] { "PSHOST" };
+
+ ///
+ /// Attempts to repair winget.
+ /// TODO: consider WhatIf and Confirm options.
+ ///
+ protected override void ProcessRecord()
+ {
+ int result = Failed;
+
+ string expectedVersion = this.Version;
+ if (this.ParameterSetName == Constants.IntegrityLatestSet)
+ {
+ var gitHubRelease = new GitHubRelease();
+ expectedVersion = gitHubRelease.GetLatestVersionTagName(this.IncludePreRelease.ToBool());
+ }
+
+ var integrityCategory = WinGetIntegrity.GetIntegrityCategory(this, expectedVersion);
+ this.WriteDebug($"Integrity category type: {integrityCategory}");
+
+ if (integrityCategory == IntegrityCategory.Installed ||
+ integrityCategory == IntegrityCategory.UnexpectedVersion)
+ {
+ result = this.VerifyWinGetInstall(integrityCategory, expectedVersion);
+ }
+ else if (integrityCategory == IntegrityCategory.NotInPath)
+ {
+ this.RepairEnvPath();
+
+ // Now try again and get the desired winget version if needed.
+ var newIntegrityCategory = WinGetIntegrity.GetIntegrityCategory(this, expectedVersion);
+ this.WriteDebug($"Integrity category after fixing PATH {newIntegrityCategory}");
+ result = this.VerifyWinGetInstall(newIntegrityCategory, expectedVersion);
+ }
+ else if (integrityCategory == IntegrityCategory.AppInstallerNotRegistered)
+ {
+ var appxModule = new AppxModuleHelper(this);
+ appxModule.RegisterAppInstaller();
+
+ // Now try again and get the desired winget version if needed.
+ var newIntegrityCategory = WinGetIntegrity.GetIntegrityCategory(this, expectedVersion);
+ this.WriteDebug($"Integrity category after registering {newIntegrityCategory}");
+ result = this.VerifyWinGetInstall(newIntegrityCategory, expectedVersion);
+ }
+ else if (integrityCategory == IntegrityCategory.AppInstallerNotInstalled ||
+ integrityCategory == IntegrityCategory.AppInstallerNotSupported ||
+ integrityCategory == IntegrityCategory.Failure)
+ {
+ // If we are here and expectedVersion is empty, it means that they just ran Repair-WinGetPackageManager.
+ // When there is not version specified, we don't want to assume an empty version means latest, but in
+ // this particular case we need to.
+ if (string.IsNullOrEmpty(expectedVersion))
+ {
+ var gitHubRelease = new GitHubRelease();
+ expectedVersion = gitHubRelease.GetLatestVersionTagName(false);
+ }
+
+ if (this.DownloadAndInstall(expectedVersion, false))
+ {
+ result = Succeeded;
+ }
+ else
+ {
+ this.WriteDebug($"Failed installing {expectedVersion}");
+ }
+ }
+ else if (integrityCategory == IntegrityCategory.AppExecutionAliasDisabled)
+ {
+ // Sorry, but the user has to manually enabled it.
+ this.WriteInformation(Resources.AppExecutionAliasDisabledHelpMessage, WriteInformationTags);
+ }
+ else
+ {
+ this.WriteInformation(Resources.WinGetNotSupportedMessage, WriteInformationTags);
+ }
+
+ this.WriteObject(result);
+ }
+
+ private int VerifyWinGetInstall(IntegrityCategory integrityCategory, string expectedVersion)
+ {
+ if (integrityCategory == IntegrityCategory.Installed)
+ {
+ // Nothing to do
+ this.WriteDebug($"WinGet is in a good state.");
+ return Succeeded;
+ }
+ else if (integrityCategory == IntegrityCategory.UnexpectedVersion)
+ {
+ // The versions are different, download and install.
+ if (!this.InstallDifferentVersion(new WinGetVersion(expectedVersion)))
+ {
+ this.WriteDebug($"Failed installing {expectedVersion}");
+ }
+ else
+ {
+ return Succeeded;
+ }
+ }
+
+ return Failed;
+ }
+
+ private bool InstallDifferentVersion(WinGetVersion toInstallVersion)
+ {
+ var installedVersion = WinGetVersion.InstalledWinGetVersion;
+
+ this.WriteDebug($"Installed WinGet version {installedVersion.TagVersion}");
+ this.WriteDebug($"Installing WinGet version {toInstallVersion.TagVersion}");
+
+ bool downgrade = false;
+ if (installedVersion.CompareAsDeployment(toInstallVersion) > 0)
+ {
+ downgrade = true;
+ }
+
+ return this.DownloadAndInstall(toInstallVersion.TagVersion, downgrade);
+ }
+
+ private bool DownloadAndInstall(string versionTag, bool downgrade)
+ {
+ // Download and install.
+ var gitHubRelease = new GitHubRelease();
+ var downloadedMsixBundlePath = gitHubRelease.DownloadRelease(versionTag);
+
+ var appxModule = new AppxModuleHelper(this);
+ appxModule.AddAppInstallerBundle(downloadedMsixBundlePath, downgrade);
+
+ // Verify that is installed
+ var integrityCategory = WinGetIntegrity.GetIntegrityCategory(this, versionTag);
+ if (integrityCategory != IntegrityCategory.Installed)
+ {
+ return false;
+ }
+
+ this.WriteDebug($"Installed WinGet version {versionTag}");
+ return true;
+ }
+
+ private void RepairEnvPath()
+ {
+ // Add windows app path to user PATH environment variable
+ Utilities.AddWindowsAppToPath();
+
+ // Update this sessions PowerShell environment so the user doesn't have to restart the terminal.
+ string envPathUser = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.User);
+ string envPathMachine = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.Machine);
+ string newPwshPathEnv = $"{envPathMachine};{envPathUser}";
+ this.SessionState.PSVariable.Set(EnvPath, newPwshPathEnv);
+
+ this.WriteDebug($"PATH environment variable updated");
+ }
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/Constants.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/Constants.cs
index a165353fa4..31f03f65f9 100644
--- a/src/PowerShell/Microsoft.WinGet.Client/Common/Constants.cs
+++ b/src/PowerShell/Microsoft.WinGet.Client/Common/Constants.cs
@@ -32,13 +32,51 @@ internal static class Constants
/// This parameter set indicates that a package was not provided via a parameter or the pipeline and it
/// needs to be found by searching a package source.
///
- public const string FoundSet = "FoundSet";
-
+ public const string FoundSet = "FoundSet";
+
+ ///
+ /// Parameter set for an specific version parameter.
+ ///
+ public const string IntegrityVersionSet = "IntegrityVersionSet";
+
+ ///
+ /// Parameter set for an latest version with optional prerelease version.
+ ///
+ public const string IntegrityLatestSet = "IntegrityLatestSet";
+
+ ///
+ /// WinGet package family name.
+ ///
+#if USE_PROD_CLSIDS
+ public const string WingetPackageFamilyName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe";
+#else
+ public const string WingetPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe";
+#endif
+
+ ///
+ /// Winget executable name.
+ ///
+#if USE_PROD_CLSIDS
+ public const string WinGetExe = "winget.exe";
+#else
+ public const string WinGetExe = "wingetdev.exe";
+#endif
+
+ ///
+ /// Name of PATH environment variable.
+ ///
+ public const string PathEnvVar = "PATH";
+
///
/// Nouns used for different cmdlets. Changing this will alter the names of the related commands.
///
public static class WinGetNouns
{
+ ///
+ /// WinGet.
+ ///
+ public const string WinGetPackageManager = "WinGetPackageManager";
+
///
/// The noun analogue of the class.
///
@@ -53,6 +91,11 @@ public static class WinGetNouns
/// The noun for any user settings cmdlet.
///
public const string UserSettings = "WinGetUserSettings";
+
+ ///
+ /// The noun for winget version.
+ ///
+ public const string Version = "WinGetVersion";
}
}
}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/IntegrityCategory.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/IntegrityCategory.cs
new file mode 100644
index 0000000000..b6ebbd136e
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Common/IntegrityCategory.cs
@@ -0,0 +1,64 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Common
+{
+ ///
+ /// The type of the integrity check failure.
+ ///
+ public enum IntegrityCategory
+ {
+ ///
+ /// WinGet is correctly installed.
+ ///
+ Installed,
+
+ ///
+ /// The version installed is not what is expected.
+ ///
+ UnexpectedVersion,
+
+ ///
+ /// Unknown reason.
+ ///
+ Unknown,
+
+ ///
+ /// A failure resulted on a simple winget command that shouldn't happen.
+ ///
+ Failure,
+
+ ///
+ /// WindowsAppPath not in PATH environment variable.
+ ///
+ NotInPath,
+
+ ///
+ /// Winget's app execution alias disabled.
+ ///
+ AppExecutionAliasDisabled,
+
+ ///
+ /// Windows OS is not supported.
+ ///
+ OsNotSupported,
+
+ ///
+ /// AppInstaller package is not installed.
+ ///
+ AppInstallerNotInstalled,
+
+ ///
+ /// AppInstaller package is not registered.
+ ///
+ AppInstallerNotRegistered,
+
+ ///
+ /// Installed App Installer package is not supported.
+ ///
+ AppInstallerNotSupported,
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/Utilities.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/Utilities.cs
index 2fb27efe91..7b4de03367 100644
--- a/src/PowerShell/Microsoft.WinGet.Client/Common/Utilities.cs
+++ b/src/PowerShell/Microsoft.WinGet.Client/Common/Utilities.cs
@@ -6,7 +6,7 @@
namespace Microsoft.WinGet.Client.Common
{
- using System.Resources;
+ using System;
using System.Security.Principal;
///
@@ -41,5 +41,43 @@ public static bool ExecutingAsSystem
return principal.IsInRole(WindowsBuiltInRole.SystemOperator);
}
}
+
+ ///
+ /// Gets the windows app path for local app data.
+ ///
+ public static string LocalDataWindowsAppPath
+ {
+ get
+ {
+ return Environment.ExpandEnvironmentVariables(@"%LOCALAPPDATA%\Microsoft\WindowsApps");
+ }
+ }
+
+ ///
+ /// Gets the windows app path for program files.
+ ///
+ public static string ProgramFilesWindowsAppPath
+ {
+ get
+ {
+ return Environment.ExpandEnvironmentVariables(@"%PROGRAMFILES%\WindowsApps");
+ }
+ }
+
+ ///
+ /// Adds the WindowsApp local app data path to the user environment path.
+ ///
+ public static void AddWindowsAppToPath()
+ {
+ var scope = EnvironmentVariableTarget.User;
+ string envPathValue = Environment.GetEnvironmentVariable(Constants.PathEnvVar, scope);
+ if (string.IsNullOrEmpty(envPathValue) || !envPathValue.Contains(Utilities.LocalDataWindowsAppPath))
+ {
+ Environment.SetEnvironmentVariable(
+ Constants.PathEnvVar,
+ $"{envPathValue};{Utilities.LocalDataWindowsAppPath}",
+ scope);
+ }
+ }
}
}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client/Common/WinGetIntegrity.cs
new file mode 100644
index 0000000000..a5a8954cd9
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Common/WinGetIntegrity.cs
@@ -0,0 +1,147 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Common
+{
+ using System;
+ using System.ComponentModel;
+ using System.IO;
+ using System.Management.Automation;
+ using Microsoft.WinGet.Client.Exceptions;
+ using Microsoft.WinGet.Client.Helpers;
+ using Microsoft.WinGet.Client.Properties;
+
+ ///
+ /// Validates winget runs correctly.
+ ///
+ internal static class WinGetIntegrity
+ {
+ ///
+ /// Verifies winget runs correctly. If it doesn't, tries to find the reason why it failed.
+ ///
+ /// The calling cmdlet.
+ /// Expected version.
+ public static void AssertWinGet(PSCmdlet psCmdlet, string expectedVersion)
+ {
+ try
+ {
+ // Start by calling winget without its WindowsApp PFN path.
+ // If it succeeds and the exit code is 0 then we are good.
+ var wingetCliWrapper = new WingetCLIWrapper(false);
+ var result = wingetCliWrapper.RunCommand("--version");
+ result.VerifyExitCode();
+ }
+ catch (Win32Exception)
+ {
+ throw new WinGetIntegrityException(GetReason(psCmdlet));
+ }
+ catch (Exception e) when (e is WinGetCLIException || e is WinGetCLITimeoutException)
+ {
+ throw new WinGetIntegrityException(IntegrityCategory.Failure, e);
+ }
+ catch (Exception e)
+ {
+ throw new WinGetIntegrityException(IntegrityCategory.Unknown, e);
+ }
+
+ // WinGet is installed. Verify version if needed.
+ if (!string.IsNullOrEmpty(expectedVersion))
+ {
+ // This assumes caller knows that the version exist.
+ WinGetVersion expectedWinGetVersion = new WinGetVersion(expectedVersion);
+ var installedVersion = WinGetVersion.InstalledWinGetVersion;
+ if (expectedWinGetVersion.CompareTo(installedVersion) != 0)
+ {
+ throw new WinGetIntegrityException(
+ IntegrityCategory.UnexpectedVersion,
+ string.Format(
+ Resources.IntegrityUnexpectedVersionMessage,
+ installedVersion,
+ expectedVersion));
+ }
+ }
+ }
+
+ ///
+ /// Verifies winget runs correctly.
+ ///
+ /// The calling cmdlet.
+ /// Expected version.
+ /// Integrity category.
+ public static IntegrityCategory GetIntegrityCategory(PSCmdlet psCmdlet, string expectedVersion)
+ {
+ try
+ {
+ AssertWinGet(psCmdlet, expectedVersion);
+ }
+ catch (WinGetIntegrityException e)
+ {
+ return e.Category;
+ }
+
+ return IntegrityCategory.Installed;
+ }
+
+ private static IntegrityCategory GetReason(PSCmdlet psCmdlet)
+ {
+ // Ok, so you are here because calling winget --version failed. Lets try to figure out why.
+
+ // First lets check if the file is there, which means it is installed or someone is taking our place.
+ if (File.Exists(WingetCLIWrapper.WinGetFullPath))
+ {
+ // The file exists, but we couldn't call it... Well maybe winget's app execution alias is not enabled.
+ // The trick is knowing that a magical file appears under WindowsApp when its enabled.
+ string wingetAliasPath = Path.Combine(Utilities.LocalDataWindowsAppPath, Constants.WinGetExe);
+ if (File.Exists(wingetAliasPath))
+ {
+ // App execution alias is enabled. Then maybe the path?
+ string envPath = Environment.GetEnvironmentVariable(Constants.PathEnvVar, EnvironmentVariableTarget.User);
+ if (string.IsNullOrEmpty(envPath) ||
+ !envPath.EndsWith(Utilities.LocalDataWindowsAppPath) ||
+ !envPath.Contains($"{Utilities.LocalDataWindowsAppPath};"))
+ {
+ return IntegrityCategory.NotInPath;
+ }
+ }
+ else
+ {
+ return IntegrityCategory.AppExecutionAliasDisabled;
+ }
+ }
+
+ // Not under %LOCALAPPDATA%\\Microsoft\\WindowsApps\PFM\
+
+ // Windows version has to be equal or newer than 10.0.17763.0
+ var minWindowsVersion = new Version(10, 0, 17763, 0);
+ var osVersion = Environment.OSVersion.Version;
+ if (osVersion.CompareTo(minWindowsVersion) < 0)
+ {
+ return IntegrityCategory.OsNotSupported;
+ }
+
+ // It could be that AppInstaller package is old or the package is not
+ // registered at this point. To know that, call Get-AppxPackage.
+ var appxModule = new AppxModuleHelper(psCmdlet);
+ string version = appxModule.GetAppInstallerPropertyValue("Version");
+ if (version is null)
+ {
+ // This can happen in Windows Sandbox.
+ return IntegrityCategory.AppInstallerNotInstalled;
+ }
+
+ // Now AppInstaller version has to be greater than 1.11.11451
+ var minAppInstallerVersion = new Version(1, 11, 11451);
+ var appInstallerVersion = new Version(version);
+ if (appInstallerVersion.CompareTo(minAppInstallerVersion) < 0)
+ {
+ return IntegrityCategory.AppInstallerNotSupported;
+ }
+
+ // If we get here, we know the package is in the machine but not registered for the user.
+ return IntegrityCategory.AppInstallerNotRegistered;
+ }
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json b/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json
index 212d4e2e39..35186772f7 100644
--- a/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json
+++ b/src/PowerShell/Microsoft.WinGet.Client/Crescendo/Crescendo.json
@@ -1,17 +1,6 @@
{
"$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
"Commands": [
- {
- "Verb": "Get",
- "Noun": "WinGetVersion",
- "Platform": [
- "Windows"
- ],
- "OriginalName": "winget.exe",
- "OriginalCommandElements": [
- "--version"
- ]
- },
{
"Verb": "Enable",
"Noun": "WinGetSetting",
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetCLITimeoutException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetCLITimeoutException.cs
new file mode 100644
index 0000000000..8ad5ff7e04
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetCLITimeoutException.cs
@@ -0,0 +1,27 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Exceptions
+{
+ using System;
+ using Microsoft.WinGet.Client.Properties;
+
+ ///
+ /// Time out exception for a winget cli command.
+ ///
+ public class WinGetCLITimeoutException : TimeoutException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Command.
+ /// Parameters.
+ public WinGetCLITimeoutException(string command, string parameters)
+ : base(string.Format(Resources.WinGetCLITimeoutExceptionMessage, command, parameters))
+ {
+ }
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetIntegrityException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetIntegrityException.cs
new file mode 100644
index 0000000000..489b1d1539
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetIntegrityException.cs
@@ -0,0 +1,67 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Exceptions
+{
+ using System;
+ using Microsoft.WinGet.Client.Common;
+ using Microsoft.WinGet.Client.Properties;
+
+ ///
+ /// WinGet Integrity exception.
+ ///
+ public class WinGetIntegrityException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Category failure.
+ public WinGetIntegrityException(IntegrityCategory category)
+ : base(GetMessage(category))
+ {
+ this.Category = category;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Category failure.
+ /// Inner exception.
+ public WinGetIntegrityException(IntegrityCategory category, Exception inner)
+ : base(GetMessage(category), inner)
+ {
+ this.Category = category;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Category failure.
+ /// Message.
+ public WinGetIntegrityException(IntegrityCategory category, string message)
+ : base(message)
+ {
+ this.Category = category;
+ }
+
+ ///
+ /// Gets the category of the integrity failure.
+ ///
+ public IntegrityCategory Category { get; }
+
+ private static string GetMessage(IntegrityCategory category) => category switch
+ {
+ IntegrityCategory.Failure => Resources.IntegrityFailureMessage,
+ IntegrityCategory.NotInPath => Resources.IntegrityNotInPathMessage,
+ IntegrityCategory.AppExecutionAliasDisabled => Resources.IntegrityAppExecutionAliasDisabledMessage,
+ IntegrityCategory.OsNotSupported => Resources.IntegrityOsNotSupportedMessage,
+ IntegrityCategory.AppInstallerNotInstalled => Resources.IntegrityAppInstallerNotInstalledMessage,
+ IntegrityCategory.AppInstallerNotRegistered => Resources.IntegrityAppInstallerNotRegisteredMessage,
+ IntegrityCategory.AppInstallerNotSupported => Resources.IntegrityAppInstallerNotSupportedMessage,
+ _ => Resources.IntegrityUnknownMessage,
+ };
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetPackageNotInstalledException.cs b/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetPackageNotInstalledException.cs
deleted file mode 100644
index 3b5a2432e6..0000000000
--- a/src/PowerShell/Microsoft.WinGet.Client/Exceptions/WinGetPackageNotInstalledException.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// -----------------------------------------------------------------------------
-//
-// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
-//
-// -----------------------------------------------------------------------------
-
-namespace Microsoft.WinGet.Client.Exceptions
-{
- using System;
- using Microsoft.WinGet.Client.Properties;
-
- ///
- /// No package found.
- ///
- [Serializable]
- public class WinGetPackageNotInstalledException : Exception
- {
- ///
- /// Initializes a new instance of the class.
- ///
- public WinGetPackageNotInstalledException()
- : base(Resources.WinGetPackageNotInstalledMessage)
- {
- }
- }
-}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client/Helpers/AppxModuleHelper.cs
new file mode 100644
index 0000000000..43bfe45bb0
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Helpers/AppxModuleHelper.cs
@@ -0,0 +1,213 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Helpers
+{
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Management.Automation;
+ using System.Management.Automation.Runspaces;
+ using System.Runtime.InteropServices;
+ using System.Text;
+ using Microsoft.WinGet.Client.Common;
+ using Microsoft.WinGet.Client.Properties;
+
+ ///
+ /// Helper to make calls to the Appx module.
+ ///
+ internal class AppxModuleHelper
+ {
+ private const string GetAppxModule = "Get-Module Appx";
+ private const string ImportModuleCore = "Import-Module Appx -UseWindowsPowerShell";
+ private const string GetAppxPackageCommand = "Get-AppxPackage {0}";
+ private const string AddAppxPackageFormat = "Add-AppxPackage -Path {0}";
+ private const string AddAppxPackageRegisterFormat = "Add-AppxPackage -Path {0} -Register -DisableDevelopmentMode";
+ private const string ForceUpdateFromAnyVersion = " -ForceUpdateFromAnyVersion";
+ private const string GetAppxPackageByVersionCommand = "Get-AppxPackage {0} | Where-Object -Property Version -eq {1}";
+
+ private const string AppInstallerName = "Microsoft.DesktopAppInstaller";
+ private const string AppxManifest = "AppxManifest.xml";
+ private const string PackageFullName = "PackageFullName";
+
+ // Dependencies
+ private const string VCLibsUWPDesktop = "Microsoft.VCLibs.140.00.UWPDesktop";
+ private const string VCLibsUWPDesktopVersion = "14.0.30704.0";
+ private const string VCLibsUWPDesktopX64 = "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx";
+ private const string VCLibsUWPDesktopX86 = "https://aka.ms/Microsoft.VCLibs.x86.14.00.Desktop.appx";
+ private const string VCLibsUWPDesktopArm = "https://aka.ms/Microsoft.VCLibs.arm.14.00.Desktop.appx";
+ private const string VCLibsUWPDesktopArm64 = "https://aka.ms/Microsoft.VCLibs.arm64.14.00.Desktop.appx";
+
+ private const string UiXaml27 = "Microsoft.UI.Xaml.2.7";
+
+ private readonly PSCmdlet psCmdlet;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The calling cmdlet.
+ public AppxModuleHelper(PSCmdlet psCmdlet)
+ {
+ this.psCmdlet = psCmdlet;
+
+ // There's a bug in the Appx Module that it can't be loaded from Core in pre 10.0.22453.0 builds without
+ // the -UseWindowsPowerShell option. In post 10.0.22453.0 builds there's really no difference between
+ // using or not -UseWindowsPowerShell as it will automatically get loaded using WinPSCompatSession remoting session.
+ // https://github.com/PowerShell/PowerShell/issues/13138.
+#if !POWERSHELL_WINDOWS
+ var appxModule = this.psCmdlet.InvokeCommand.InvokeScript(GetAppxModule);
+ if (appxModule is null)
+ {
+ this.psCmdlet.InvokeCommand.InvokeScript(ImportModuleCore);
+ }
+#endif
+ }
+
+ ///
+ /// Calls Get-AppxPackage Microsoft.DesktopAppInstaller.
+ ///
+ /// Result of Get-AppxPackage.
+ public PSObject GetAppInstallerObject()
+ {
+ return this.GetAppxObject(AppInstallerName);
+ }
+
+ ///
+ /// Gets the string value a property from the Get-AppxPackage object of AppInstaller.
+ ///
+ /// Property name.
+ /// Value, null if doesn't exist.
+ public string GetAppInstallerPropertyValue(string propertyName)
+ {
+ string result = null;
+ var packageObj = this.GetAppInstallerObject();
+ if (packageObj is not null)
+ {
+ var property = packageObj.Properties.Where(p => p.Name == propertyName).FirstOrDefault();
+ if (property is not null)
+ {
+ result = property.Value as string;
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Calls Add-AppxPackage with the specified path.
+ ///
+ /// The path of the package to add.
+ /// If the package version is lower than the installed one.
+ public void AddAppInstallerBundle(string localPath, bool downgrade = false)
+ {
+ // A better implementation would use Add-AppxPackage with -DependencyPath, but
+ // the Appx module needs to be remoted into Windows PowerShell. When the string[] parameter
+ // gets deserialized from Core the result is a single string which breaks Add-AppxPackage.
+ // Here we should: if we are in Windows Powershell then run Add-AppxPackage with -DependencyPath
+ // if we are in Core, then start powershell.exe and run the same command. Right now, we just
+ // do Add-AppxPackage for each one.
+ this.InstallVCLibsDependencies();
+ this.InstallUiXaml();
+
+ StringBuilder sb = new StringBuilder();
+ sb.Append(string.Format(AddAppxPackageFormat, localPath));
+
+ if (downgrade)
+ {
+ sb.Append(ForceUpdateFromAnyVersion);
+ }
+
+ // Using this method simplifies a lot of things, but the error is not propagated with
+ // the default parameters. PipelineResultTypes.Error will at least output it in the terminal.
+ this.psCmdlet.InvokeCommand.InvokeScript(
+ sb.ToString(),
+ useNewScope: true,
+ PipelineResultTypes.Error,
+ input: null);
+ }
+
+ ///
+ /// Calls Add-AppxPackage to register with AppInstaller's AppxManifest.xml.
+ ///
+ public void RegisterAppInstaller()
+ {
+ string packageFullName = this.GetAppInstallerPropertyValue(PackageFullName);
+ string appxManifestPath = Path.Combine(
+ Utilities.ProgramFilesWindowsAppPath,
+ packageFullName,
+ AppxManifest);
+
+ this.psCmdlet.InvokeCommand.InvokeScript(
+ string.Format(AddAppxPackageRegisterFormat, appxManifestPath));
+ }
+
+ private PSObject GetAppxObject(string packageName)
+ {
+ return this.psCmdlet.InvokeCommand
+ .InvokeScript(string.Format(GetAppxPackageCommand, packageName))
+ .FirstOrDefault();
+ }
+
+ private IReadOnlyList GetVCLibsDependencies()
+ {
+ var vcLibsDependencies = new List();
+ var vcLibsPackageObjs = this.psCmdlet.InvokeCommand
+ .InvokeScript(string.Format(GetAppxPackageByVersionCommand, VCLibsUWPDesktop, VCLibsUWPDesktopVersion));
+ if (vcLibsPackageObjs is null ||
+ vcLibsPackageObjs.Count == 0)
+ {
+ var arch = RuntimeInformation.OSArchitecture;
+ if (arch == Architecture.X64)
+ {
+ vcLibsDependencies.Add(VCLibsUWPDesktopX64);
+ }
+ else if (arch == Architecture.X86)
+ {
+ vcLibsDependencies.Add(VCLibsUWPDesktopX86);
+ }
+ else if (arch == Architecture.Arm64)
+ {
+ // Deployment please figure out for me.
+ vcLibsDependencies.Add(VCLibsUWPDesktopX64);
+ vcLibsDependencies.Add(VCLibsUWPDesktopX86);
+ vcLibsDependencies.Add(VCLibsUWPDesktopArm);
+ vcLibsDependencies.Add(VCLibsUWPDesktopArm64);
+ }
+ else
+ {
+ throw new PSNotSupportedException(arch.ToString());
+ }
+ }
+ else
+ {
+ this.psCmdlet.WriteDebug($"VCLibs are updated.");
+ }
+
+ return vcLibsDependencies;
+ }
+
+ private void InstallVCLibsDependencies()
+ {
+ var packages = this.GetVCLibsDependencies();
+ foreach (var package in packages)
+ {
+ this.psCmdlet.WriteDebug($"Installing VCLibs {package}");
+ this.psCmdlet.InvokeCommand.InvokeScript(string.Format(AddAppxPackageFormat, package));
+ }
+ }
+
+ private void InstallUiXaml()
+ {
+ // TODO: We need to follow up for Microsoft.UI.Xaml.2.7
+ // downloading the nuget and extracting it doesn't sound like the right thing to do.
+ var uiXamlObjs = this.GetAppxObject(UiXaml27);
+ if (uiXamlObjs is null)
+ {
+ throw new PSNotImplementedException(Resources.MicrosoftUIXaml27Message);
+ }
+ }
+ }
+}
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Helpers/ComObjectFactory.cs b/src/PowerShell/Microsoft.WinGet.Client/Helpers/ComObjectFactory.cs
index 98a28fbfed..ccd28ccb41 100644
--- a/src/PowerShell/Microsoft.WinGet.Client/Helpers/ComObjectFactory.cs
+++ b/src/PowerShell/Microsoft.WinGet.Client/Helpers/ComObjectFactory.cs
@@ -9,7 +9,7 @@ namespace Microsoft.WinGet.Client.Factories
using System;
using System.Runtime.InteropServices;
using Microsoft.Management.Deployment;
- using Microsoft.WinGet.Client.Common;
+ using Microsoft.WinGet.Client.Common;
using Microsoft.WinGet.Client.Exceptions;
#if NET
@@ -123,7 +123,7 @@ private static T Create(Type type, in Guid iid)
{
if (hr == ErrorCode.FileNotFound)
{
- throw new WinGetPackageNotInstalledException();
+ throw new WinGetIntegrityException(IntegrityCategory.AppInstallerNotInstalled);
}
else
{
diff --git a/src/PowerShell/Microsoft.WinGet.Client/Helpers/GitHubRelease.cs b/src/PowerShell/Microsoft.WinGet.Client/Helpers/GitHubRelease.cs
new file mode 100644
index 0000000000..3e96d1c1c0
--- /dev/null
+++ b/src/PowerShell/Microsoft.WinGet.Client/Helpers/GitHubRelease.cs
@@ -0,0 +1,127 @@
+// -----------------------------------------------------------------------------
+//
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+//
+// -----------------------------------------------------------------------------
+
+namespace Microsoft.WinGet.Client.Helpers
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using Octokit;
+ using FileMode = System.IO.FileMode;
+
+ ///
+ /// Handles WinGet's releases in GitHub.
+ ///
+ internal class GitHubRelease
+ {
+ private const string Owner = "microsoft";
+ private const string Repo = "winget-cli";
+ private const string UserAgent = "winget-powershell";
+ private const string MsixBundleName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle";
+ private const string ContentType = "application/octet-stream";
+
+ private readonly IGitHubClient gitHubClient;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GitHubRelease()
+ {
+ this.gitHubClient = new GitHubClient(new ProductHeaderValue(UserAgent));
+ }
+
+ ///
+ /// Download a release from winget-cli.
+ ///
+ /// Optional release name. If null, gets latest.
+ /// Path where the msix bundle is downloaded.
+ public string DownloadRelease(string releaseTag)
+ {
+ return this.DownloadReleaseAsync(releaseTag).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Gets the latest released version and waits.
+ ///
+ /// Include prerelease.
+ /// Latest version.
+ public string GetLatestVersionTagName(bool includePreRelease)
+ {
+ return this.GetLatestVersionAsync(includePreRelease).GetAwaiter().GetResult().TagName;
+ }
+
+ ///
+ /// Downloads a file from a url and waits.
+ ///
+ /// Url.
+ /// File name.
+ public void DownloadUrl(string url, string fileName)
+ {
+ this.DownloadUrlAsync(url, fileName).GetAwaiter().GetResult();
+ }
+
+ ///
+ /// Download asynchronously a release from winget-cli.
+ ///
+ /// Optional release name. If null, gets latest.
+ /// Path where the msix bundle is downloaded.
+ public async Task DownloadReleaseAsync(string releaseTag)
+ {
+ Release release = await this.gitHubClient.Repository.Release.Get(Owner, Repo, releaseTag);
+
+ // Get asset and download.
+ var msixBundleAsset = release.Assets.Where(a => a.Name == MsixBundleName).First();
+
+ var tmpFile = Path.GetTempFileName();
+ await this.DownloadUrlAsync(msixBundleAsset.Url, tmpFile);
+ return tmpFile;
+ }
+
+ ///
+ /// Downloads a file from a url.
+ ///
+ /// Url.
+ /// File name.
+ /// A representing the asynchronous operation.
+ public async Task DownloadUrlAsync(string url, string fileName)
+ {
+ var response = await this.gitHubClient.Connection.Get