commit d637f0e86f1f96b3dbf9b1e96892538ab2063ba8 parent 2287ad2e3bd76a7e11adb6ea794ab33f51750ef0 Author: Ruben Guerrero <rubengu@microsoft.com> Date: Fri, 14 Jul 2023 12:58:49 -0700 Repair-WinGetPackageManager improvements (#3423) This PR address #3374 Change Make Repair-WinGetPackageManager repair known issues until its fixed or there's a non fixable state. Adds support for installing Microsoft.UI.Xaml.2.7 package from their GitHub release. Adds new parameter switch -AllUsers to Repair-WinGetPackageManager. If this is on, repair uses Add-AppxProvisionedPackage instead of Add-AppxPackage. Adds new integrity category to detect winget.exe failures due to missing license. To fix it Repair-WinGetPackageManager -AllUsers must be executed in admin mode. Fix adding preprocessor macros for net48. This cause Repair-WinGetPackageManager to always fail for Windows PowerShell. There's a breaking change in Repair-WinGetPackageManager. It will now throw if there's an issue repairing. Good thing this is a "prerelease" module 🗡️ Validation Test locally on machines where Microsoft.UI.Xaml.2.7 was not preinstalled and on Windows Server 2022. Diffstat:
15 files changed, 700 insertions(+), 341 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -475,6 +475,7 @@ UWP VALUENAMECASE VERSI VERSIE +vclib vns vsconfig vstest diff --git a/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Client.Cmdlets/Cmdlets/RepairWinGetPackageManagerCmdlet.cs @@ -22,6 +22,12 @@ namespace Microsoft.WinGet.Client.Commands public class RepairWinGetPackageManagerCmdlet : WinGetPackageManagerCmdlet { /// <summary> + /// Gets or sets a value indicating whether to repair for all users. Requires admin. + /// </summary> + [Parameter(ValueFromPipelineByPropertyName = true)] + public SwitchParameter AllUsers { get; set; } + + /// <summary> /// Attempts to repair winget. /// TODO: consider WhatIf and Confirm options. /// </summary> @@ -30,11 +36,11 @@ namespace Microsoft.WinGet.Client.Commands var command = new WinGetPackageManagerCommand(this); if (this.ParameterSetName == Constants.IntegrityLatestSet) { - command.RepairUsingLatest(this.IncludePreRelease.ToBool()); + command.RepairUsingLatest(this.IncludePreRelease.ToBool(), this.AllUsers.ToBool()); } else { - command.Repair(this.Version); + command.Repair(this.Version, this.AllUsers.ToBool()); } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs @@ -7,11 +7,14 @@ namespace Microsoft.WinGet.Client.Engine.Commands { using System; + using System.Collections.Generic; using System.Management.Automation; using Microsoft.WinGet.Client.Engine.Commands.Common; using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Client.Engine.Exceptions; using Microsoft.WinGet.Client.Engine.Helpers; using Microsoft.WinGet.Client.Engine.Properties; + using static Microsoft.WinGet.Client.Engine.Common.Constants; /// <summary> /// Used by Repair-WinGetPackageManager and Assert-WinGetPackageManager. @@ -19,10 +22,6 @@ namespace Microsoft.WinGet.Client.Engine.Commands public sealed class WinGetPackageManagerCommand : BaseCommand { private const string EnvPath = "env:PATH"; - private const int Succeeded = 0; - private const int Failed = -1; - - private static readonly string[] WriteInformationTags = new string[] { "PSHOST" }; /// <summary> /// Initializes a new instance of the <see cref="WinGetPackageManagerCommand"/> class. @@ -39,8 +38,8 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// <param name="preRelease">Use prerelease version on GitHub.</param> public void AssertUsingLatest(bool preRelease) { - var gitHubRelease = new GitHubRelease(); - string expectedVersion = gitHubRelease.GetLatestVersionTagName(preRelease); + var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + string expectedVersion = gitHubClient.GetLatestVersionTagName(preRelease); this.Assert(expectedVersion); } @@ -57,144 +56,132 @@ namespace Microsoft.WinGet.Client.Engine.Commands /// Repairs winget using the latest version on winget-cli. /// </summary> /// <param name="preRelease">Use prerelease version on GitHub.</param> - public void RepairUsingLatest(bool preRelease) + /// <param name="allUsers">Install for all users. Requires admin.</param> + public void RepairUsingLatest(bool preRelease, bool allUsers) { - var gitHubRelease = new GitHubRelease(); - string expectedVersion = gitHubRelease.GetLatestVersionTagName(preRelease); - this.Repair(expectedVersion); + var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + string expectedVersion = gitHubClient.GetLatestVersionTagName(preRelease); + this.Repair(expectedVersion, allUsers); } /// <summary> /// Repairs winget if needed. /// </summary> /// <param name="expectedVersion">The expected version, if any.</param> - public void Repair(string expectedVersion) + /// <param name="allUsers">Install for all users. Requires admin.</param> + public void Repair(string expectedVersion, bool allUsers) { - int result = Failed; - - var integrityCategory = WinGetIntegrity.GetIntegrityCategory(this.PsCmdlet, expectedVersion); - this.PsCmdlet.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.PsCmdlet, expectedVersion); - this.PsCmdlet.WriteDebug($"Integrity category after fixing PATH {newIntegrityCategory}"); - result = this.VerifyWinGetInstall(newIntegrityCategory, expectedVersion); - } - else if (integrityCategory == IntegrityCategory.AppInstallerNotRegistered) + if (allUsers) { - var appxModule = new AppxModuleHelper(this.PsCmdlet); - appxModule.RegisterAppInstaller(); - - // Now try again and get the desired winget version if needed. - var newIntegrityCategory = WinGetIntegrity.GetIntegrityCategory(this.PsCmdlet, expectedVersion); - this.PsCmdlet.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)) + if (Utilities.ExecutingAsSystem) { - var gitHubRelease = new GitHubRelease(); - expectedVersion = gitHubRelease.GetLatestVersionTagName(false); + throw new NotSupportedException(); } - if (this.DownloadAndInstall(expectedVersion, false)) - { - result = Succeeded; - } - else + if (!Utilities.ExecutingAsAdministrator) { - this.PsCmdlet.WriteDebug($"Failed installing {expectedVersion}"); + throw new WinGetRepairException(Resources.RepairAllUsersMessage); } } - else if (integrityCategory == IntegrityCategory.AppExecutionAliasDisabled) - { - // Sorry, but the user has to manually enabled it. - this.PsCmdlet.WriteInformation(Resources.AppExecutionAliasDisabledHelpMessage, WriteInformationTags); - } - else - { - this.PsCmdlet.WriteInformation(Resources.WinGetNotSupportedMessage, WriteInformationTags); - } - this.PsCmdlet.WriteObject(result); + this.RepairStateMachine(expectedVersion, allUsers); } - private int VerifyWinGetInstall(IntegrityCategory integrityCategory, string expectedVersion) + private void RepairStateMachine(string expectedVersion, bool allUsers) { - if (integrityCategory == IntegrityCategory.Installed) - { - // Nothing to do - this.PsCmdlet.WriteDebug($"WinGet is in a good state."); - return Succeeded; - } - else if (integrityCategory == IntegrityCategory.UnexpectedVersion) + var seenCategories = new HashSet<IntegrityCategory>(); + + var currentCategory = IntegrityCategory.Unknown; + while (currentCategory != IntegrityCategory.Installed) { - // The versions are different, download and install. - if (!this.InstallDifferentVersion(new WinGetVersion(expectedVersion))) + try { - this.PsCmdlet.WriteDebug($"Failed installing {expectedVersion}"); + WinGetIntegrity.AssertWinGet(this.PsCmdlet, expectedVersion); + this.PsCmdlet.WriteDebug($"WinGet is in a good state."); + currentCategory = IntegrityCategory.Installed; } - else + catch (WinGetIntegrityException e) { - return Succeeded; + currentCategory = e.Category; + + if (seenCategories.Contains(currentCategory)) + { + this.PsCmdlet.WriteDebug($"{currentCategory} encountered previously"); + throw; + } + + this.PsCmdlet.WriteDebug($"Integrity category type: {currentCategory}"); + seenCategories.Add(currentCategory); + + switch (currentCategory) + { + case IntegrityCategory.UnexpectedVersion: + this.InstallDifferentVersion(new WinGetVersion(expectedVersion), allUsers); + break; + case IntegrityCategory.NotInPath: + this.RepairEnvPath(); + break; + case IntegrityCategory.AppInstallerNotRegistered: + this.Register(); + break; + case IntegrityCategory.AppInstallerNotInstalled: + case IntegrityCategory.AppInstallerNotSupported: + case IntegrityCategory.Failure: + this.Install(expectedVersion, allUsers); + break; + case IntegrityCategory.AppInstallerNoLicense: + // This requires -AllUsers in admin mode. + if (allUsers && Utilities.ExecutingAsAdministrator) + { + this.Install(expectedVersion, allUsers); + } + else + { + throw new WinGetRepairException(e); + } + + break; + case IntegrityCategory.AppExecutionAliasDisabled: + case IntegrityCategory.Unknown: + throw new WinGetRepairException(e); + default: + throw new NotSupportedException(); + } } } - - return Failed; } - private bool InstallDifferentVersion(WinGetVersion toInstallVersion) + private void InstallDifferentVersion(WinGetVersion toInstallVersion, bool allUsers) { var installedVersion = WinGetVersion.InstalledWinGetVersion; + bool isDowngrade = installedVersion.CompareAsDeployment(toInstallVersion) > 0; - this.PsCmdlet.WriteDebug($"Installed WinGet version {installedVersion.TagVersion}"); - this.PsCmdlet.WriteDebug($"Installing WinGet version {toInstallVersion.TagVersion}"); + this.PsCmdlet.WriteDebug($"Installed WinGet version '{installedVersion.TagVersion}' " + + $"Installing WinGet version '{toInstallVersion.TagVersion}' " + + $"Is downgrade {isDowngrade}"); + var appxModule = new AppxModuleHelper(this.PsCmdlet); + appxModule.InstallFromGitHubRelease(toInstallVersion.TagVersion, allUsers, isDowngrade); + } - bool downgrade = false; - if (installedVersion.CompareAsDeployment(toInstallVersion) > 0) + private void Install(string toInstallVersion, bool allUsers) + { + // If we are here and toInstallVersion 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(toInstallVersion)) { - downgrade = true; + var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + toInstallVersion = gitHubClient.GetLatestVersionTagName(false); } - return this.DownloadAndInstall(toInstallVersion.TagVersion, downgrade); + var appxModule = new AppxModuleHelper(this.PsCmdlet); + appxModule.InstallFromGitHubRelease(toInstallVersion, allUsers, false); } - private bool DownloadAndInstall(string versionTag, bool downgrade) + private void Register() { - using var tempFile = new TempFile(); - - // Download and install. - var gitHubRelease = new GitHubRelease(); - gitHubRelease.DownloadRelease(versionTag, tempFile.FullPath); - var appxModule = new AppxModuleHelper(this.PsCmdlet); - appxModule.AddAppInstallerBundle(tempFile.FullPath, downgrade); - - // Verify that is installed - var integrityCategory = WinGetIntegrity.GetIntegrityCategory(this.PsCmdlet, versionTag); - if (integrityCategory != IntegrityCategory.Installed) - { - this.PsCmdlet.WriteDebug($"Failed installing {versionTag}. IntegrityCategory after attempt: '{integrityCategory}'"); - return false; - } - - this.PsCmdlet.WriteDebug($"Installed WinGet version {versionTag}"); - return true; + appxModule.RegisterAppInstaller(); } private void RepairEnvPath() diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Constants.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/Constants.cs @@ -33,5 +33,32 @@ namespace Microsoft.WinGet.Client.Engine.Common /// Name of PATH environment variable. /// </summary> public const string PathEnvVar = "PATH"; + + /// <summary> + /// Repository owners. + /// </summary> + public class RepositoryOwner + { + /// <summary> + /// Microsoft org. + /// </summary> + public const string Microsoft = "microsoft"; + } + + /// <summary> + /// Repository names. + /// </summary> + public class RepositoryName + { + /// <summary> + /// https://github.com/microsoft/winget-cli . + /// </summary> + public const string WinGetCli = "winget-cli"; + + /// <summary> + /// https://github.com/microsoft/microsoft-ui-xaml . + /// </summary> + public const string UiXaml = "microsoft-ui-xaml"; + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/IntegrityCategory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/IntegrityCategory.cs @@ -60,5 +60,10 @@ namespace Microsoft.WinGet.Client.Engine.Common /// Installed App Installer package is not supported. /// </summary> AppInstallerNotSupported, + + /// <summary> + /// No applicable license found. + /// </summary> + AppInstallerNoLicense, } } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Common/WinGetIntegrity.cs @@ -74,35 +74,34 @@ namespace Microsoft.WinGet.Client.Engine.Common IntegrityCategory.UnexpectedVersion, string.Format( Resources.IntegrityUnexpectedVersionMessage, - installedVersion, + installedVersion.TagVersion, expectedVersion)); } } } - /// <summary> - /// Verifies winget runs correctly. - /// </summary> - /// <param name="psCmdlet">The calling cmdlet.</param> - /// <param name="expectedVersion">Expected version.</param> - /// <returns>Integrity category.</returns> - public static IntegrityCategory GetIntegrityCategory(PSCmdlet psCmdlet, string expectedVersion) + private static IntegrityCategory GetReason(PSCmdlet psCmdlet) { + // Ok, so you are here because calling winget --version failed. Lets try to figure out why. + + // When running winget.exe on PowerShell the message of the Win32Exception will distinguish between + // 'The system cannot find the file specified' and 'No applicable app licenses found' but of course + // the HRESULT is the same (E_FAIL). + // To not compare strings let Powershell handle it. If calling winget throws an + // ApplicationFailedException then is most likely that the license is not there. try { - AssertWinGet(psCmdlet, expectedVersion); + var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); + ps.AddCommand("winget").Invoke(); } - catch (WinGetIntegrityException e) + catch (ApplicationFailedException e) + { + psCmdlet.WriteDebug(e.Message); + return IntegrityCategory.AppInstallerNoLicense; + } + catch (Exception) { - 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)) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetIntegrityException.cs @@ -62,6 +62,7 @@ namespace Microsoft.WinGet.Client.Engine.Exceptions IntegrityCategory.AppInstallerNotInstalled => Resources.IntegrityAppInstallerNotInstalledMessage, IntegrityCategory.AppInstallerNotRegistered => Resources.IntegrityAppInstallerNotRegisteredMessage, IntegrityCategory.AppInstallerNotSupported => Resources.IntegrityAppInstallerNotSupportedMessage, + IntegrityCategory.AppInstallerNoLicense => Resources.IntegrityAppInstallerLicense, _ => Resources.IntegrityUnknownMessage, }; } diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetRepairException.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Exceptions/WinGetRepairException.cs @@ -0,0 +1,70 @@ +// ----------------------------------------------------------------------------- +// <copyright file="WinGetRepairException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Exceptions +{ + using System; + using System.Management.Automation; + using Microsoft.WinGet.Client.Engine.Common; + using Microsoft.WinGet.Client.Engine.Properties; + + /// <summary> + /// WinGet repair exception. + /// </summary> + [Serializable] + public class WinGetRepairException : RuntimeException + { + /// <summary> + /// Initializes a new instance of the <see cref="WinGetRepairException"/> class. + /// </summary> + /// <param name="ie">Integrity exception.</param> + public WinGetRepairException(WinGetIntegrityException ie) + : base(GetMessage(ie), ie) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WinGetRepairException"/> class. + /// </summary> + /// <param name="e">Inner exception.</param> + public WinGetRepairException(Exception e) + : base(Resources.RepairFailureMessage, e) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WinGetRepairException"/> class. + /// </summary> + public WinGetRepairException() + : base(Resources.RepairFailureMessage) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WinGetRepairException"/> class. + /// </summary> + /// <param name="message">Message.</param>. + public WinGetRepairException(string message) + : base(message) + { + } + + private static string GetMessage(WinGetIntegrityException ie) + { + string message = Resources.RepairFailureMessage; + if (ie.Category == IntegrityCategory.AppInstallerNoLicense) + { + message += $" {Resources.RepairAllUsersHelpMessage}"; + } + else if (ie.Category == IntegrityCategory.AppExecutionAliasDisabled) + { + message += $" {Resources.RepairAppExecutionAliasMessage}"; + } + + return message; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs @@ -12,7 +12,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers using System.Management.Automation; using System.Runtime.InteropServices; using Microsoft.WinGet.Client.Engine.Common; - using Microsoft.WinGet.Client.Engine.Properties; + using static Microsoft.WinGet.Client.Engine.Common.Constants; /// <summary> /// Helper to make calls to the Appx module. @@ -23,17 +23,21 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private const string ImportModule = "Import-Module"; private const string GetAppxPackage = "Get-AppxPackage"; private const string AddAppxPackage = "Add-AppxPackage"; + private const string AddAppxProvisionedPackage = "Add-AppxProvisionedPackage"; // Parameters name private const string Name = "Name"; private const string Path = "Path"; private const string ErrorAction = "ErrorAction"; private const string WarningAction = "WarningAction"; + private const string PackagePath = "PackagePath"; + private const string LicensePath = "LicensePath"; // Parameter Values private const string Appx = "Appx"; private const string Stop = "Stop"; private const string SilentlyContinue = "SilentlyContinue"; + private const string Online = "Online"; // Options private const string UseWindowsPowerShell = "UseWindowsPowerShell"; @@ -45,7 +49,12 @@ namespace Microsoft.WinGet.Client.Engine.Helpers private const string AppxManifest = "AppxManifest.xml"; private const string PackageFullName = "PackageFullName"; + // Assets + private const string MsixBundleName = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"; + private const string License = "License1.xml"; + // Dependencies + // VCLibs 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"; @@ -53,7 +62,13 @@ namespace Microsoft.WinGet.Client.Engine.Helpers 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"; + // Xaml + private const string XamlPackage27 = "Microsoft.UI.Xaml.2.7"; + private const string XamlReleaseTag273 = "v2.7.3"; + private const string XamlAssetX64 = "Microsoft.UI.Xaml.2.7.x64.appx"; + private const string XamlAssetX86 = "Microsoft.UI.Xaml.2.7.x86.appx"; + private const string XamlAssetArm = "Microsoft.UI.Xaml.2.7.arm.appx"; + private const string XamlAssetArm64 = "Microsoft.UI.Xaml.2.7.arm64.appx"; private readonly PSCmdlet psCmdlet; @@ -97,46 +112,6 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } /// <summary> - /// Calls Add-AppxPackage with the specified path. - /// </summary> - /// <param name="localPath">The path of the package to add.</param> - /// <param name="downgrade">If the package version is lower than the installed one.</param> - 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(); - - var options = new List<string>(); - if (downgrade) - { - options.Add(ForceUpdateFromAnyVersion); - } - - try - { - _ = this.ExecuteAppxCmdlet( - AddAppxPackage, - new Dictionary<string, object> - { - { Path, localPath }, - { ErrorAction, Stop }, - }, - options); - } - catch (RuntimeException e) - { - this.psCmdlet.WriteError(e.ErrorRecord); - throw e; - } - } - - /// <summary> /// Calls Add-AppxPackage to register with AppInstaller's AppxManifest.xml. /// </summary> public void RegisterAppInstaller() @@ -160,6 +135,93 @@ namespace Microsoft.WinGet.Client.Engine.Helpers }); } + /// <summary> + /// Install AppInstaller's bundle from a GitHub release. + /// </summary> + /// <param name="releaseTag">Release tag of GitHub release.</param> + /// <param name="allUsers">If install for all users is needed.</param> + /// <param name="isDowngrade">Is downgrade.</param> + public void InstallFromGitHubRelease(string releaseTag, bool allUsers, bool isDowngrade) + { + this.InstallDependencies(); + + if (isDowngrade) + { + // Add-AppxProvisionedPackage doesn't support downgrade. + this.AddAppInstallerBundle(releaseTag, true); + + if (allUsers) + { + this.AddProvisionPackage(releaseTag); + } + } + else + { + if (allUsers) + { + this.AddProvisionPackage(releaseTag); + } + else + { + this.AddAppInstallerBundle(releaseTag, false); + } + } + } + + private void AddProvisionPackage(string releaseTag) + { + var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + var release = githubClient.GetRelease(releaseTag); + + using var bundleFile = new TempFile(); + var bundleAsset = release.Assets.Where(a => a.Name == MsixBundleName).First(); + githubClient.DownloadUrl(bundleAsset.BrowserDownloadUrl, bundleFile.FullPath); + + using var licenseFile = new TempFile(); + var licenseAsset = release.Assets.Where(a => a.Name.EndsWith(License)).First(); + githubClient.DownloadUrl(licenseAsset.BrowserDownloadUrl, licenseFile.FullPath); + + try + { + var ps = PowerShell.Create(RunspaceMode.CurrentRunspace); + ps.AddCommand(AddAppxProvisionedPackage) + .AddParameter(Online) + .AddParameter(PackagePath, bundleFile.FullPath) + .AddParameter(LicensePath, licenseFile.FullPath) + .AddParameter(ErrorAction, Stop) + .Invoke(); + } + catch (RuntimeException e) + { + this.psCmdlet.WriteDebug($"Failed installing bundle via Add-AppxProvisionedPackage {e}"); + throw e; + } + } + + private void AddAppInstallerBundle(string releaseTag, bool downgrade) + { + var options = new List<string>(); + if (downgrade) + { + options.Add(ForceUpdateFromAnyVersion); + } + + try + { + var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); + var release = githubClient.GetRelease(releaseTag); + + using var bundleFile = new TempFile(); + var bundleAsset = release.Assets.Where(a => a.Name == MsixBundleName).First(); + this.AddAppxPackageAsUri(bundleAsset.BrowserDownloadUrl, options); + } + catch (RuntimeException e) + { + this.psCmdlet.WriteDebug($"Failed installing bundle via Add-AppxPackage {e}"); + throw e; + } + } + private PSObject GetAppxObject(string packageName) { return this.ExecuteAppxCmdlet( @@ -171,10 +233,20 @@ namespace Microsoft.WinGet.Client.Engine.Helpers .FirstOrDefault(); } - private IReadOnlyList<string> GetVCLibsDependencies() + private void InstallDependencies() { - var vcLibsDependencies = new List<string>(); + // 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(); + } + private void InstallVCLibsDependencies() + { var result = this.ExecuteAppxCmdlet( GetAppxPackage, new Dictionary<string, object> @@ -200,6 +272,8 @@ namespace Microsoft.WinGet.Client.Engine.Helpers if (!isInstalled) { this.psCmdlet.WriteDebug("Couldn't find required VCLibs package"); + + var vcLibsDependencies = new List<string>(); var arch = RuntimeInformation.OSArchitecture; if (arch == Architecture.X64) { @@ -221,46 +295,69 @@ namespace Microsoft.WinGet.Client.Engine.Helpers { throw new PSNotSupportedException(arch.ToString()); } + + foreach (var vclib in vcLibsDependencies) + { + this.AddAppxPackageAsUri(vclib); + } } else { this.psCmdlet.WriteDebug($"VCLibs are updated."); } - - return vcLibsDependencies; - } - - private void InstallVCLibsDependencies() - { - var packages = this.GetVCLibsDependencies(); - foreach (var package in packages) - { - this.AddAppxPackageAsUri(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); + var uiXamlObjs = this.GetAppxObject(XamlPackage27); if (uiXamlObjs is null) { - throw new PSNotImplementedException(Resources.MicrosoftUIXaml27Message); + var githubRelease = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.UiXaml); + + var xamlRelease = githubRelease.GetRelease(XamlReleaseTag273); + + var packagesToInstall = new List<string>(); + var arch = RuntimeInformation.OSArchitecture; + if (arch == Architecture.X64) + { + packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetX64).First().BrowserDownloadUrl); + } + else if (arch == Architecture.X86) + { + packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetX86).First().BrowserDownloadUrl); + } + else if (arch == Architecture.Arm64) + { + // Deployment please figure out for me. + packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetX64).First().BrowserDownloadUrl); + packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetX86).First().BrowserDownloadUrl); + packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetArm).First().BrowserDownloadUrl); + packagesToInstall.Add(xamlRelease.Assets.Where(a => a.Name == XamlAssetArm64).First().BrowserDownloadUrl); + } + else + { + throw new PSNotSupportedException(arch.ToString()); + } + + foreach (var package in packagesToInstall) + { + this.AddAppxPackageAsUri(package); + } } } - private void AddAppxPackageAsUri(string packageUri) + private void AddAppxPackageAsUri(string packageUri, IList<string> options = null) { try { _ = this.ExecuteAppxCmdlet( - AddAppxPackage, - new Dictionary<string, object> - { - { Path, packageUri }, - { ErrorAction, Stop }, - }); + AddAppxPackage, + new Dictionary<string, object> + { + { Path, packageUri }, + { ErrorAction, Stop }, + }, + options); } catch (RuntimeException e) { @@ -268,7 +365,7 @@ namespace Microsoft.WinGet.Client.Engine.Helpers if (e.ErrorRecord.CategoryInfo.Category == ErrorCategory.OpenError) { this.psCmdlet.WriteDebug($"Failed adding package [{packageUri}]. Retrying downloading it."); - this.DownloadPackageAndAdd(packageUri); + this.DownloadPackageAndAdd(packageUri, options); } else { @@ -278,21 +375,22 @@ namespace Microsoft.WinGet.Client.Engine.Helpers } } - private void DownloadPackageAndAdd(string packageUrl) + private void DownloadPackageAndAdd(string packageUrl, IList<string> options) { - var tempFile = new TempFile(); + using var tempFile = new TempFile(); // This is weird but easy. - var githubRelease = new GitHubRelease(); + var githubRelease = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli); githubRelease.DownloadUrl(packageUrl, tempFile.FullPath); _ = this.ExecuteAppxCmdlet( - AddAppxPackage, - new Dictionary<string, object> - { - { Path, tempFile.FullPath }, - { ErrorAction, Stop }, - }); + AddAppxPackage, + new Dictionary<string, object> + { + { Path, tempFile.FullPath }, + { ErrorAction, Stop }, + }, + options); } private Collection<PSObject> ExecuteAppxCmdlet(string cmdlet, Dictionary<string, object> parameters = null, IList<string> options = null) diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs @@ -0,0 +1,123 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GitHubClient.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading.Tasks; + using Octokit; + using FileMode = System.IO.FileMode; + + /// <summary> + /// Handles GitHub interactions. + /// </summary> + internal class GitHubClient + { + private const string UserAgent = "winget-powershell"; + private const string ContentType = "application/octet-stream"; + + private readonly string owner; + private readonly string repo; + + private readonly IGitHubClient gitHubClient; + + /// <summary> + /// Initializes a new instance of the <see cref="GitHubClient"/> class. + /// </summary> + /// <param name="owner">Owner.</param> + /// <param name="repo">Repository.</param> + public GitHubClient(string owner, string repo) + { + this.gitHubClient = new Octokit.GitHubClient(new ProductHeaderValue(UserAgent)); + this.owner = owner; + this.repo = repo; + } + + /// <summary> + /// Gets a release. + /// </summary> + /// <param name="releaseTag">Release tag.</param> + /// <returns>The Release.</returns> + public Release GetRelease(string releaseTag) + { + return this.GetReleaseAsync(releaseTag).GetAwaiter().GetResult(); + } + + /// <summary> + /// Gets a release. + /// </summary> + /// <param name="releaseTag">Release tag.</param> + /// <returns>The Release.</returns> + public async Task<Release> GetReleaseAsync(string releaseTag) + { + return await this.gitHubClient.Repository.Release.Get(this.owner, this.repo, releaseTag); + } + + /// <summary> + /// Gets the latest released version and waits. + /// </summary> + /// <param name="includePreRelease">Include prerelease.</param> + /// <returns>Latest version.</returns> + public string GetLatestVersionTagName(bool includePreRelease) + { + return this.GetLatestVersionAsync(includePreRelease).GetAwaiter().GetResult().TagName; + } + + /// <summary> + /// Downloads a file from a url and waits. + /// </summary> + /// <param name="url">Url.</param> + /// <param name="fileName">File name.</param> + public void DownloadUrl(string url, string fileName) + { + this.DownloadUrlAsync(url, fileName).GetAwaiter().GetResult(); + } + + /// <summary> + /// Downloads a file from a url. + /// </summary> + /// <param name="url">Url.</param> + /// <param name="fileName">File name.</param> + /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> + public async Task DownloadUrlAsync(string url, string fileName) + { + var response = await this.gitHubClient.Connection.Get<object>( + new Uri(url), + new Dictionary<string, string>(), + ContentType); + + using var memoryStream = new MemoryStream((byte[])response.Body); + using var fileStream = File.Open(fileName, FileMode.OpenOrCreate); + memoryStream.Position = 0; + await memoryStream.CopyToAsync(fileStream); + } + + /// <summary> + /// Gets the latest released version. + /// </summary> + /// <param name="includePreRelease">Include prerelease.</param> + /// <returns>Latest version.</returns> + internal async Task<Release> GetLatestVersionAsync(bool includePreRelease) + { + Release release; + + // GetLatest doesn't respect prerelease or gives an option to get it. + if (includePreRelease) + { + // GetAll orders by newest and includes pre releases. + release = (await this.gitHubClient.Repository.Release.GetAll(this.owner, this.repo))[0]; + } + else + { + release = await this.gitHubClient.Repository.Release.GetLatest(this.owner, this.repo); + } + + return release; + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubRelease.cs @@ -1,126 +0,0 @@ -// ----------------------------------------------------------------------------- -// <copyright file="GitHubRelease.cs" company="Microsoft Corporation"> -// Copyright (c) Microsoft Corporation. Licensed under the MIT License. -// </copyright> -// ----------------------------------------------------------------------------- - -namespace Microsoft.WinGet.Client.Engine.Helpers -{ - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Threading.Tasks; - using Octokit; - using FileMode = System.IO.FileMode; - - /// <summary> - /// Handles WinGet's releases in GitHub. - /// </summary> - 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; - - /// <summary> - /// Initializes a new instance of the <see cref="GitHubRelease"/> class. - /// </summary> - public GitHubRelease() - { - this.gitHubClient = new GitHubClient(new ProductHeaderValue(UserAgent)); - } - - /// <summary> - /// Download a release from winget-cli. - /// </summary> - /// <param name="releaseTag">Optional release name. If null, gets latest.</param> - /// <param name="outputFile">Output file.</param> - public void DownloadRelease(string releaseTag, string outputFile) - { - this.DownloadReleaseAsync(releaseTag, outputFile).GetAwaiter().GetResult(); - } - - /// <summary> - /// Gets the latest released version and waits. - /// </summary> - /// <param name="includePreRelease">Include prerelease.</param> - /// <returns>Latest version.</returns> - public string GetLatestVersionTagName(bool includePreRelease) - { - return this.GetLatestVersionAsync(includePreRelease).GetAwaiter().GetResult().TagName; - } - - /// <summary> - /// Downloads a file from a url and waits. - /// </summary> - /// <param name="url">Url.</param> - /// <param name="fileName">File name.</param> - public void DownloadUrl(string url, string fileName) - { - this.DownloadUrlAsync(url, fileName).GetAwaiter().GetResult(); - } - - /// <summary> - /// Download asynchronously a release from winget-cli. - /// </summary> - /// <param name="releaseTag">Optional release name. If null, gets latest.</param> - /// <param name="outputFile">Output file.</param> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - public async Task DownloadReleaseAsync(string releaseTag, string outputFile) - { - 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(); - - await this.DownloadUrlAsync(msixBundleAsset.Url, outputFile); - } - - /// <summary> - /// Downloads a file from a url. - /// </summary> - /// <param name="url">Url.</param> - /// <param name="fileName">File name.</param> - /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> - public async Task DownloadUrlAsync(string url, string fileName) - { - var response = await this.gitHubClient.Connection.Get<object>( - new Uri(url), - new Dictionary<string, string>(), - ContentType); - - using var memoryStream = new MemoryStream((byte[])response.Body); - using var fileStream = File.Open(fileName, FileMode.OpenOrCreate); - memoryStream.Position = 0; - await memoryStream.CopyToAsync(fileStream); - } - - /// <summary> - /// Gets the latest released version. - /// </summary> - /// <param name="includePreRelease">Include prerelease.</param> - /// <returns>Latest version.</returns> - internal async Task<Release> GetLatestVersionAsync(bool includePreRelease) - { - Release release; - - // GetLatest doesn't respect prerelease or gives an option to get it. - if (includePreRelease) - { - // GetAll orders by newest and includes pre releases. - release = (await this.gitHubClient.Repository.Release.GetAll(Owner, Repo))[0]; - } - else - { - release = await this.gitHubClient.Repository.Release.GetLatest(Owner, Repo); - } - - return release; - } - } -} diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempDirectory.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/TempDirectory.cs @@ -0,0 +1,118 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TempDirectory.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Client.Engine.Helpers +{ + using System; + using System.IO; + + /// <summary> + /// Creates a temporary directory in the user's temporary directory. + /// </summary> + internal class TempDirectory : IDisposable + { + private readonly bool cleanup; + private bool disposed = false; + + /// <summary> + /// Initializes a new instance of the <see cref="TempDirectory"/> class. + /// </summary> + /// <param name="directoryName">Optional directory name. If null, creates a random directory name.</param> + /// <param name="deleteIfExists">Delete directory if already exists. Default true.</param> + /// <param name="cleanup">Deletes directory at disposing time. Default true.</param> + public TempDirectory( + string directoryName = null, + bool deleteIfExists = true, + bool cleanup = true) + { + if (directoryName is null) + { + this.DirectoryName = Path.GetRandomFileName(); + } + else + { + this.DirectoryName = directoryName; + } + + this.FullDirectoryPath = Path.Combine(Path.GetTempPath(), this.DirectoryName); + + if (deleteIfExists && Directory.Exists(this.FullDirectoryPath)) + { + Directory.Delete(this.FullDirectoryPath, true); + } + + Directory.CreateDirectory(this.FullDirectoryPath); + this.cleanup = cleanup; + } + + /// <summary> + /// Gets the directory name. + /// </summary> + public string DirectoryName { get; } + + /// <summary> + /// Gets the full directory name. + /// </summary> + public string FullDirectoryPath { get; } + + /// <summary> + /// IDisposable.Dispose . + /// </summary> + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Copies all contents of a directory into this directory. + /// </summary> + /// <param name="sourceDir">Source directory.</param> + public void CopyDirectory(string sourceDir) + { + this.CopyDirectory(sourceDir, this.FullDirectoryPath); + } + + /// <summary> + /// Protected disposed. + /// </summary> + /// <param name="disposing">Disposing.</param> + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (this.cleanup && Directory.Exists(this.FullDirectoryPath)) + { + Directory.Delete(this.FullDirectoryPath, true); + } + + this.disposed = true; + } + } + + private void CopyDirectory(string sourceDir, string destinationDir) + { + var dir = new DirectoryInfo(sourceDir); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException(dir.FullName); + } + + Directory.CreateDirectory(destinationDir); + + foreach (FileInfo file in dir.GetFiles()) + { + file.CopyTo(Path.Combine(destinationDir, file.Name)); + } + + foreach (DirectoryInfo subDir in dir.GetDirectories()) + { + this.CopyDirectory(subDir.FullName, Path.Combine(destinationDir, subDir.Name)); + } + } + } +} 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 @@ -22,7 +22,7 @@ </PropertyGroup> <PropertyGroup Condition="'$(UseProdCLSIDs)' == 'true'"> - <DefineConstants>USE_PROD_CLSIDS</DefineConstants> + <DefineConstants>$(DefineConstants);USE_PROD_CLSIDS</DefineConstants> </PropertyGroup> <ItemGroup> @@ -118,7 +118,7 @@ </ItemGroup> <PropertyGroup Condition="'$(TargetFramework)' == '$(DesktopFramework)'"> - <DefineConstants>POWERSHELL_WINDOWS</DefineConstants> + <DefineConstants>$(DefineConstants);POWERSHELL_WINDOWS</DefineConstants> </PropertyGroup> <ItemGroup> diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.Designer.cs @@ -61,15 +61,6 @@ namespace Microsoft.WinGet.Client.Engine.Properties { } /// <summary> - /// Looks up a localized string similar to The App Execution Alias for the Windows Package Manager is disabled. You should enable the App Execution Alias for the Windows Package Manager. Go to App execution aliases option in Apps & features Settings to enable it.. - /// </summary> - internal static string AppExecutionAliasDisabledHelpMessage { - get { - return ResourceManager.GetString("AppExecutionAliasDisabledHelpMessage", resourceCulture); - } - } - - /// <summary> /// Looks up a localized string similar to An error occurred while connecting to the catalog.. /// </summary> internal static string CatalogConnectExceptionMessage { @@ -97,6 +88,15 @@ namespace Microsoft.WinGet.Client.Engine.Properties { } /// <summary> + /// Looks up a localized string similar to No applicable license found.. + /// </summary> + internal static string IntegrityAppInstallerLicense { + get { + return ResourceManager.GetString("IntegrityAppInstallerLicense", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The App Installer is not installed.. /// </summary> internal static string IntegrityAppInstallerNotInstalledMessage { @@ -232,6 +232,42 @@ namespace Microsoft.WinGet.Client.Engine.Properties { } /// <summary> + /// Looks up a localized string similar to Try running with -AllUsers in administrator mode.. + /// </summary> + internal static string RepairAllUsersHelpMessage { + get { + return ResourceManager.GetString("RepairAllUsersHelpMessage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to -AllUsers requires administrator mode.. + /// </summary> + internal static string RepairAllUsersMessage { + get { + return ResourceManager.GetString("RepairAllUsersMessage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The App Execution Alias for the Windows Package Manager is disabled. You should enable the App Execution Alias for the Windows Package Manager. Go to App execution aliases option in Apps & features Settings to enable it.. + /// </summary> + internal static string RepairAppExecutionAliasMessage { + get { + return ResourceManager.GetString("RepairAppExecutionAliasMessage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Failed to repair winget.. + /// </summary> + internal static string RepairFailureMessage { + get { + return ResourceManager.GetString("RepairFailureMessage", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Single threaded apartment (STA) is not currently supported in this context; run PowerShell in Multi-threaded apartment mode (MTA).. /// </summary> internal static string SingleThreadedApartmentNotSupportedMessage { @@ -259,7 +295,7 @@ namespace Microsoft.WinGet.Client.Engine.Properties { } /// <summary> - /// Looks up a localized string similar to This cmdlet is no supported in Windows PowerShell. + /// Looks up a localized string similar to This cmdlet is not supported in Windows PowerShell.. /// </summary> internal static string WindowsPowerShellNotSupported { get { diff --git a/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx b/src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/Resources.resx @@ -186,7 +186,7 @@ <data name="IntegrityAppInstallerNotRegisteredMessage" xml:space="preserve"> <value>The App Installer is not registered.</value> </data> - <data name="AppExecutionAliasDisabledHelpMessage" xml:space="preserve"> + <data name="RepairAppExecutionAliasMessage" xml:space="preserve"> <value>The App Execution Alias for the Windows Package Manager is disabled. You should enable the App Execution Alias for the Windows Package Manager. Go to App execution aliases option in Apps & features Settings to enable it.</value> </data> <data name="MicrosoftUIXaml27Message" xml:space="preserve"> @@ -207,4 +207,18 @@ <data name="WindowsPowerShellNotSupported" xml:space="preserve"> <value>This cmdlet is not supported in Windows PowerShell.</value> </data> + <data name="IntegrityAppInstallerLicense" xml:space="preserve"> + <value>No applicable license found.</value> + </data> + <data name="RepairAllUsersHelpMessage" xml:space="preserve"> + <value>Try running with -AllUsers in administrator mode.</value> + <comment>{Locked="-AllUsers"}</comment> + </data> + <data name="RepairAllUsersMessage" xml:space="preserve"> + <value>-AllUsers requires administrator mode.</value> + <comment>{Locked="-AllUsers"}</comment> + </data> + <data name="RepairFailureMessage" xml:space="preserve"> + <value>Failed to repair winget.</value> + </data> </root> \ No newline at end of file